mirror of
https://github.com/RealKai42/qwerty-learner-vscode.git
synced 2024-11-25 16:52:37 +08:00
refact: refact current code base
This commit is contained in:
parent
645e900926
commit
a5a80d7c68
15
.prettierrc
15
.prettierrc
@ -1,15 +0,0 @@
|
||||
{
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all",
|
||||
"printWidth": 140,
|
||||
"bracketSpacing": true,
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"jsxSingleQuote": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ".prettierrc",
|
||||
"options": { "parser": "json" }
|
||||
}
|
||||
]
|
||||
}
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -14,5 +14,5 @@
|
||||
},
|
||||
"css.lint.unknownAtRules": "ignore",
|
||||
"css.validate": false,
|
||||
"cSpell.words": ["usphone", "ukphone"]
|
||||
"cSpell.words": ["rodio", "romaji", "ukphone", "usphone"]
|
||||
}
|
||||
|
20
README.md
20
README.md
@ -33,13 +33,13 @@
|
||||
|
||||
本项目为 [Qwerty Learner](https://github.com/Kaiyiwing/qwerty-learner) 的 VSCode 插件版本,访问原始项目获得更好的体验。
|
||||
|
||||
(注:依赖 VSCode 最低版本为 1.53.0,如提示 ` it is not compatible with the current version of VS Code` 请升级 VSCode 版本)
|
||||
(注:依赖 VSCode 最低版本为 1.53.0,如提示 `it is not compatible with the current version of VS Code` 请升级 VSCode 版本)
|
||||
|
||||
## ✨ 实现原理
|
||||
|
||||
因为 VSC 没有提供对 Keypress 的回调,所以实现上使用了较为取巧的方式,监听用户当前输入文档的改变,然后删除用户输入。 用户可以在任意代码、文档页面开启软件进行英语打字练习,插件会自动删除用户输入的文字,不会对文档内容造成影响。
|
||||
|
||||
目前存在的 Bug,在用户输入速度较快(特别是同时按下多个键)时,可能会导致删除不完全,用户自行删除输入即可。目前处于项目初期,可能会频繁更新,还望见谅,
|
||||
目前存在的 Bug,在用户输入速度较快(特别是同时按下多个键)时,可能会导致删除不完全,用户自行删除输入即可。
|
||||
|
||||
## 🎛 使用说明
|
||||
|
||||
@ -64,9 +64,8 @@
|
||||
- Change Chapter 可以切换章节
|
||||
- Change Dictionary 可以切换字典
|
||||
- Start/Pause 可以开关插件,功能等价于一键启动快捷键
|
||||
- Open Read Only Mode 开启只读模式
|
||||
- Close Read Only Mode 关闭只读模式
|
||||
- Toggle Word Visibility 切换是否展示单词(默写模式)
|
||||
- Toggle Read Only Mode 开关只读模式
|
||||
|
||||
命令面板快捷键
|
||||
Mac: `cmd + shift + p`
|
||||
@ -76,7 +75,7 @@ Win: `ctrl + shift + p`
|
||||
|
||||
可以在设置面板查找关键字 “Qwerty” 修改设置
|
||||
|
||||
```
|
||||
```json
|
||||
"qwerty-learner.highlightWrongColor": {
|
||||
"type": "string",
|
||||
"default": "#EE3D11",
|
||||
@ -109,10 +108,12 @@ Win: `ctrl + shift + p`
|
||||
"maximum": 100,
|
||||
"description": "每个章节包含的单词数量"
|
||||
},
|
||||
"qwerty-learner.reWrite": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "是否开启循环模式(循环单一章节)"
|
||||
"qwerty-learner.wordExerciseTime": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"description": "每个单词的练习次数"
|
||||
},
|
||||
"qwerty-learner.readOnlyInterval": {
|
||||
"type": "number",
|
||||
@ -193,3 +194,4 @@ Win: `ctrl + shift + p`
|
||||
- Essential Words
|
||||
- suffix word
|
||||
- word roots1
|
||||
- ...
|
||||
|
87
package.json
87
package.json
@ -2,7 +2,7 @@
|
||||
"name": "qwerty-learner",
|
||||
"displayName": "Qwerty Learner",
|
||||
"description": "为 Coder 设计的单词记忆与英语肌肉记忆锻炼软件 摸🐟版",
|
||||
"version": "0.2.5",
|
||||
"version": "0.2.6",
|
||||
"publisher": "kaiyi",
|
||||
"icon": "docs/logo.png",
|
||||
"engines": {
|
||||
@ -27,16 +27,21 @@
|
||||
"url": "https://github.com/Kaiyiwing/qwerty-learner-vscode/"
|
||||
},
|
||||
"homepage": "https://github.com/Kaiyiwing/qwerty-learner-vscode",
|
||||
"activationEvents": [
|
||||
"onCommand:qwerty-learner.Start",
|
||||
"onCommand:qwerty-learner.changeChapter",
|
||||
"onCommand:qwerty-learner.changeDict"
|
||||
],
|
||||
"main": "./dist/extension",
|
||||
"scripts": {
|
||||
"pretest": "yarn run compile && yarn run lint",
|
||||
"lint": "eslint src --ext ts",
|
||||
"test": "node ./out/test/runTest.js",
|
||||
"vscode:prepublish": "webpack --mode production",
|
||||
"compile": "webpack --mode development",
|
||||
"watch": "webpack --mode development --watch",
|
||||
"test-compile": "tsc -p ./",
|
||||
"vsce:publish": "vsce package"
|
||||
},
|
||||
"main": "./dist/index",
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{
|
||||
"command": "qwerty-learner.Start",
|
||||
"command": "qwerty-learner.start",
|
||||
"title": "Qwerty Learner Start/Pause"
|
||||
},
|
||||
{
|
||||
@ -47,34 +52,18 @@
|
||||
"command": "qwerty-learner.changeDict",
|
||||
"title": "Qwerty Learner Change Dictionary"
|
||||
},
|
||||
{
|
||||
"command": "qwerty-learner.closeReadOnlyMode",
|
||||
"title": "Qwerty Learner Close Read Only Mode"
|
||||
},
|
||||
{
|
||||
"command": "qwerty-learner.openReadOnlyMode",
|
||||
"title": "Qwerty Learner Open Read Only Mode"
|
||||
},
|
||||
{
|
||||
"command": "qwerty-learner.toggleWordVisibility",
|
||||
"title": "Qwerty Learner Toggle Word Visibility"
|
||||
},
|
||||
{
|
||||
"command": "qwerty-learner.toggleReadOnlyMode",
|
||||
"title": "Qwerty Learner Toggle Read Only Mode"
|
||||
}
|
||||
],
|
||||
"menus": {
|
||||
"commandPalette": [
|
||||
{
|
||||
"command": "qwerty-learner.closeReadOnlyMode",
|
||||
"when": "qwerty-learner.readOnlyMode"
|
||||
},
|
||||
{
|
||||
"command": "qwerty-learner.openReadOnlyMode",
|
||||
"when": "!qwerty-learner.readOnlyMode"
|
||||
}
|
||||
]
|
||||
},
|
||||
"keybindings": [
|
||||
{
|
||||
"command": "qwerty-learner.Start",
|
||||
"command": "qwerty-learner.start",
|
||||
"key": "shift+alt+q",
|
||||
"mac": "ctrl+shift+q",
|
||||
"when": "editorTextFocus"
|
||||
@ -83,16 +72,6 @@
|
||||
"configuration": {
|
||||
"title": "Qwerty Learner",
|
||||
"properties": {
|
||||
"qwerty-learner.highlightWrongColor": {
|
||||
"type": "string",
|
||||
"default": "#EE3D11",
|
||||
"description": "输入错误时单词的颜色"
|
||||
},
|
||||
"qwerty-learner.highlightWrongDelay": {
|
||||
"type": "number",
|
||||
"default": 400,
|
||||
"description": "输入错误时清空输入的延迟时间"
|
||||
},
|
||||
"qwerty-learner.keySound": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
@ -115,10 +94,22 @@
|
||||
"maximum": 100,
|
||||
"description": "每个章节包含的单词数量"
|
||||
},
|
||||
"qwerty-learner.reWrite": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "是否开启循环模式(循环单一章节)"
|
||||
"qwerty-learner.wordExerciseTime": {
|
||||
"type": "integer",
|
||||
"default": 1,
|
||||
"minimum": 1,
|
||||
"maximum": 100,
|
||||
"description": "每个单词的练习次数"
|
||||
},
|
||||
"qwerty-learner.highlightWrongColor": {
|
||||
"type": "string",
|
||||
"default": "#EE3D11",
|
||||
"description": "输入错误时单词的颜色"
|
||||
},
|
||||
"qwerty-learner.highlightWrongDelay": {
|
||||
"type": "number",
|
||||
"default": 400,
|
||||
"description": "输入错误时清空输入的延迟时间"
|
||||
},
|
||||
"qwerty-learner.readOnlyInterval": {
|
||||
"type": "number",
|
||||
@ -149,16 +140,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"pretest": "yarn run compile && yarn run lint",
|
||||
"lint": "eslint src --ext ts",
|
||||
"test": "node ./out/test/runTest.js",
|
||||
"vscode:prepublish": "webpack --mode production",
|
||||
"compile": "webpack --mode development",
|
||||
"watch": "webpack --mode development --watch",
|
||||
"test-compile": "tsc -p ./",
|
||||
"vsce:publish": "vsce package"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/glob": "^7.1.3",
|
||||
"@types/lodash": "^4.14.168",
|
||||
|
15
prettier.config.js
Normal file
15
prettier.config.js
Normal file
@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
singleQuote: true,
|
||||
trailingComma: 'all',
|
||||
printWidth: 140,
|
||||
bracketSpacing: true,
|
||||
semi: false,
|
||||
tabWidth: 2,
|
||||
jsxSingleQuote: false,
|
||||
overrides: [
|
||||
{
|
||||
files: '.prettierrc',
|
||||
options: { parser: 'json' },
|
||||
},
|
||||
],
|
||||
}
|
269
src/extension.ts
269
src/extension.ts
@ -1,269 +0,0 @@
|
||||
import * as vscode from 'vscode'
|
||||
import cet4 from './assets/CET4_T.json'
|
||||
import { range } from 'lodash'
|
||||
import { compareWord, getConfig, dicts, DictPickItem, getDictFile } from './utils'
|
||||
import { soundPlayer } from './sound'
|
||||
import { voicePlayer, getVoiceType } from './voice'
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const globalState = context.globalState
|
||||
globalState.setKeysForSync(['chapter', 'order', 'dictKey'])
|
||||
let chapterLength = getConfig('chapterLength')
|
||||
let readOnlyMode = globalState.get('readOnlyMode', false)
|
||||
let readOnlyInterval = getConfig('readOnlyInterval')
|
||||
let readOnlyIntervalId: NodeJS.Timeout | null = null
|
||||
let placeholder = getConfig('placeholder') // 用于控制word不可见时,inputBar中是否出现占位符及样式
|
||||
let wordVisibility = globalState.get('wordVisibility', true)
|
||||
let prevOrder = globalState.get('order', 0)
|
||||
if (prevOrder > chapterLength) {
|
||||
prevOrder = 0
|
||||
}
|
||||
let isStart = false,
|
||||
hasWrong = false,
|
||||
chapter = globalState.get('chapter', 0),
|
||||
order = prevOrder,
|
||||
dict = cet4,
|
||||
dictKey = 'cet4',
|
||||
voiceType = getVoiceType(),
|
||||
voiceLock = false
|
||||
let wordList = dict.slice(chapter * chapterLength, (chapter + 1) * chapterLength)
|
||||
let totalChapters = Math.ceil(dict.length / chapterLength)
|
||||
let inputBarIndex = 0 // 当前inputBar将要输入到的下标
|
||||
const wordBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -100)
|
||||
const inputBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -101)
|
||||
const transBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -102)
|
||||
changeDict(globalState.get('dictKey', 'cet4'))
|
||||
|
||||
function setupWord() {
|
||||
if (order === wordList.length) {
|
||||
if (!getConfig('reWrite')) {
|
||||
if (chapter === totalChapters - 1) {
|
||||
chapter = 0
|
||||
} else {
|
||||
chapter += 1
|
||||
}
|
||||
}
|
||||
order = 0
|
||||
wordList = dict.slice(chapter * chapterLength, (chapter + 1) * chapterLength)
|
||||
}
|
||||
let phonetic = ''
|
||||
switch (getConfig('phonetic')) {
|
||||
case 'us':
|
||||
phonetic = wordList[order].usphone || ''
|
||||
break
|
||||
case 'uk':
|
||||
phonetic = wordList[order].ukphone || ''
|
||||
break
|
||||
case 'close':
|
||||
phonetic = ''
|
||||
break
|
||||
}
|
||||
|
||||
// API 字典会出现括号,但部分 vscode 插件会拦截括号的输入
|
||||
wordList[order].name = wordList[order].name.replace('(', '').replace(')', '')
|
||||
wordBar.text = `${dicts[dictKey].name} chp.${chapter + 1} ${order + 1}/${wordList.length} ${
|
||||
wordVisibility ? wordList[order].name : ''
|
||||
}`
|
||||
if (wordVisibility || placeholder === '') {
|
||||
inputBar.text = ''
|
||||
} else {
|
||||
// 拼接占位符
|
||||
inputBar.text = placeholder.repeat(wordList[order].name.length)
|
||||
}
|
||||
inputBarIndex = 0
|
||||
transBar.text = phonetic ? `/${phonetic}/ ` : ''
|
||||
transBar.text += wordList[order].trans.join('; ')
|
||||
if (voiceType && !voiceLock) {
|
||||
voiceLock = true
|
||||
voicePlayer(wordList[order].name, voiceType, () => {
|
||||
voiceLock = false
|
||||
})
|
||||
}
|
||||
updateGlobalState()
|
||||
}
|
||||
|
||||
function refreshWordList() {
|
||||
totalChapters = Math.ceil(dict.length / chapterLength)
|
||||
wordList = dict.slice(chapter * chapterLength, (chapter + 1) * chapterLength)
|
||||
setupWord()
|
||||
}
|
||||
|
||||
function changeDict(key: string) {
|
||||
if (key === 'cet4') {
|
||||
dict = cet4
|
||||
} else {
|
||||
dict = getDictFile(dicts[key].url)
|
||||
}
|
||||
dictKey = key
|
||||
refreshWordList()
|
||||
}
|
||||
|
||||
function updateGlobalState() {
|
||||
globalState.update('chapter', chapter)
|
||||
globalState.update('order', order)
|
||||
globalState.update('dictKey', dictKey)
|
||||
}
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration(function (event) {
|
||||
const configList = ['qwerty-learner.chapterLength']
|
||||
const affected = configList.some((item) => event.affectsConfiguration(item))
|
||||
if (affected) {
|
||||
chapter = 0
|
||||
order = 0
|
||||
chapterLength = getConfig('chapterLength')
|
||||
refreshWordList()
|
||||
}
|
||||
|
||||
// 修改了placeholder只需要更新placeholder
|
||||
if (event.affectsConfiguration('qwerty-learner.placeholder')) {
|
||||
placeholder = getConfig('placeholder')
|
||||
setupWord()
|
||||
}
|
||||
})
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
if (isStart && !readOnlyMode) {
|
||||
const { uri } = e.document
|
||||
// 避免破坏配置文件
|
||||
if (uri.scheme.indexOf('vscode') !== -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const { range, text, rangeLength } = e.contentChanges[0]
|
||||
|
||||
if (text !== '' && text.length === 1) {
|
||||
// 删除用户输入的字符
|
||||
const newRange = new vscode.Range(range.start.line, range.start.character, range.end.line, range.end.character + 1)
|
||||
const editAction = new vscode.WorkspaceEdit()
|
||||
editAction.delete(uri, newRange)
|
||||
vscode.workspace.applyEdit(editAction)
|
||||
if (!hasWrong) {
|
||||
soundPlayer('click')
|
||||
let curInput = ''
|
||||
if (wordVisibility || placeholder === '') {
|
||||
// 没有使用placeholder,不需要特殊处理
|
||||
inputBar.text += text
|
||||
curInput = inputBar.text
|
||||
} else {
|
||||
// 拼接占位符 && 获取当前已经键入的值进行比较
|
||||
inputBar.text = inputBar.text.substring(0, inputBarIndex) + text + inputBar.text.substring(inputBarIndex + 1)
|
||||
inputBarIndex += 1
|
||||
curInput = inputBar.text.substring(0, inputBarIndex)
|
||||
}
|
||||
const result = compareWord(wordList[order].name, curInput)
|
||||
if (result === -2) {
|
||||
order++
|
||||
soundPlayer('success')
|
||||
// 可能单词太短,播放发音回调还没执行,就输完切下一个词了,这里在切换下一个词前解发音锁
|
||||
voiceLock = false
|
||||
setupWord()
|
||||
} else if (result >= 0) {
|
||||
hasWrong = true
|
||||
inputBar.color = getConfig('highlightWrongColor')
|
||||
soundPlayer('wrong')
|
||||
setTimeout(() => {
|
||||
hasWrong = false
|
||||
inputBar.color = undefined
|
||||
setupWord()
|
||||
}, getConfig('highlightWrongDelay'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let startFunction = () => {
|
||||
// 在第一次启动时,设置 qwerty-learner.readOnlyMode 的正确值
|
||||
vscode.commands.executeCommand('setContext', 'qwerty-learner.readOnlyMode', readOnlyMode)
|
||||
vscode.commands.executeCommand('setContext', 'qwerty-learner.wordVisibility', wordVisibility)
|
||||
|
||||
isStart = !isStart
|
||||
if (isStart) {
|
||||
wordBar.show()
|
||||
inputBar.show()
|
||||
transBar.show()
|
||||
setupWord()
|
||||
if (readOnlyMode) {
|
||||
readOnlyIntervalId = setInterval(() => {
|
||||
order++
|
||||
setupWord()
|
||||
}, readOnlyInterval)
|
||||
}
|
||||
} else {
|
||||
wordBar.hide()
|
||||
inputBar.hide()
|
||||
transBar.hide()
|
||||
if (readOnlyMode) {
|
||||
removeInterval()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeInterval() {
|
||||
if (readOnlyIntervalId !== null) {
|
||||
clearInterval(readOnlyIntervalId)
|
||||
readOnlyIntervalId = null
|
||||
}
|
||||
}
|
||||
|
||||
let startCom = vscode.commands.registerCommand('qwerty-learner.Start', startFunction)
|
||||
|
||||
let changeChapterCom = vscode.commands.registerCommand('qwerty-learner.changeChapter', async () => {
|
||||
const inputChapter = await vscode.window.showQuickPick(
|
||||
range(1, totalChapters + 1).map((i) => i.toString()),
|
||||
{ placeHolder: `当前章节: ${chapter + 1} 共 ${totalChapters}章节` },
|
||||
)
|
||||
if (inputChapter !== undefined) {
|
||||
chapter = parseInt(inputChapter) - 1
|
||||
order = 0
|
||||
refreshWordList()
|
||||
}
|
||||
})
|
||||
|
||||
let changeDictCom = vscode.commands.registerCommand('qwerty-learner.changeDict', async () => {
|
||||
const dictList: DictPickItem[] = []
|
||||
Object.keys(dicts).forEach((key) => {
|
||||
dictList.push({ label: dicts[key].name, path: dicts[key].url, detail: dicts[key].description, key: key })
|
||||
})
|
||||
const inputDict = await vscode.window.showQuickPick(dictList, { placeHolder: `当前字典: ${dicts[dictKey].name}` })
|
||||
if (inputDict !== undefined) {
|
||||
chapter = 0
|
||||
order = 0
|
||||
changeDict(inputDict.key)
|
||||
}
|
||||
})
|
||||
|
||||
let closeReadOnlyMode = vscode.commands.registerCommand('qwerty-learner.closeReadOnlyMode', () => {
|
||||
readOnlyMode = false
|
||||
globalState.update('readOnlyMode', false)
|
||||
vscode.commands.executeCommand('setContext', 'qwerty-learner.readOnlyMode', false)
|
||||
removeInterval()
|
||||
})
|
||||
|
||||
let openReadOnlyMode = vscode.commands.registerCommand('qwerty-learner.openReadOnlyMode', () => {
|
||||
readOnlyMode = true
|
||||
globalState.update('readOnlyMode', true)
|
||||
vscode.commands.executeCommand('setContext', 'qwerty-learner.readOnlyMode', true)
|
||||
isStart = false
|
||||
startFunction()
|
||||
})
|
||||
|
||||
let toggleWordVisibility = vscode.commands.registerCommand('qwerty-learner.toggleWordVisibility', () => {
|
||||
wordVisibility = !wordVisibility
|
||||
globalState.update('wordVisibility', wordVisibility)
|
||||
vscode.commands.executeCommand('setContext', 'qwerty-learner.toggleWordVisibility', wordVisibility)
|
||||
setupWord()
|
||||
})
|
||||
|
||||
context.subscriptions.push(startCom)
|
||||
context.subscriptions.push(changeChapterCom)
|
||||
context.subscriptions.push(changeDictCom)
|
||||
context.subscriptions.push(closeReadOnlyMode)
|
||||
context.subscriptions.push(openReadOnlyMode)
|
||||
context.subscriptions.push(toggleWordVisibility)
|
||||
}
|
||||
|
||||
// this method is called when your extension is deactivated
|
||||
export function deactivate() {
|
||||
console.log('我裂开了')
|
||||
}
|
168
src/index.ts
Normal file
168
src/index.ts
Normal file
@ -0,0 +1,168 @@
|
||||
import { dictionaries } from './resource/dictionary'
|
||||
import { DictPickItem } from './typings/index'
|
||||
import * as vscode from 'vscode'
|
||||
import cet4 from './assets/CET4_T.json'
|
||||
import { range } from 'lodash'
|
||||
import { compareWord, getConfig, getDictFile } from './utils'
|
||||
import { soundPlayer } from './sound'
|
||||
import { voicePlayer } from './resource/voice'
|
||||
import PluginState from './utils/PluginState'
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
const pluginState = new PluginState(context)
|
||||
|
||||
const wordBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -100)
|
||||
const inputBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -101)
|
||||
const transBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, -102)
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
if (!pluginState.isStart) {
|
||||
return
|
||||
}
|
||||
|
||||
const { uri } = e.document
|
||||
// 避免破坏配置文件
|
||||
if (uri.scheme.indexOf('vscode') !== -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const { range, text, rangeLength } = e.contentChanges[0]
|
||||
if (!(text !== '' && text.length === 1)) {
|
||||
return
|
||||
}
|
||||
// 删除用户输入的字符
|
||||
const newRange = new vscode.Range(range.start.line, range.start.character, range.end.line, range.end.character + 1)
|
||||
const editAction = new vscode.WorkspaceEdit()
|
||||
editAction.delete(uri, newRange)
|
||||
vscode.workspace.applyEdit(editAction)
|
||||
if (pluginState.hasWrong) {
|
||||
return
|
||||
}
|
||||
soundPlayer('click')
|
||||
inputBar.text = pluginState.getCurrentInputBarContent(text)
|
||||
|
||||
const compareResult = pluginState.compareResult
|
||||
if (compareResult === -2) {
|
||||
// 用户完成单词输入
|
||||
soundPlayer('success')
|
||||
pluginState.finishWord()
|
||||
initializeBar()
|
||||
} else if (compareResult >= 0) {
|
||||
pluginState.wrongInput()
|
||||
inputBar.color = pluginState.highlightWrongColor
|
||||
soundPlayer('wrong')
|
||||
setTimeout(() => {
|
||||
pluginState.clearWrong()
|
||||
inputBar.color = undefined
|
||||
initializeBar()
|
||||
}, pluginState.highlightWrongDelay)
|
||||
}
|
||||
})
|
||||
|
||||
vscode.workspace.onDidChangeConfiguration((event) => {
|
||||
if (event.affectsConfiguration('qwerty-learner.placeholder')) {
|
||||
pluginState.placeholder = getConfig('placeholder')
|
||||
initializeBar()
|
||||
}
|
||||
|
||||
if (event.affectsConfiguration('qwerty-learner.chapterLength')) {
|
||||
pluginState.chapterLength = getConfig('chapterLength')
|
||||
initializeBar()
|
||||
}
|
||||
})
|
||||
|
||||
// 注册 vscode commands
|
||||
context.subscriptions.push(
|
||||
...[
|
||||
vscode.commands.registerCommand('qwerty-learner.start', () => {
|
||||
pluginState.isStart = !pluginState.isStart
|
||||
if (pluginState.isStart) {
|
||||
initializeBar()
|
||||
wordBar.show()
|
||||
inputBar.show()
|
||||
transBar.show()
|
||||
// todo: 只读模式相关设置
|
||||
if (pluginState.readOnlyMode) {
|
||||
setUpReadOnlyInterval()
|
||||
}
|
||||
|
||||
// todo: 随机模式
|
||||
} else {
|
||||
wordBar.hide()
|
||||
inputBar.hide()
|
||||
transBar.hide()
|
||||
removeReadOnlyInterval()
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand('qwerty-learner.changeChapter', async () => {
|
||||
const inputChapter = await vscode.window.showQuickPick(
|
||||
range(1, pluginState.totalChapters + 1).map((i) => i.toString()),
|
||||
{ placeHolder: `当前章节: ${pluginState.chapter + 1} 共 ${pluginState.totalChapters}章节` },
|
||||
)
|
||||
if (inputChapter !== undefined) {
|
||||
pluginState.chapter = parseInt(inputChapter) - 1
|
||||
initializeBar()
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand('qwerty-learner.changeDict', async () => {
|
||||
const dictList: DictPickItem[] = []
|
||||
dictionaries.forEach((dict) => {
|
||||
dictList.push({ label: dict.name, path: dict.url, detail: dict.description, key: dict.id })
|
||||
})
|
||||
const inputDict = await vscode.window.showQuickPick(dictList, { placeHolder: `当前字典: ${pluginState.dict.name}` })
|
||||
if (inputDict !== undefined) {
|
||||
pluginState.dictKey = inputDict.key
|
||||
initializeBar()
|
||||
}
|
||||
}),
|
||||
vscode.commands.registerCommand('qwerty-learner.toggleWordVisibility', () => {
|
||||
pluginState.wordVisibility = !pluginState.wordVisibility
|
||||
initializeBar()
|
||||
}),
|
||||
vscode.commands.registerCommand('qwerty-learner.toggleReadOnlyMode', () => {
|
||||
pluginState.readOnlyMode = !pluginState.readOnlyMode
|
||||
if (pluginState.readOnlyMode) {
|
||||
setUpReadOnlyInterval()
|
||||
} else {
|
||||
removeReadOnlyInterval()
|
||||
}
|
||||
}),
|
||||
],
|
||||
)
|
||||
|
||||
function initializeBar() {
|
||||
setUpWordBar()
|
||||
setUpTransBar()
|
||||
setUpInputBar()
|
||||
}
|
||||
function setUpWordBar() {
|
||||
wordBar.text = pluginState.getInitialWordBarContent()
|
||||
if (pluginState.shouldPlayVoice) {
|
||||
pluginState.voiceLock = true
|
||||
voicePlayer(pluginState.currentWord.name, () => {
|
||||
pluginState.voiceLock = false
|
||||
})
|
||||
}
|
||||
}
|
||||
function setUpTransBar() {
|
||||
transBar.text = pluginState.getInitialTransBarContent()
|
||||
}
|
||||
function setUpInputBar() {
|
||||
inputBar.text = pluginState.getInitialInputBarContent()
|
||||
}
|
||||
|
||||
function setUpReadOnlyInterval() {
|
||||
if (!pluginState.readOnlyIntervalId) {
|
||||
pluginState.readOnlyIntervalId = setInterval(() => {
|
||||
pluginState.finishWord()
|
||||
initializeBar()
|
||||
}, pluginState.readOnlyInterval)
|
||||
}
|
||||
}
|
||||
function removeReadOnlyInterval() {
|
||||
if (pluginState.readOnlyIntervalId) {
|
||||
clearInterval(pluginState.readOnlyIntervalId)
|
||||
pluginState.readOnlyIntervalId = null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,7 @@
|
||||
import * as vscode from 'vscode'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
export type Dictionary = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
url: string
|
||||
length: number
|
||||
language: 'en' | 'romaji' | 'zh' | 'ja' | 'code' | 'de'
|
||||
}
|
||||
|
||||
export type Dicts = {
|
||||
[key: string]: Dictionary
|
||||
}
|
||||
import { Dictionary } from '@/typings'
|
||||
|
||||
// 使用与网页版 qwerty 一样的格式,方便共享数据
|
||||
const dictInfos: Dictionary[] = [
|
||||
export const dictionaries: Dictionary[] = [
|
||||
{
|
||||
id: 'cet4',
|
||||
name: 'CET-4',
|
||||
@ -667,35 +652,7 @@ const dictInfos: Dictionary[] = [
|
||||
},
|
||||
]
|
||||
|
||||
export const dicts: Dicts = {}
|
||||
dictInfos.forEach((i) => (dicts[i.id] = i))
|
||||
|
||||
export function compareWord(word: string, input: string) {
|
||||
// 错误返回错误索引,正确返回-2,未完成输入且无错误返回-1
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
if (typeof input[i] !== 'undefined') {
|
||||
if (word[i] !== input[i]) {
|
||||
return i
|
||||
}
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return -2
|
||||
}
|
||||
|
||||
export function getConfig(key: string) {
|
||||
return vscode.workspace.getConfiguration('qwerty-learner')[key]
|
||||
}
|
||||
|
||||
export function getDictFile(dictPath: string) {
|
||||
const filePath = path.join(__dirname, '..', 'assets/dicts', dictPath)
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
||||
|
||||
export interface DictPickItem {
|
||||
label: string
|
||||
path: string
|
||||
key: string
|
||||
detail: string
|
||||
}
|
||||
/**
|
||||
* An object-map from dictionary IDs to dictionary themselves.
|
||||
*/
|
||||
export const idDictionaryMap: Record<string, Dictionary> = Object.fromEntries(dictionaries.map((dict) => [dict.id, dict]))
|
@ -1,4 +1,5 @@
|
||||
import { getConfig } from './utils'
|
||||
import { VoiceType } from './../../typings/index'
|
||||
import { getConfig } from '../../utils'
|
||||
interface NativeModule {
|
||||
playerPlay(voiceUrl: string, callback: () => void): void
|
||||
}
|
||||
@ -30,30 +31,9 @@ if (!(NATIVE && NATIVE.playerPlay)) {
|
||||
NATIVE = null
|
||||
}
|
||||
|
||||
type VoiceType = 'us' | 'uk' | 'close'
|
||||
|
||||
export const getVoiceType = () => {
|
||||
const voiceType: VoiceType = getConfig('voiceType')
|
||||
let type
|
||||
switch (voiceType) {
|
||||
case 'us':
|
||||
type = 2
|
||||
break
|
||||
case 'uk':
|
||||
type = 1
|
||||
break
|
||||
case 'close':
|
||||
type = ''
|
||||
break
|
||||
default:
|
||||
type = ''
|
||||
break
|
||||
}
|
||||
return type
|
||||
}
|
||||
|
||||
export const voicePlayer = (word: string, type: string | number, callback: () => void) => {
|
||||
export const voicePlayer = (word: string, callback: () => void) => {
|
||||
if (NATIVE) {
|
||||
const type = getConfig('voiceType') === 'us' ? 2 : 1
|
||||
NATIVE.playerPlay(`https://dict.youdao.com/dictvoice?audio=${word}&type=${type}`, callback)
|
||||
} else {
|
||||
callback()
|
25
src/typings/index.ts
Normal file
25
src/typings/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
export type DictPickItem = {
|
||||
label: string
|
||||
path: string
|
||||
key: string
|
||||
detail: string
|
||||
}
|
||||
|
||||
export type Word = {
|
||||
name: string
|
||||
trans: string[]
|
||||
usphone?: string
|
||||
ukphone?: string
|
||||
}
|
||||
|
||||
export type VoiceType = 'us' | 'uk' | 'close'
|
||||
|
||||
export type Dictionary = {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
url: string
|
||||
length: number
|
||||
language: 'en' | 'romaji' | 'zh' | 'ja' | 'code' | 'de'
|
||||
}
|
239
src/utils/PluginState.ts
Normal file
239
src/utils/PluginState.ts
Normal file
@ -0,0 +1,239 @@
|
||||
import { Dictionary } from '@/typings'
|
||||
import { VoiceType } from './../typings/index'
|
||||
import { idDictionaryMap } from './../resource/dictionary'
|
||||
import { compareWord, getConfig, getDictFile } from '.'
|
||||
import * as vscode from 'vscode'
|
||||
import { Word } from '@/typings'
|
||||
|
||||
export default class PluginState {
|
||||
private _globalState: vscode.Memento
|
||||
|
||||
private _dictKey: string
|
||||
private dictWords: Word[]
|
||||
public dict: Dictionary
|
||||
|
||||
public chapterLength: number
|
||||
private _readOnlyMode: boolean
|
||||
public readOnlyIntervalId: NodeJS.Timeout | null
|
||||
public placeholder: string
|
||||
|
||||
private isShowPhonetic: boolean
|
||||
public _wordVisibility: boolean
|
||||
private currentExerciseCount: number
|
||||
|
||||
private _order: number
|
||||
private _chapter: number
|
||||
public isStart: boolean
|
||||
public hasWrong: boolean
|
||||
private curInput: string
|
||||
|
||||
public voiceLock: boolean
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
const globalState = context.globalState
|
||||
this._globalState = globalState
|
||||
globalState.setKeysForSync(['chapter', 'dictKey'])
|
||||
|
||||
this._dictKey = globalState.get('dictKey', 'cet4')
|
||||
this.dict = idDictionaryMap[this._dictKey]
|
||||
this.dictWords = []
|
||||
this.loadDict()
|
||||
|
||||
this._order = globalState.get('order', 0)
|
||||
this._chapter = globalState.get('chapter', 0)
|
||||
this.isStart = false
|
||||
this.hasWrong = false
|
||||
this.curInput = ''
|
||||
this.currentExerciseCount = 0
|
||||
|
||||
this.chapterLength = getConfig('chapterLength')
|
||||
// this.readOnlyMode = globalState.get('readOnlyMode', false)
|
||||
this._readOnlyMode = false
|
||||
this.readOnlyIntervalId = null
|
||||
this.placeholder = getConfig('placeholder') // 用于控制word不可见时,inputBar中是否出现占位符及样式
|
||||
|
||||
this.isShowPhonetic = getConfig('phonetic')
|
||||
this._wordVisibility = globalState.get('wordVisibility', true)
|
||||
|
||||
this.voiceLock = false
|
||||
}
|
||||
|
||||
get chapter(): number {
|
||||
return this._chapter
|
||||
}
|
||||
set chapter(value: number) {
|
||||
this._chapter = value
|
||||
this.order = 0
|
||||
this._globalState.update('chapter', this._chapter)
|
||||
this._globalState.update('order', this.order)
|
||||
}
|
||||
|
||||
get order(): number {
|
||||
return this._order
|
||||
}
|
||||
set order(value: number) {
|
||||
this._order = value
|
||||
this._globalState.update('order', this._order)
|
||||
}
|
||||
|
||||
get dictKey(): string {
|
||||
return this._dictKey
|
||||
}
|
||||
set dictKey(value: string) {
|
||||
this.order = 0
|
||||
this.chapter = 0
|
||||
this._dictKey = value
|
||||
this.dict = idDictionaryMap[this._dictKey]
|
||||
this._globalState.update('dictKey', this._dictKey)
|
||||
this.loadDict()
|
||||
}
|
||||
|
||||
get wordExerciseTime(): number {
|
||||
return getConfig('wordExerciseTime')
|
||||
}
|
||||
get wordList(): Word[] {
|
||||
const wordList = this.dictWords.slice(this.chapter * this.chapterLength, (this.chapter + 1) * this.chapterLength)
|
||||
wordList.forEach((word) => {
|
||||
// API 字典会出现括号,但部分 vscode 插件会拦截括号的输入
|
||||
word.name = word.name.replace('(', '').replace(')', '')
|
||||
})
|
||||
|
||||
return wordList
|
||||
}
|
||||
|
||||
get wordVisibility(): boolean {
|
||||
return this._wordVisibility
|
||||
}
|
||||
set wordVisibility(value: boolean) {
|
||||
this._wordVisibility = value
|
||||
this._globalState.update('wordVisibility', this._wordVisibility)
|
||||
}
|
||||
|
||||
get totalChapters(): number {
|
||||
if (this.dictWords) {
|
||||
return Math.ceil(this.dictWords.length / this.chapterLength)
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
get currentWord(): Word {
|
||||
return this.wordList[this.order]
|
||||
}
|
||||
get compareResult(): number {
|
||||
return compareWord(this.currentWord.name, this.curInput)
|
||||
}
|
||||
get highlightWrongColor(): string {
|
||||
return getConfig('highlightWrongColor')
|
||||
}
|
||||
get highlightWrongDelay(): number {
|
||||
return getConfig('highlightWrongDelay')
|
||||
}
|
||||
get readOnlyMode(): boolean {
|
||||
return this._readOnlyMode
|
||||
}
|
||||
set readOnlyMode(value: boolean) {
|
||||
this._readOnlyMode = value
|
||||
this._globalState.update('readOnlyMode', this._readOnlyMode)
|
||||
}
|
||||
get readOnlyInterval(): number {
|
||||
return getConfig('readOnlyInterval')
|
||||
}
|
||||
get voiceType(): VoiceType {
|
||||
return getConfig('voiceType')
|
||||
}
|
||||
get shouldPlayVoice(): boolean {
|
||||
return this.voiceType !== 'close' && !this.voiceLock
|
||||
}
|
||||
|
||||
wrongInput() {
|
||||
this.hasWrong = true
|
||||
this.curInput = ''
|
||||
}
|
||||
|
||||
clearWrong() {
|
||||
this.hasWrong = false
|
||||
}
|
||||
|
||||
finishWord() {
|
||||
this.curInput = ''
|
||||
this.currentExerciseCount += 1
|
||||
if (this.currentExerciseCount >= this.wordExerciseTime) {
|
||||
this.currentExerciseCount = 0
|
||||
this.nextWord()
|
||||
}
|
||||
this.voiceLock = false
|
||||
}
|
||||
|
||||
nextWord() {
|
||||
if (this.order === this.wordList.length - 1) {
|
||||
// 结束本章节
|
||||
if (this.chapter === this.totalChapters - 1) {
|
||||
this.chapter = 0
|
||||
} else {
|
||||
this.chapter += 1
|
||||
}
|
||||
|
||||
this.order = 0
|
||||
} else {
|
||||
this.order += 1
|
||||
}
|
||||
}
|
||||
|
||||
getInitialWordBarContent() {
|
||||
return `${this.dict.name} chp.${this.chapter + 1} ${this.order + 1}/${this.wordList.length} ${
|
||||
this.wordVisibility ? this.currentWord.name : ''
|
||||
}`
|
||||
}
|
||||
|
||||
getInitialInputBarContent() {
|
||||
let content = ''
|
||||
if (this.wordVisibility || this.placeholder === '') {
|
||||
content = ''
|
||||
} else {
|
||||
// 拼接占位符
|
||||
content = this.placeholder.repeat(this.currentWord.name.length)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
getInitialTransBarContent() {
|
||||
let content = this._getCurrentWordPhonetic() + this.currentWord.trans.join('; ')
|
||||
content = content.replace(/\n/g, ' ')
|
||||
return content
|
||||
}
|
||||
|
||||
getCurrentInputBarContent(input: string) {
|
||||
let content = ''
|
||||
if (this.wordVisibility || this.placeholder === '') {
|
||||
// 没有使用placeholder,不需要特殊处理
|
||||
this.curInput += input
|
||||
content = this.curInput
|
||||
} else {
|
||||
// 拼接占位符 && 获取当前已经键入的值进行比较
|
||||
this.curInput += input
|
||||
content = this.curInput + this.placeholder.repeat(this.currentWord.name.length - this.curInput.length)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
private _getCurrentWordPhonetic() {
|
||||
let phonetic = ''
|
||||
switch (getConfig('phonetic')) {
|
||||
case 'us':
|
||||
phonetic = this.currentWord.usphone || ''
|
||||
break
|
||||
case 'uk':
|
||||
phonetic = this.currentWord.ukphone || ''
|
||||
break
|
||||
case 'close':
|
||||
phonetic = ''
|
||||
break
|
||||
}
|
||||
return phonetic
|
||||
}
|
||||
|
||||
private loadDict() {
|
||||
this.dictWords = getDictFile(this.dict.url)
|
||||
}
|
||||
}
|
28
src/utils/index.ts
Normal file
28
src/utils/index.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import * as vscode from 'vscode'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
|
||||
/**
|
||||
* 错误返回错误索引,正确返回-2,未完成输入且无错误返回-1
|
||||
*/
|
||||
export function compareWord(word: string, input: string) {
|
||||
for (let i = 0; i < word.length; i++) {
|
||||
if (typeof input[i] !== 'undefined') {
|
||||
if (word[i] !== input[i]) {
|
||||
return i
|
||||
}
|
||||
} else {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return -2
|
||||
}
|
||||
|
||||
export function getConfig(key: string) {
|
||||
return vscode.workspace.getConfiguration('qwerty-learner')[key]
|
||||
}
|
||||
|
||||
export function getDictFile(dictPath: string) {
|
||||
const filePath = path.join(__dirname, '..', 'assets/dicts', dictPath)
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'))
|
||||
}
|
@ -3,17 +3,19 @@
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"outDir": "out",
|
||||
"lib": ["es6"],
|
||||
"lib": ["esnext"],
|
||||
"types": ["node"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */,
|
||||
"resolveJsonModule": true,
|
||||
"esModuleInterop": true
|
||||
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "noUnusedParameters": true, /* Report errors on unused parameters. */
|
||||
"esModuleInterop": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"]
|
||||
},
|
||||
"typeRoots": ["./src/typings", "./node_modules/@types"]
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", ".vscode-test"]
|
||||
}
|
||||
|
@ -8,11 +8,11 @@ const path = require('path')
|
||||
const config = {
|
||||
target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/
|
||||
|
||||
entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
||||
entry: './src/index.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/
|
||||
output: {
|
||||
// the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'extension.js',
|
||||
filename: 'index.js',
|
||||
libraryTarget: 'commonjs2',
|
||||
devtoolModuleFilenameTemplate: '../[resource-path]',
|
||||
},
|
||||
|
1660
yarn-error.log
1660
yarn-error.log
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user