From 5329ddd61c54d525628c390843da9583065a419c Mon Sep 17 00:00:00 2001 From: Johannes Theiner Date: Mon, 22 Nov 2021 14:12:32 +0100 Subject: [PATCH] + add api for other plugins(References #3) + Better settings interface ~ migrate back to esbuild as the erroneous dependency is no longer used. --- .editorconfig | 9 +++++ .eslintignore | 2 ++ README.md | 21 +++++++++-- esbuild.config.mjs | 29 +++++++++++++++ manifest.json | 4 +-- package.json | 12 +++---- rollup.config.js | 31 ---------------- src/LanguageVoiceModal.ts | 29 +++++++++++---- src/main.ts | 74 +++++++++++++++++++++++++-------------- src/settings.ts | 26 +++++++++++--- versions.json | 3 +- 11 files changed, 159 insertions(+), 81 deletions(-) create mode 100644 .editorconfig create mode 100644 .eslintignore create mode 100644 esbuild.config.mjs delete mode 100644 rollup.config.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..c9cfdb3 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +insert_final_newline = true +indent_style = tab +indent_size = 4 +tab_width = 4 diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..b178d27 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,2 @@ +npm node_modules +build diff --git a/README.md b/README.md index 2d60aeb..9eac121 100644 --- a/README.md +++ b/README.md @@ -6,14 +6,16 @@ Plugin for [Obsidian](https://obsidian.md) ![GitHub](https://img.shields.io/github/license/joethei/obsidian-tts) [![libera manifesto](https://img.shields.io/badge/libera-manifesto-lightgrey.svg)](https://liberamanifesto.com) --- +**This plugin is currently in beta** You can create language specific voices, which will be used when you have a note with -```lang: {language name}``` +```lang: {languageCode}``` in the [Frontmatter](https://help.obsidian.md/Advanced+topics/YAML+front+matter). +The language code can be seen in the settings and is a two letter [ISO 639-1](https://www.loc.gov/standards/iso639-2/php/English_list.php) code. -This plugin will not work on android due to [this bug in the Webview](https://bugs.chromium.org/p/chromium/issues/detail?id=487255). +This plugin will **NOT** work on android due to [this bug in the Webview](https://bugs.chromium.org/p/chromium/issues/detail?id=487255). ## Adding languages @@ -23,3 +25,18 @@ to add a new language reference the documentation accordingly: - [MacOS](https://support.apple.com/guide/mac-help/change-the-system-language-mh26684/mac) - [iOS](https://support.apple.com/guide/iphone/change-the-language-and-region-iphce20717a3/ios) + +## API +You can use this plugins API to add Text to Speech capabilities to your plugin. + +```js +//@ts-ignore +if(this.app.plugins.plugins["obsidian-tts"]) {//check if the plugin is loaded + //@ts-ignore + await this.app.plugins.plugins["obsidian-tts"].say(title, text, language);//language is optional +} +``` +Parameters: +- title: Title of your text, will only be spoken if the the user has the setting enabled +- text +- language(optional): language code according to the [ISO 639-1](https://www.loc.gov/standards/iso639-2/php/English_list.php), if there is no voice configured for that language, the plugin will use the default voice. diff --git a/esbuild.config.mjs b/esbuild.config.mjs new file mode 100644 index 0000000..575a560 --- /dev/null +++ b/esbuild.config.mjs @@ -0,0 +1,29 @@ +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 +https://github.com/joethei/obsidian-rss +*/ +`; + +const prod = (process.argv[2] === 'production'); + +esbuild.build({ + banner: { + js: banner, + }, + entryPoints: ['src/main.ts'], + bundle: true, + external: ['obsidian', 'electron', ...builtins], + format: 'cjs', + watch: !prod, + target: 'es2016', + logLevel: "info", + sourcemap: prod ? false : 'inline', + treeShaking: true, + outfile: 'main.js', +}).catch(() => process.exit(1)); diff --git a/manifest.json b/manifest.json index ebd9cce..6295fb4 100644 --- a/manifest.json +++ b/manifest.json @@ -1,9 +1,9 @@ { "id": "obsidian-tts", "name": "Text to Speech", - "version": "0.1.0", + "version": "0.2.0", "minAppVersion": "0.12.0", - "description": "This is a sample plugin for Obsidian. This plugin demonstrates some of the capabilities of the Obsidian API.", + "description": "Text to speech for Obsidian. Hear your notes.", "author": "Johannes Theiner", "authorUrl": "https://github.com/joethei", "isDesktopOnly": false diff --git a/package.json b/package.json index 066f883..41acd4b 100644 --- a/package.json +++ b/package.json @@ -1,25 +1,23 @@ { "name": "obsidian-tts", - "version": "0.1.0", + "version": "0.2.0", "description": "Text to speech for Obsidian. Hear your notes.", "main": "main.js", "scripts": { - "dev": "rollup --config rollup.config.js -w", - "build": "rollup --config rollup.config.js --environment BUILD:production", + "dev": "node esbuild.config.mjs", + "build": "node esbuild.config.mjs production", "lint": "eslint . --ext .ts" }, "keywords": [], "author": "joethei", "license": "GPL-3.0", "devDependencies": { - "@rollup/plugin-commonjs": "^21.0.1", - "@rollup/plugin-node-resolve": "^13.0.6", - "@rollup/plugin-typescript": "^8.3.0", - "rollup": "^2.60.0", + "@cospired/i18n-iso-languages": "^3.1.1", "@types/node": "^16.11.6", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "builtin-modules": "^3.2.0", + "esbuild": "0.13.12", "eslint": "^7.32.0", "obsidian": "^0.12.17", "tslib": "2.3.1", diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index 4c96d82..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,31 +0,0 @@ -import typescript from "@rollup/plugin-typescript"; -import { nodeResolve } from "@rollup/plugin-node-resolve"; -import commonjs from "@rollup/plugin-commonjs"; - -const isProd = process.env.BUILD === "production"; - - -const banner = `/* -THIS IS A GENERATED/BUNDLED FILE BY ROLLUP -if you want to view the source visit the plugins github repository -https://github.com/joethei/obsidian-tts -*/ -`; - -export default { - input: "src/main.ts", - output: { - sourcemap: "inline", - sourcemapExcludeSources: isProd, - format: "cjs", - exports: "default", - file: "main.js", - banner, - }, - external: ["obsidian"], - plugins: [ - typescript(), - nodeResolve({ browser: true }), - commonjs(), - ], -}; diff --git a/src/LanguageVoiceModal.ts b/src/LanguageVoiceModal.ts index 8da86d6..d1d8ed6 100644 --- a/src/LanguageVoiceModal.ts +++ b/src/LanguageVoiceModal.ts @@ -2,6 +2,7 @@ import {Modal, Setting} from "obsidian"; import {LanguageVoiceMap} from "./settings"; import {TextInputPrompt} from "./TextInputPrompt"; import TTSPlugin from "./main"; +import languages from "@cospired/i18n-iso-languages"; export class LanguageVoiceModal extends Modal { plugin: TTSPlugin; @@ -25,13 +26,29 @@ export class LanguageVoiceModal extends Modal { contentEl.empty(); + //not know to rollup and webstorm, but exists in obsidian + //@ts-ignore + const languageNames = new Intl.DisplayNames(['en'], {type: 'language'}); + new Setting(contentEl) .setName("Language") - .setDesc("what name this will be referenced by in frontmatter") - .addText((text) => { - text.setValue(this.language) + .addDropdown(async (dropdown) => { + + for (let languageCodeKey in languages.getAlpha2Codes()) { + //@ts-ignore + const displayNames = new Intl.DisplayNames([languageCodeKey], {type: 'language', fallback: 'none'}); + if(displayNames) { + const name = displayNames.of(languageCodeKey); + if(name) { + dropdown.addOption(languageCodeKey, name); + } + } + } + + dropdown + .setValue(this.language) .onChange((value) => { - this.language = value; + this.language = value; }); }); @@ -40,7 +57,7 @@ export class LanguageVoiceModal extends Modal { .addDropdown(async (dropdown) => { const voices = window.speechSynthesis.getVoices(); for (const voice of voices) { - dropdown.addOption(voice.name, voice.lang + " " + voice.name); + dropdown.addOption(voice.name, voice.name + " - " + languageNames.of(voice.lang)); } dropdown .setValue(this.voice) @@ -55,7 +72,7 @@ export class LanguageVoiceModal extends Modal { const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian"); await input.openAndGetValue((async value => { if (value.getValue().length === 0) return; - await this.plugin.playText(value.getValue(), this.voice); + await this.plugin.sayWithVoice('', value.getValue(), this.voice); })); diff --git a/src/main.ts b/src/main.ts index f871d6a..39548ab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -156,17 +156,59 @@ export default class TTSPlugin extends Plugin { }); } - async playText(text: string, voice?: string): Promise { - const configuredVoice = (voice) ? voice : this.settings.defaultVoice; + async sayWithVoice(title: string, text: string, voice: string): Promise { + console.log("saying " + voice); + let content = text; + if (!this.settings.speakSyntax) { + content = content.replace(/#/g, ""); + content = content.replace("-", ""); + content = content.replace("_", ""); + } + if (!this.settings.speakLinks) { + content = content.replace(/(?:https?|ftp|file|data:):\/\/[\n\S]+/g, ''); + } + if(!this.settings.speakCodeblocks) { + content = content.replace(/```[\s\S]*?```/g, ''); + } + + if (this.settings.speakTitle) { + content = title + " " + content; + } + + + const msg = new SpeechSynthesisUtterance(); - msg.text = text; + msg.text = content; msg.volume = this.settings.volume; msg.rate = this.settings.rate; msg.pitch = this.settings.pitch; - msg.voice = window.speechSynthesis.getVoices().filter(voice => voice.name === configuredVoice)[0]; + msg.voice = window.speechSynthesis.getVoices().filter(otherVoice => otherVoice.name === voice)[0]; window.speechSynthesis.speak(msg); + this.statusbar.setText("TTS: playing"); } + + getVoice(languageCode: string) : string { + const filtered = this.settings.languageVoices.filter((lang) => lang.language === languageCode); + if (filtered.length === 0) return null; + return filtered[0].voice; + } + + async say(title: string, text: string, languageCode?: string): Promise { + let usedVoice = this.settings.defaultVoice; + if (languageCode) { + const voice = this.getVoice(languageCode); + if (voice) { + usedVoice = voice; + } else { + new Notice("TTS: could not find voice for language " + languageCode + ". Using default voice."); + } + } + + await this.sayWithVoice(title, text, usedVoice); + } + + async play(view: MarkdownView): Promise { let content = view.getViewData(); let language: string; @@ -186,30 +228,8 @@ export default class TTSPlugin extends Plugin { content = content.substring(content.indexOf("---") + 1); } - if (!this.settings.speakSyntax) { - content = content.replace(/#/g, ""); - content = content.replace("-", ""); - content = content.replace("_", ""); - } - if (!this.settings.speakLinks) { - content = content.replace(/(?:https?|ftp|file|data:):\/\/[\n\S]+/g, ''); - } - if (this.settings.speakTitle) { - content = view.getDisplayText() + content; - } - - if (language != undefined) { - const entry = this.settings.languageVoices.filter(item => item.language == language)[0]; - if (!entry) { - new Notice("TTS: could not find voice for language " + language); - return; - } - await this.playText(content, entry.voice); - } else { - await this.playText(content); - } + await this.say(view.getDisplayText(), content, language); - this.statusbar.setText("TTS: playing"); } async onunload(): Promise { diff --git a/src/settings.ts b/src/settings.ts index 1d57e65..6dceb55 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -16,6 +16,7 @@ export interface TTSSettings { speakLinks: boolean; speakFrontmatter: boolean; speakSyntax: boolean; + speakCodeblocks: boolean; speakTitle: boolean; languageVoices: LanguageVoiceMap[]; } @@ -29,6 +30,7 @@ export const DEFAULT_SETTINGS: TTSSettings = { speakFrontmatter: false, speakSyntax: false, speakTitle: true, + speakCodeblocks: false, languageVoices: [] } @@ -52,7 +54,7 @@ export class TTSSettingsTab extends PluginSettingTab { .addDropdown(async (dropdown) => { const voices = window.speechSynthesis.getVoices(); for (const voice of voices) { - dropdown.addOption(voice.name, voice.lang + " " + voice.name); + dropdown.addOption(voice.name, voice.name); } dropdown .setValue(this.plugin.settings.defaultVoice) @@ -68,7 +70,7 @@ export class TTSSettingsTab extends PluginSettingTab { const input = new TextInputPrompt(this.app, "What do you want to hear?", "", "Hello world this is Text to speech running in obsidian", "Hello world this is Text to speech running in obsidian"); await input.openAndGetValue((async value => { if (value.getValue().length === 0) return; - await this.plugin.playText(value.getValue()); + await this.plugin.say('', value.getValue()); })); @@ -82,7 +84,7 @@ export class TTSSettingsTab extends PluginSettingTab { .setDesc("Add a new language specific voice") .addButton((button: ButtonComponent): ButtonComponent => { return button - .setTooltip("add new Feed") + .setTooltip("add new language specific voice") .setIcon("create-new") .onClick(async () => { const modal = new LanguageVoiceModal(this.plugin); @@ -107,9 +109,12 @@ export class TTSSettingsTab extends PluginSettingTab { const voicesDiv = additionalContainer.createDiv("voices"); for (const languageVoice of this.plugin.settings.languageVoices) { + console.log(languageVoice); - const setting = new Setting(voicesDiv); - setting.setName(languageVoice.language); + //@ts-ignore + const displayNames = new Intl.DisplayNames([languageVoice.language], {type: 'language', fallback: 'none'}); + const setting = new Setting(voicesDiv); + setting.setName(displayNames.of(languageVoice.language) + " - " + languageVoice.language); setting.setDesc(languageVoice.voice); setting @@ -244,6 +249,17 @@ export class TTSSettingsTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName("Codeblocks") + .addToggle(async (toggle) => { + toggle + .setValue(this.plugin.settings.speakCodeblocks) + .onChange(async (value) => { + this.plugin.settings.speakCodeblocks = value; + await this.plugin.saveSettings(); + }); + }); + new Setting(containerEl) .setName("Syntax") .addToggle(async (toggle) => { diff --git a/versions.json b/versions.json index 934b633..700e6c9 100644 --- a/versions.json +++ b/versions.json @@ -1,3 +1,4 @@ { - "0.1.0": "0.9.12" + "0.1.0": "0.9.12", + "0.2.0": "0.9.12" }