├── .babelrc ├── .electron-vue ├── build.js ├── dev-client.js ├── dev-runner.js ├── webpack.main.config.js ├── webpack.renderer.config.js └── webpack.translator.config.js ├── .github └── imgs │ └── how_it_looks.jpg ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── appveyor.yml ├── build └── icons │ └── icon.png ├── config ├── azureApi.js ├── baiduApi.js ├── config.json ├── newBaiduApi.js ├── qqApi.js ├── tencentApi.js └── youdaoApi.js ├── docs ├── ConfigFiles_CN.md ├── FAQ_CN.md └── README_EN.md ├── lib ├── dict │ └── jb │ │ ├── @djz020815 │ │ ├── Changes.txt │ │ ├── JcUserdic │ │ │ ├── Jcuser.CCT │ │ │ ├── Jcuser.DB1 │ │ │ ├── Jcuser.DC1 │ │ │ ├── Jcuser.DDB │ │ │ ├── Jcuser.DIC │ │ │ ├── Jcuser.ctl │ │ │ └── ╬┤├ⁿ├√.JCT │ │ └── URL.txt │ │ ├── @jichi │ │ ├── JcUserdic │ │ │ ├── Jcuser.CCT │ │ │ ├── Jcuser.DB1 │ │ │ ├── Jcuser.DC1 │ │ │ ├── Jcuser.DDB │ │ │ ├── Jcuser.DIC │ │ │ ├── Jcuser.ctl │ │ │ └── ╬┤├ⁿ├√.JCT │ │ └── VERSION.txt │ │ └── @najizhimo │ │ ├── Changes.txt │ │ ├── JcUserdic │ │ ├── Jcuser.CCT │ │ ├── Jcuser.DB1 │ │ ├── Jcuser.DC1 │ │ ├── Jcuser.DDB │ │ ├── Jcuser.DIC │ │ ├── Jcuser.ctl │ │ └── ╬┤├ⁿ├√.JCT │ │ └── URL.txt └── textractor │ ├── TextractorCLI.exe │ └── texthook.dll ├── package.json ├── src ├── common │ ├── ApplicationBuilder.ts │ ├── IpcTypes.ts │ ├── locales.json │ └── vuetify.ts ├── index.ejs ├── main │ ├── BaseGame.ts │ ├── Downloader.ts │ ├── DownloaderFactory.ts │ ├── Game.ts │ ├── GameFromProcess.ts │ ├── Hooker.ts │ ├── Processes.ts │ ├── TranslatorWindow.ts │ ├── Win32.ts │ ├── config │ │ ├── Config.ts │ │ ├── ConfigManager.ts │ │ ├── DefaultConfig.ts │ │ ├── GamesConfig.ts │ │ ├── GuiConfig.ts │ │ └── TextsConfig.ts │ ├── index.dev.ts │ ├── index.ts │ ├── middlewares │ │ ├── FilterMiddleware.ts │ │ ├── MeCabMiddleware.ts │ │ ├── PublishMiddleware.ts │ │ ├── TextInterceptorMiddleware.ts │ │ ├── TextMergerMiddleware.ts │ │ └── TextModifierMiddleware.ts │ ├── setup │ │ └── Ipc.ts │ └── translate │ │ ├── Api.ts │ │ ├── DictManager.ts │ │ ├── ExternalApi.ts │ │ ├── JBeijing.ts │ │ ├── JBeijingAdapter.ts │ │ ├── LingoesDict.ts │ │ └── TranslationManager.ts ├── renderer │ ├── App.vue │ ├── assets │ │ ├── .gitkeep │ │ └── icon.png │ ├── components │ │ ├── AboutPage.vue │ │ ├── AddGamePage.vue │ │ ├── AppSidebar.vue │ │ ├── DebugMessagesPage.vue │ │ ├── DownloadProgress.vue │ │ ├── GamesPage.vue │ │ ├── GamesPageGameCard.vue │ │ ├── LibrarySettings.vue │ │ ├── LocaleChangerSettings.vue │ │ ├── PageContent.vue │ │ ├── PageHeader.vue │ │ ├── SettingsPage.vue │ │ └── TranslatorSettings.vue │ ├── main.ts │ ├── router │ │ └── index.ts │ └── store │ │ ├── index.ts │ │ └── modules │ │ ├── Config.ts │ │ ├── Gui.ts │ │ └── index.ts ├── translator.ejs ├── translator │ ├── App.vue │ ├── assets │ │ └── .gitkeep │ ├── class-component-hooks.ts │ ├── common │ │ └── Window.ts │ ├── components │ │ ├── HookSettings.vue │ │ ├── HookSettingsHookInfo.vue │ │ ├── HooksPage.vue │ │ ├── MecabText.vue │ │ ├── SettingsPage.vue │ │ ├── TextDisplay.vue │ │ ├── Titlebar.vue │ │ └── TranslatePage.vue │ ├── main.ts │ ├── router │ │ └── index.ts │ └── store │ │ ├── index.ts │ │ └── modules │ │ ├── Config.ts │ │ ├── Hooks.ts │ │ ├── View.ts │ │ └── index.ts └── types │ ├── common.d.ts │ ├── config.d.ts │ ├── ext.d.ts │ ├── global.d.ts │ ├── muse-ui.d.ts │ ├── store.d.ts │ ├── translation.d.ts │ └── vue-component.d.ts ├── test ├── .eslintrc ├── e2e │ ├── index.js │ ├── specs │ │ └── Launch.spec.js │ └── utils.js └── unit │ ├── karma.main.conf.js │ ├── karma.renderer.conf.js │ ├── main.js │ ├── renderer.js │ ├── specs │ ├── main │ │ ├── Api.spec.ts │ │ ├── ApplicationBuilder.spec.ts │ │ ├── Config.spec.ts │ │ ├── Downloader.spec.ts │ │ ├── ExternalApi.spec.ts │ │ ├── LingoesDict.spec.ts │ │ ├── Mecab.spec.ts │ │ ├── Processes.spec.ts │ │ ├── Texts.spec.ts │ │ └── Win32.spec.ts │ └── renderer │ │ └── GamesPage.spec.ts │ └── temp │ ├── dictResult.json │ └── qqApi.js ├── tsconfig.json ├── tslint.json ├── types └── vuetify.d.ts └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | [ 7 | "env", 8 | { 9 | "targets": { "node": "current" } 10 | } 11 | ] 12 | ], 13 | "plugins": [["istanbul"], ["transform-decorators-legacy"]] 14 | }, 15 | "main": { 16 | "presets": [ 17 | [ 18 | "env", 19 | { 20 | "targets": { "node": "current" } 21 | } 22 | ] 23 | ], 24 | "plugins": ["transform-decorators-legacy"] 25 | }, 26 | "renderer": { 27 | "presets": [ 28 | [ 29 | "env", 30 | { 31 | "modules": false 32 | } 33 | ] 34 | ], 35 | "plugins": ["transform-decorators-legacy"] 36 | }, 37 | "translator": { 38 | "presets": [ 39 | [ 40 | "env", 41 | { 42 | "modules": false 43 | } 44 | ] 45 | ], 46 | "plugins": ["transform-decorators-legacy"] 47 | }, 48 | "web": { 49 | "presets": [ 50 | [ 51 | "env", 52 | { 53 | "modules": false 54 | } 55 | ] 56 | ], 57 | "plugins": ["transform-decorators-legacy"] 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.env.NODE_ENV = "production"; 4 | 5 | const chalk = require("chalk"); 6 | const del = require("del"); 7 | const webpack = require("webpack"); 8 | const Multispinner = require("multispinner"); 9 | 10 | const mainConfig = require("./webpack.main.config"); 11 | const rendererConfig = require("./webpack.renderer.config"); 12 | const translatorConfig = require("./webpack.translator.config"); 13 | 14 | const doneLog = chalk.bgGreen.white(" DONE ") + " "; 15 | const errorLog = chalk.bgRed.white(" ERROR ") + " "; 16 | const okayLog = chalk.bgBlue.white(" OKAY ") + " "; 17 | 18 | if (process.env.BUILD_TARGET === "clean") clean(); 19 | else build(); 20 | 21 | function clean() { 22 | del.sync(["build/*", "!build/icons", "!build/icons/icon.*"]); 23 | console.log(`\n${doneLog}\n`); 24 | process.exit(); 25 | } 26 | 27 | function build() { 28 | console.log("building..."); 29 | 30 | del.sync(["dist/electron/*", "!.gitkeep"]); 31 | 32 | const tasks = ["main", "renderer", "translator"]; 33 | const m = new Multispinner(tasks, { 34 | preText: "building", 35 | postText: "process" 36 | }); 37 | 38 | let results = ""; 39 | 40 | m.on("success", () => { 41 | process.stdout.write("\x1B[2J\x1B[0f"); 42 | console.log(`\n\n${results}`); 43 | console.log( 44 | `${okayLog}take it away ${chalk.yellow("`electron-builder`")}\n` 45 | ); 46 | process.exit(); 47 | }); 48 | 49 | pack(mainConfig) 50 | .then(result => { 51 | results += result + "\n\n"; 52 | m.success("main"); 53 | }) 54 | .catch(err => { 55 | m.error("main"); 56 | console.log(`\n ${errorLog}failed to build main process`); 57 | console.error(`\n${err}\n`); 58 | process.exit(1); 59 | }); 60 | 61 | pack(rendererConfig) 62 | .then(result => { 63 | results += result + "\n\n"; 64 | m.success("renderer"); 65 | }) 66 | .catch(err => { 67 | m.error("renderer"); 68 | console.log(`\n ${errorLog}failed to build renderer process`); 69 | console.error(`\n${err}\n`); 70 | process.exit(1); 71 | }); 72 | 73 | pack(translatorConfig) 74 | .then(result => { 75 | results += result + "\n\n"; 76 | m.success("translator"); 77 | }) 78 | .catch(err => { 79 | m.error("translator"); 80 | console.log(`\n ${errorLog}failed to build translator process`); 81 | console.error(`\n${err}\n`); 82 | process.exit(1); 83 | }); 84 | } 85 | 86 | function pack(config) { 87 | return new Promise((resolve, reject) => { 88 | webpack(config, (err, stats) => { 89 | if (err) reject(err.stack || err); 90 | else if (stats.hasErrors()) { 91 | let err = ""; 92 | 93 | stats 94 | .toString({ 95 | chunks: false, 96 | colors: true 97 | }) 98 | .split(/\r?\n/) 99 | .forEach(line => { 100 | err += ` ${line}\n`; 101 | }); 102 | 103 | reject(err); 104 | } else { 105 | resolve( 106 | stats.toString({ 107 | chunks: false, 108 | colors: true 109 | }) 110 | ); 111 | } 112 | }); 113 | }); 114 | } 115 | -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require("webpack-hot-middleware/client?noInfo=true&reload=true"); 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === "compiling") { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | `; 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | process.env.BABEL_ENV = "main"; 4 | 5 | const path = require("path"); 6 | const { dependencies, devDependencies } = require("../package.json"); 7 | const webpack = require("webpack"); 8 | const InjectPlugin = require("webpack-inject-plugin"); 9 | const HardSourceWebpackPlugin = require("hard-source-webpack-plugin"); 10 | 11 | let mainConfig = { 12 | optimization: { 13 | minimize: false 14 | }, 15 | mode: process.env.NODE_ENV, 16 | entry: { 17 | main: path.join(__dirname, "../src/main/index.ts") 18 | }, 19 | externals: [ 20 | ...Object.keys(dependencies || {}), 21 | ...Object.keys(devDependencies || {}) 22 | ], 23 | module: { 24 | rules: [ 25 | { 26 | test: /\.d\.ts$/, 27 | use: "ignore-loader" 28 | }, 29 | { 30 | test: /\.ts$/, 31 | use: [ 32 | "thread-loader", 33 | { 34 | loader: "babel-loader?cacheDirectory=true" 35 | }, 36 | { 37 | loader: "ts-loader", 38 | options: { happyPackMode: true, appendTsSuffixTo: [/\.vue$/] } 39 | } 40 | ], 41 | exclude: /node_modules|\.d\.ts$/ 42 | }, 43 | { 44 | test: /\.js$/, 45 | use: ["thread-loader", "babel-loader?cacheDirectory=true"], 46 | exclude: /node_modules/ 47 | }, 48 | { 49 | test: /\.node$/, 50 | use: "node-loader" 51 | } 52 | ] 53 | }, 54 | node: { 55 | __dirname: process.env.NODE_ENV !== "production", 56 | __filename: process.env.NODE_ENV !== "production" 57 | }, 58 | output: { 59 | filename: "[name].js", 60 | libraryTarget: "commonjs2", 61 | path: path.join(__dirname, "../dist/electron") 62 | }, 63 | plugins: [ 64 | new InjectPlugin.default( 65 | function() { 66 | return "process.env.DEBUG = 'yuki:*';process.env.DEBUG_COLORS = '1';"; 67 | }, 68 | { entryOrder: InjectPlugin.ENTRY_ORDER.First } 69 | ), 70 | new webpack.NoEmitOnErrorsPlugin() 71 | ], 72 | resolve: { 73 | extensions: [".ts", ".js", ".json", ".node"] 74 | }, 75 | target: "electron-main" 76 | }; 77 | 78 | /** 79 | * Adjust mainConfig for development settings 80 | */ 81 | if (process.env.NODE_ENV !== "production") { 82 | mainConfig.plugins.push( 83 | new webpack.DefinePlugin({ 84 | __static: `"${path.join(__dirname, "../static").replace(/\\/g, "\\\\")}"` 85 | }) 86 | ); 87 | } 88 | 89 | /** 90 | * Adjust mainConfig for production settings 91 | */ 92 | if (process.env.NODE_ENV === "production") { 93 | mainConfig.plugins.push( 94 | new webpack.DefinePlugin({ 95 | "process.env.NODE_ENV": '"production"' 96 | }) 97 | ); 98 | } 99 | 100 | module.exports = mainConfig; 101 | -------------------------------------------------------------------------------- /.github/imgs/how_it_looks.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/.github/imgs/how_it_looks.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/ 3 | build/* 4 | !build/icons 5 | coverage 6 | node_modules/ 7 | npm-debug.log 8 | npm-debug.log.* 9 | thumbs.db 10 | !.gitkeep 11 | .vscode/yuki.code-workspace 12 | config/games.json 13 | config/gui.json 14 | config/texts.json 15 | lib/dict/lingoes 16 | package-lock.json -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Debug Main Process", 11 | "runtimeExecutable": "node", 12 | "runtimeArgs": [".electron-vue/dev-runner.js"], 13 | "env": { 14 | "npm_execpath": "npm-cli.js" 15 | }, 16 | "sourceMaps": true, 17 | "outFiles": ["${workspaceRoot}/dist/electron/main.js"], 18 | "autoAttachChildProcesses": true, 19 | "internalConsoleOptions": "openOnFirstSessionStart", 20 | "console": "integratedTerminal", 21 | "timeout": 60000 22 | }, 23 | { 24 | "type": "node", 25 | "request": "attach", 26 | "name": "Attach", 27 | "port": 5858, 28 | "sourceMaps": true, 29 | "outFiles": ["${workspaceRoot}/dist/electron/main.js"] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.tabSize": 2, 3 | "typescript.tsdk": "node_modules\\typescript\\lib" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 

2 | YUKI 3 |
4 | YUKI Galgame 翻译器 5 |
6 |
7 |

8 | 9 |

10 | JavaScript Style Guide 11 | GPL 3.0 LICENSE 12 | Build status 13 |

14 | 15 |

16 | 中文 • 17 | English 18 |

19 | 20 | ![它看起来的样子](https://raw.githubusercontent.com/project-yuki/YUKI/master/.github/imgs/how_it_looks.jpg) 21 | 22 | ## 紧急公告 23 | 24 | 2020.5.30 晚间,大量用户使用 YUKI 时遇到错误为`ERR: TypeError: Cannot read property 'target' of undefined`的报错窗口。经排查,该报错由彩云 API 目前严重的不稳定状态导致。现已紧急推送 v0.14.3,修复上述问题,但彩云 API 目前仍无法使用,因此请目前所有版本的 YUKI 用户关闭彩云 API(具体方法:打开 YUKI 根目录下的 config\config.json 文件,将 onlineApis 数组中 name = "彩云" 的对象的 enable 属性改为 false)。关闭后,即使不升级至 v0.14.3 也可解决报错问题。 25 | 26 | ## 下载 27 | 28 | 1. 点击[这里](https://github.com/project-yuki/YUKI/releases) 29 | 2. 点开最新版本介绍下面的 " > Assets ",第一个 ZIP 文件就是编译后的 YUKI :) 30 | 31 | ## 文档 32 | 33 | - [YUKI 配置文件详解](/docs/ConfigFiles_CN.md) 34 | - [YUKI 常见问题](/docs/FAQ_CN.md) 35 | 36 | ## 挖坑的动力 37 | 38 | 我们已经有 Visual Novel Reader (VNR)了,为什么还要再开发一个 Galgame 翻译器? 39 | 40 | 嗯...有以下三个原因: 41 | 42 | 1. VNR 用 Python(甚至是 Python 2)来渲染 UI,这导致了极端的卡顿,并且完全没有必要; 43 | 2. sakuradite.com(VNR 官网)挂了,似乎现在并没有针对 VNR 的官方维护,只剩下贴吧零零散散的 BUG 修复与改进; 44 | 3. VNR 的功能(也可以说脚本)太多了,想添加/修改一个功能要读很久的源码,十分费劲。 45 | 46 | 但是,如果要用 Qt5 重写的话,手动管理内存、配置、国际化什么的又显得太麻烦了。 47 | 48 | 因此,用 Electron 作为用户交互的前端,而用原始的 Windows API 作为后端(比如文本提取),不失为一种比较好的选择。 49 | 50 | ## 功能 51 | 52 | - 从正在运行的 Galgame 里即时提取文本 53 | - 从离线字典中获取翻译,如 J 北京等 54 | - 从在线翻译 API 中获取翻译,如谷歌、百度、有道等 55 | - 可编程外部翻译 API (参考 config\ 目录下的 JavaScript 文件) 56 | - 在游戏窗口上方浮动显示原文+翻译(就像 VNR 一样) 57 | - 自定义在线 API 翻译获取方式: URL、请求方法、请求报头格式、响应的解析方式等 58 | - 支持扩展 59 | 60 | ## 计划 61 | 62 | - [计划仓库(英文)](https://github.com/project-yuki/planning/issues) 63 | - [各版本计划(英文)](https://github.com/project-yuki/YUKI/projects) 64 | 65 | ## 尝个鲜? 66 | 67 | 安装好 Node.js 和 yarn 后,随便找个文件夹,运行以下代码: 68 | 69 | git clone https://github.com/project-yuki/YUKI.git 70 | cd YUKI 71 | yarn 72 | yarn dev 73 | 74 | ## 许可证 75 | 76 | YUKI 使用 GPLv3 许可证开源。 77 | 78 | 我本来是想用 MIT 的,但是由于上游依赖(如文本提取器)用的是 GPL,所以我也很为难啊。 79 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2019 10 | platform: 11 | - x86 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 10 x86 24 | - npm install -g npm@6.11.3 25 | - git reset --hard HEAD 26 | - yarn 27 | - node --version 28 | 29 | build_script: 30 | #- yarn test 31 | - yarn build:dir 32 | 33 | after_build: 34 | - ps: Rename-Item -path build\win-ia32-unpacked -newName YUKI 35 | - 7z a -t7z YUKI.zip %APPVEYOR_BUILD_FOLDER%\build\YUKI 36 | 37 | test: off 38 | 39 | artifacts: 40 | - path: YUKI.zip 41 | name: YUKI 42 | 43 | deploy: 44 | release: v$(appveyor_build_version) 45 | provider: GitHub 46 | auth_token: 47 | secure: jrOwWRtwmimOB6Wn0mZkhkzdoXCVg7kpYi8RHYWLKYNF/5C/MNY/+E0Px+OvGc58 48 | artifact: YUKI.zip 49 | draft: true 50 | prerelease: false 51 | on: 52 | branch: master 53 | APPVEYOR_REPO_TAG: false 54 | -------------------------------------------------------------------------------- /build/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/build/icons/icon.png -------------------------------------------------------------------------------- /config/azureApi.js: -------------------------------------------------------------------------------- 1 | AZURE_TRANSLATION_URL = "https://api.cognitive.microsofttranslator.com/translate?api-version=3.0&to=zh-Hans&from=ja"; 2 | // Get a key at Azure, free 2 million words / month 3 | SUBSCRIPTION_KEY = "5ce850af5f37474c91e6911ca36ddc24"; 4 | 5 | requestTranslation = () => { 6 | return new Promise((resolve, reject) => { 7 | Request.post(AZURE_TRANSLATION_URL, { 8 | headers: { 9 | "Content-Type": "application/json", 10 | "Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY 11 | }, 12 | json: true, 13 | body: [{ Text: text }] 14 | }).then(json => { 15 | if (json.error) { 16 | callback(`Error: ${json.error.message}`); 17 | } else { 18 | var result = json[0].translations[0].text; 19 | callback(result); 20 | } 21 | }).catch(err => { 22 | callback(`error: ${err}`); 23 | }) 24 | }) 25 | } 26 | requestTranslation(); -------------------------------------------------------------------------------- /config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "localeChangers": { 3 | "alphaROMdiE": { 4 | "enable": false, 5 | "exec": "C:\\Resources\\Games\\AlphaROMdiE.exe %GAME_PATH%", 6 | "name": "AlphaROMdiE" 7 | }, 8 | "localeEmulator": { 9 | "enable": true, 10 | "exec": "C:\\LocaleEmulator\\LEProc.exe %GAME_PATH%", 11 | "name": "Locale Emulator" 12 | }, 13 | "noChanger": { 14 | "enable": false, 15 | "exec": "%GAME_PATH%", 16 | "name": "No Changer" 17 | }, 18 | "ntleas": { 19 | "enable": false, 20 | "exec": "C:\\ntleas046_x64\\x86\\ntleas.exe %GAME_PATH%", 21 | "name": "Ntleas" 22 | } 23 | }, 24 | "onlineApis": [ 25 | { 26 | "enable": true, 27 | "external": true, 28 | "jsFile": "config\\qqApi.js", 29 | "name": "腾讯" 30 | }, 31 | { 32 | "enable": false, 33 | "external": true, 34 | "jsFile": "config\\tencentApi.js", 35 | "name": "腾讯云" 36 | }, 37 | { 38 | "enable": true, 39 | "external": true, 40 | "jsFile": "config\\youdaoApi.js", 41 | "name": "有道" 42 | }, 43 | { 44 | "enable": false, 45 | "method": "POST", 46 | "name": "谷歌", 47 | "requestBodyFormat": "X{\"q\": %TEXT%, \"sl\": \"ja\", \"tl\": \"zh-CN\"}", 48 | "responseBodyPattern": "Rclass=\"t0\">([^<]*)<", 49 | "url": "https://translate.google.cn/m" 50 | }, 51 | { 52 | "enable": false, 53 | "method": "POST", 54 | "name": "彩云", 55 | "requestBodyFormat": "J{\"source\": %TEXT%, \"trans_type\": \"ja2zh\", \"request_id\": \"demo\", \"detect\": \"true\"}", 56 | "requestHeaders": "{\"X-Authorization\": \"token 3975l6lr5pcbvidl6jl2\"}", 57 | "responseBodyPattern": "J%RESPONSE%.target", 58 | "url": "https://api.interpreter.caiyunai.com/v1/translator" 59 | }, 60 | { 61 | "enable": false, 62 | "external": true, 63 | "jsFile": "config\\azureApi.js", 64 | "name": "Azure" 65 | }, 66 | { 67 | "enable": false, 68 | "external": true, 69 | "jsFile": "config\\baiduApi.js", 70 | "name": "百度" 71 | }, 72 | { 73 | "enable": false, 74 | "external": true, 75 | "jsFile": "config\\newBaiduApi.js", 76 | "name": "百度开放平台" 77 | } 78 | ], 79 | "translators": { 80 | "jBeijing": { 81 | "dictPath": "C:\\YUKI\\yuki\\lib\\dict\\jb", 82 | "enable": true, 83 | "path": "C:\\JBeijing7" 84 | } 85 | }, 86 | "dictionaries": { 87 | "lingoes": { 88 | "enable": true, 89 | "path": "C:\\YUKI\\libraries\\dict\\lingoes\\njcd.db" 90 | } 91 | }, 92 | "mecab": { 93 | "enable": true, 94 | "path": "C:\\YUKI\\libraries\\pos\\mecab-ipadic" 95 | }, 96 | "librariesRepoUrl": "https://github.com/project-yuki/libraries/raw/master/_pack/", 97 | "language": "zh" 98 | } 99 | -------------------------------------------------------------------------------- /config/qqApi.js: -------------------------------------------------------------------------------- 1 | SESSION_URL = "https://fanyi.qq.com/" 2 | TRANSLATE_URL = 3 | "https://fanyi.qq.com/api/translate" 4 | USER_AGENT = 5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36" 6 | 7 | var session 8 | var bv 9 | var requestTranslation 10 | var initSession 11 | var qtv 12 | var qtk 13 | 14 | if (!session) { 15 | session = Request.jar() 16 | 17 | requestTranslation = () => { 18 | return new Promise((resolve, reject) => { 19 | Request.post(TRANSLATE_URL, { 20 | jar: session, 21 | gzip: true, 22 | headers: { 23 | Referer: SESSION_URL, 24 | "User-Agent": USER_AGENT 25 | }, 26 | form: { 27 | source: "jp", 28 | target: "zh", 29 | sourceText: text, 30 | qtv: qtv, 31 | qtk: qtk 32 | } 33 | }) 34 | .then(body => { 35 | let sentences = JSON.parse(body).translate.records 36 | let result = ""; 37 | for (let i in sentences) { 38 | result += sentences[i].targetText 39 | } 40 | result = result.replace(/({[^}]*})|(\(\([^\)]*\)\))/g, '') 41 | if (result === '') initSession() 42 | else callback(result) 43 | }) 44 | .catch(err => { 45 | callback(qtv, qtk) 46 | }); 47 | }); 48 | }; 49 | 50 | initSession = () => { 51 | return Request.get(SESSION_URL, { 52 | jar: session, 53 | gzip: true, 54 | headers: { 55 | Referer: SESSION_URL, 56 | "User-Agent": USER_AGENT 57 | } 58 | }) 59 | .then(body => { 60 | qtv = /var qtv = "([^\"]+)";/.exec(body)[1] 61 | qtk = /var qtk = "([^\"]+)";/.exec(body)[1] 62 | }) 63 | .then(requestTranslation) 64 | }; 65 | 66 | initSession() 67 | } else { 68 | requestTranslation() 69 | } 70 | -------------------------------------------------------------------------------- /config/tencentApi.js: -------------------------------------------------------------------------------- 1 | var from = 'auto'; 2 | var to = 'zh'; 3 | var SecretId = '123465'; //https://console.cloud.tencent.com/cam/capi 腾讯云控制台获取 4 | var SecretKey = 'abcdefg'; //https://console.cloud.tencent.com/cam/capi 腾讯云控制台获取 5 | 6 | var sign = (secretKey, signStr, date) => { 7 | SecretDate = crypto.createHmac('sha256', "TC3" + secretKey).update(date).digest(); 8 | SecretService = crypto.createHmac('sha256', SecretDate).update('tmt').digest(); 9 | SecretSigning = crypto.createHmac('sha256', SecretService).update("tc3_request").digest(); 10 | ret = crypto.createHmac('sha256', SecretSigning).update(signStr).digest('hex'); 11 | return ret 12 | } 13 | 14 | var hash = (str) => { 15 | let hash = crypto.createHash('sha256'); 16 | return hash.update(str).digest('hex') 17 | } 18 | 19 | var client = { 20 | path: "/", 21 | credential: { 22 | SecretId, SecretKey 23 | }, 24 | region: "ap-shanghai", 25 | apiVersion: "2018-03-21", 26 | endpoint: "tmt.tencentcloudapi.com" 27 | } 28 | 29 | var formatRequestData = (action, params) => { 30 | params.headers = { 31 | "Host": client.endpoint, 32 | "X-TC-Action": action, 33 | "X-TC-RequestClient": "APIExplorer", 34 | "X-TC-Timestamp": Math.round(Date.now() / 1000), 35 | "X-TC-Version": client.apiVersion, 36 | "X-TC-Region": client.region, 37 | "X-TC-Language": "zh-CN", 38 | "Content-Type": "application/json", 39 | }; 40 | params.Service = "/tmt/tc3_request"; 41 | params.Date = new Date(Date.now()).toISOString().slice(0, 10); 42 | params.SecretId = client.credential.SecretId; 43 | let CredentialScope = `${params.Date}${params.Service}` 44 | let requestStr = formatSignString(params); 45 | let signStr = `TC3-HMAC-SHA256\n${params.headers["X-TC-Timestamp"]}\n${CredentialScope}\n${hash(requestStr).toLowerCase()}` 46 | let signature = sign(client.credential.SecretKey, signStr, params.Date).toLowerCase(); 47 | params.headers["Authorization"] = `TC3-HMAC-SHA256 Credential=${params.SecretId}/${CredentialScope}, SignedHeaders=content-type;host, Signature=${signature}`; 48 | return params; 49 | }; 50 | 51 | var formatSignString = (params) => { 52 | let requestStr = `POST\n/\n\ncontent-type:application/json\nhost:tmt.tencentcloudapi.com\n\ncontent-type;host\n${hash(JSON.stringify(params.body)).toLowerCase()}`; 53 | return requestStr; 54 | } 55 | 56 | var requestTranslation = () => { 57 | let params = { 58 | body: { 59 | SourceText: text, 60 | Source: from, 61 | Target: to, 62 | ProjectId: 0 63 | } 64 | } 65 | params = formatRequestData('TextTranslate', params); 66 | 67 | return new Promise( 68 | (resolve, reject) => { 69 | Request.post(`https://${client.endpoint}${client.path}`, { 70 | headers: params.headers, 71 | body: params.body, 72 | json: true, 73 | }).then(json => { 74 | if (!json.Response) { 75 | callback(`Error: UNKNOWN ERROR`); 76 | } if (json.Response.Error) { 77 | var result = json.Response.Error.Message; 78 | callback(result); 79 | } else { 80 | callback(json.Response.TargetText) 81 | } 82 | }).catch(err => { 83 | callback(`error: ${err}`); 84 | }) 85 | } 86 | ) 87 | } 88 | 89 | requestTranslation() -------------------------------------------------------------------------------- /config/youdaoApi.js: -------------------------------------------------------------------------------- 1 | SESSION_URL = "http://fanyi.youdao.com"; 2 | TRANSLATE_URL = 3 | "http://fanyi.youdao.com/translate_o?smartresult=dict&smartresult=rule"; 4 | USER_AGENT = 5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36"; 6 | 7 | var session; 8 | var bv; 9 | var requestTranslation; 10 | var initSession; 11 | 12 | if (!session) { 13 | session = Request.jar(); 14 | 15 | requestTranslation = () => { 16 | return new Promise((resolve, reject) => { 17 | let ts = "" + new Date().getTime(); 18 | let salt = ts + parseInt(10 * Math.random(), 10); 19 | let sign = md5( 20 | "fanyideskweb" + text + salt + "n%A-rKaT5fb[Gy?;N5@Tj", 21 | "hex" 22 | ); 23 | 24 | Request.post(TRANSLATE_URL, { 25 | jar: session, 26 | gzip: true, 27 | headers: { 28 | Referer: SESSION_URL, 29 | "User-Agent": USER_AGENT 30 | }, 31 | form: { 32 | from: "ja", 33 | to: "zh-CHS", 34 | i: text, 35 | salt: salt, 36 | ts: ts, 37 | smartresult: "dict", 38 | client: "fanyideskweb", 39 | doctype: "json", 40 | version: "2.1", 41 | keyfrom: "fanyi.web", 42 | action: "FY_BY_REALTIME", 43 | typoResult: false, 44 | sign: sign, 45 | bv: bv 46 | } 47 | }) 48 | .then(body => { 49 | let sentences = JSON.parse(body).translateResult[0]; 50 | let result = ""; 51 | for (let i in sentences) { 52 | result += sentences[i].tgt; 53 | } 54 | callback(result); 55 | }) 56 | .catch(err => { 57 | return initSession(); 58 | }); 59 | }); 60 | }; 61 | 62 | initSession = () => { 63 | return Request.get(SESSION_URL, { 64 | jar: session, 65 | gzip: true, 66 | headers: { 67 | Referer: SESSION_URL, 68 | "User-Agent": USER_AGENT 69 | } 70 | }) 71 | .then(() => { 72 | bv = md5("5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3915.0 Safari/537.36 Edg/79.0.287.0", "hex"); 73 | let ts = "" + new Date().getTime(); 74 | session.setCookie(Request.cookie(`___rl__test__cookies=${ts - 10}`)); 75 | }) 76 | .then(requestTranslation); 77 | }; 78 | 79 | result = initSession(); 80 | } else { 81 | result = requestTranslation(); 82 | } 83 | -------------------------------------------------------------------------------- /docs/FAQ_CN.md: -------------------------------------------------------------------------------- 1 | # YUKI 常见问题(Frequently Asked Questions) 2 | 3 | ## 综合篇 4 | 5 | ### YUKI 和 VNR 相比有何优缺点? 6 | 7 | YUKI 相比 VNR,有如下优点: 8 | 9 | 1. 基于 Electron->Node->Google V8 的框架链,使得其相比于基于 PyQt->Python 的 VNR 有了**巨幅**的性能提升 10 | 2. 相比功能强大的 VNR,其配置难度**可能**较低(如果你熟悉一点编程) 11 | 3. 基于 Textractor 的文本提取,可以免特殊码支持更多的游戏 12 | 13 | 当然,作为一个功能迭代时间不算长的项目,YUKI 相比 VNR 有如下的缺点: 14 | 15 | 1. 缺少很多基于网络数据库的功能,如共享辞书、人工字幕等。这点不急于实现的原因很大程度上在于目前在线翻译 API 逐渐 AI 化,导致替换文字有时反而会降低其翻译质量 16 | 2. 缺少 OCR 功能,尚处于摸索阶段 17 | 3. BUG 多,这点没什么好说的 18 | 4. Electron 应用的通病,磁盘和内存占用较大 19 | 20 | ### 为何这么多功能设置都需要手动修改配置文件? 21 | 22 | 因为设计 UI 比实现功能难多了(确信) 23 | 24 | ### 为何修改配置文件后需要重启 YUKI? 25 | 26 | 由于架构设计问题,目前很多配置仅在程序启动时进行加载并参与模块构建,此后该模块在程序运行期间一直驻留内存,无法进行热替换,因此必须重启以重新加载模块。 27 | 28 | 这也是上一个问题的另一部分原因。 29 | 30 | ~~然而改架构又太费时间,说到底还是一条懒狗。~~ 31 | 32 | ### 为何那么多功能计划拖了这么久还不实现? 33 | 34 | 作者目前留学中,更新频率相比以往会有很大程度的下降。 35 | 36 | 当然,作为一个开源项目,肯定是希望有更多人参与项目讨论与开发的。把功能计划放出来的目的就是期待有开发者能够一同完成/改进。 37 | 38 | ## 使用篇 39 | 40 | ### 如何开启/关闭某一翻译 API? 41 | 42 | 请参照 [config.json 配置详解](/docs/ConfigFiles_CN.md#configjson),修改各个 onlineApi 的 enable 属性为 true/false,重启 YUKI 即可。 43 | 44 | ### 某些翻译 API 经常挂掉? 45 | 46 | 目前监测下来百度和沪江容易出现此情况,可能跟其限制同一设备日均访问次数/频率有关,建议 47 | 48 | - 文本不要推得太快,给出一定的 API 调用间隔 49 | - 如果不幸被限制,可以更换其私人 API(如百度开放平台) 50 | - 禁用该 API,换用其它的 API 51 | 52 | ### 如何去除文本中出现的叠字(如变 AAABBBCCC 为 ABC)? 53 | 54 | 请参照 [text.json 配置详解](/docs/ConfigFiles_CN.md#textsjson),修改 deduplicate 属性为 true,重启 YUKI 即可。 55 | 56 | ### 怎样调整翻译窗口透明度? 57 | 58 | 翻译器窗口->翻译器设置 中有修改背景色的选项,第二个拖动条就是 Alpha 通道,其值位于 0~1,1 为完全不透明。如果想要全透明效果,记得调整该值为 0。 59 | 60 | ### 翻译窗口有一层毛玻璃效果,怎样去除? 61 | 62 | 修改 gui.json 文件中的 translatorWindow.renderMode 为 transparent,重启 YUKI 即可。 63 | 64 | ### 我要快进一段剧情,怎样不让 YUKI 翻译快进部分的文本? 65 | 66 | 使用“暂停文本获取”功能。 67 | 68 | 1. 点击翻译窗口右上角的暂停按钮 69 | 2. 快进到想要的位置 70 | 3. 点击翻译窗口右上角的播放按钮 71 | 72 | ### 翻译窗口挡住了游戏窗口,不能正常操作,怎么办? 73 | 74 | 善用“翻译窗口置顶”功能。其位于翻译窗口右上角图案为锁的按钮,点击即可在窗口置顶/窗口不置顶间切换。 75 | 76 | 一种可能的操作方案: 77 | 78 | 1. 点击按钮,切换为窗口不置顶模式 79 | 2. 点击游戏窗口。此时游戏窗口显示在翻译窗口上方 80 | 3. 操作游戏窗口 81 | 4. 在进入对话阶段后,点击翻译窗口。此时翻译窗口显示在游戏窗口上方 82 | 5. 点击按钮,切换为窗口置顶模式 83 | 6. 点击游戏窗口,推进文本,查看翻译 84 | 85 | ### 我想要...功能! 86 | 87 | 请先查阅[YUKI 配置文件详解](https://github.com/project-yuki/YUKI/blob/master/docs/ConfigFiles_CN.md),看看想要的功能是否已经存在,如果是,则按照对应要求修改配置文件以开启该功能。 88 | 89 | ## 进阶篇 90 | 91 | ### config 文件夹下的那些 .js 文件是什么? 92 | 93 | 外部 API(External API),一种可编程式翻译 API,可以供 YUKI 在翻译文本时调用。 94 | 95 | 为什么要有这样的设计? 96 | 97 | 一部分原因在于基于配置的 API(也称为内部 API)很难计算出一些翻译 API 要求的 sign 或 timestamp,而这些值对于其 API 正常工作来说是必要的,因此可以在外部 API 中编写一些函数用以生成此类参数。 98 | 99 | 另一部分原因在于这种设计可以将 API 与翻译器本身解耦,以防止像目前的 VNR 一样出现停止维护后翻译 API 需要修改源代码才能进行更新/修复的情况,以最大程度延长未来 YUKI **可能的**停止维护后的可用时间。 100 | 101 | ### 我自己编写了一个可以使用的外部 API,怎样提供给他人使用? 102 | 103 | 由于外部 API 文件独立于主程序之外,可以直接将分发出去,他人设置好 config.json 中对应的属性,即可正常使用。 104 | 105 | 当然,如果想要合并进 YUKI 以供更多人使用交流,欢迎提交 Pull Request :) 106 | -------------------------------------------------------------------------------- /docs/README_EN.md: -------------------------------------------------------------------------------- 1 | 

2 | YUKI 3 |
4 | YUKI - Yummy Utterance Knowledge Interface 5 |
6 |
7 |

8 | 9 |

10 | JavaScript Style Guide 11 | GPL 3.0 LICENSE 12 | Build status 13 |

14 | 15 |

16 | 中文 • 17 | English 18 |

19 | 20 | ![how it looks](https://raw.githubusercontent.com/project-yuki/yuki/master/.github/imgs/how_it_looks.jpg) 21 | 22 | ## Emergency Announcement 23 | 24 | In the evening of May 30th, 2020, many users faced an error reporting window with error `ERR: TypeError: Cannot read property 'target' of undefined`. Through investigation, this report is due to the highly instability of ColorfulClouds API. Now a emergency patch of version 0.14.3 has been released to repair that problem. However, at present ColorfulClouds API still cannot be used, hence users of all versions please kindly disable ColorfulClouds API (Method: open config\config.json in YUKI root directory, change the enable property of the object with name = '彩云' in the onlineApis array from true to false). After disabling, the problem will be resolved without upgrading to v0.14.3. 25 | 26 | ## Download 27 | 28 | 1. Click [Here](https://github.com/project-yuki/YUKI/releases) 29 | 2. Click " > Assets " under the description of latest version, and the first ZIP file is the compiled version of YUKI 30 | 31 | ## Motivation 32 | 33 | We already have Visual Novel Reader (VNR), so why another galgame translator? 34 | 35 | Well, there are three reasons: 36 | 37 | 1. VNR uses Python (even Python 2) for UI generation, which is extremely slow and unnecessary. 38 | 2. sakuradite.com is down, seems there is no official maintenance for VNR. 39 | 3. VNR has so many features (I mean, Python scripts), which can cause desperation when trying to modify/add one. 40 | 41 | However, if using Qt5, it can be such an annoy thing to manage memory/i18n/configuration manually. 42 | 43 | So, using Electron as frontend and traditional Windows API (Text Hooker wrapped as a native Node module) as backend seems a good idea. 44 | 45 | ## Features 46 | 47 | - Real-time text extractor from running Galgame 48 | - Get translation from dictionary: JBeijing, etc. 49 | - Get translation from online translator API: Google, Baidu, Youdao, etc. 50 | - Programmable external translator API (refer to JavaScript files in config\ folder for examples) 51 | - Show on floating window on top of game window 52 | - Custom online API settings: URL, request format, response regex parser 53 | - Support extension 54 | 55 | ## TODO 56 | 57 | - [planning repo](https://github.com/project-yuki/planning/issues) 58 | - [planning of various versions](https://github.com/project-yuki/YUKI/projects) 59 | 60 | ## Usage 61 | 62 | After installing Node.js and yarn, run the following commands in any folder: 63 | 64 | git clone https://github.com/project-yuki/YUKI.git 65 | cd YUKI 66 | yarn 67 | yarn dev 68 | 69 | ## License 70 | 71 | YUKI is licensed under GPLv3. 72 | 73 | I'd like to use MIT license, but the upstream softwares (Text Hooker for example) are licensed under GPL, so, no choice. 74 | -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.CCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.CCT -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.DB1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.DB1 -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.DC1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.DC1 -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.DDB: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.DDB -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.DIC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.DIC -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/Jcuser.ctl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/Jcuser.ctl -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/JcUserdic/╬┤├ⁿ├√.JCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@djz020815/JcUserdic/╬┤├ⁿ├√.JCT -------------------------------------------------------------------------------- /lib/dict/jb/@djz020815/URL.txt: -------------------------------------------------------------------------------- 1 | http://sakuradite.com/topic/220 2 | https://mega.co.nz/#F!lQpRATJA!PnjEHjRhaampUAAvLnrtiQ 3 | -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.CCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.CCT -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.DB1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.DB1 -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.DC1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.DC1 -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.DDB: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.DDB -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.DIC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.DIC -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/Jcuser.ctl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/Jcuser.ctl -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/JcUserdic/╬┤├ⁿ├√.JCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@jichi/JcUserdic/╬┤├ⁿ├√.JCT -------------------------------------------------------------------------------- /lib/dict/jb/@jichi/VERSION.txt: -------------------------------------------------------------------------------- 1 | 4/5/2015 jichi 2 | Changes 3 | 4 | 4/5/2015 5 | role pattern source target 6 | x ZX[A-G]Z 成語 成語 7 | n ZN[A-G]Z 普通名詞 普通名詞 8 | m ZM[A-G]Z 人名 人名 9 | 10 | // EOF 11 | -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/Changes.txt: -------------------------------------------------------------------------------- 1 | # 150924 2 | 3 | 名词: 4 | わらわ 5 | ヌキゲー 6 | ヌキゲーム 7 | ステイタス 8 | キーマン 9 | ジャマー 10 | コア 11 | スイーツ 12 | 朧気 13 | 落ちこぼれ 14 | 様々 15 | デッサン 16 | 自然 17 | 豊か(设为形容动词会出错) 18 | 腋コキ 19 | ピアス 20 | 独占欲 21 | おにぎり 22 | 缶詰め 23 | 蒲焼き 24 | 蒲焼 25 | 口々 26 | 強靱 27 | モデル 28 | チャイム 29 | 合宿 30 | 近場 31 | 32 | 33 | 无宾サ变名词: 34 | ベタベタ 35 | 36 | 37 | 单宾サ变名词: 38 | スピーク 39 | デッサン 40 | ニンマリ 41 | チヤホヤ 42 | スルー 43 | 44 | 45 | 单宾及物动词: 46 | 損ねる 47 | 引き継ぐ 48 | 引く 49 | 誘う 50 | 51 | 52 | 副词: 53 | ジーッ 54 | 自然 55 | タプタプ 56 | ギュギュッ 57 | チクリ 58 | 59 | 60 | の形态的形容动词: 61 | ジュヴナイル 62 | 自然 63 | 強靱 64 | 倫理的 65 | 66 | 67 | 非の形态的形容动词: 68 | スイーツ 69 | 70 | 71 | 成语: 72 | オーライ 73 | ファックユー 74 | どうかしたの 75 | どうかした 76 | 大器晩成 77 | 78 | # 150918 79 | 80 | 名词: 81 | シルエット 82 | ストロング(为了覆盖JB自带的人名) 83 | 隠しキャラ 84 | エンプレス 85 | リロード 86 | ビジョン 87 | お昼 88 | 木霊 89 | ピース 90 | 間 91 | コーナー 92 | 特撮 93 | 昆布 94 | 台無し 95 | 物好き 96 | 地形 97 | ネタ 98 | 元 99 | もと 100 | 同性 101 | アドバイス 102 | ライト 103 | オートマトン 104 | オートマタ 105 | ショック 106 | 佳模 107 | ガモ 108 | エール 109 | 110 | 111 | 人名 112 | 小林 113 | 114 | 115 | 单宾サ变名词: 116 | ヒラヒラ 117 | リロード 118 | 木霊 119 | デコピン 120 | 121 | 122 | 单宾及物动词: 123 | 蒸し返す 124 | 微睡める 125 | 掠める 126 | かすめる 127 | 感じ取れる 128 | 感じとれる 129 | 見守れる 130 | 131 | 132 | 单宾不及物动词: 133 | 会いたいと願う 134 | 135 | 136 | 无宾不及物动词: 137 | お互いに解りあえ 138 | 139 | 140 | 副词: 141 | くらっ 142 | ずるずる 143 | つらつら 144 | スルッ 145 | コツコツ 146 | 台無し 147 | プーっ 148 | ククッ 149 | ドン 150 | ペタン 151 | もう 152 | 絮絮叨叨 153 | のほほん 154 | ピョコン 155 | ワラワラ 156 | 157 | 158 | 形容词: 159 | 青い 160 | 緩い 161 | 歳の近い 162 | 足の遅い 163 | 164 | 165 | の形态的形容动词: 166 | ミリタリ 167 | ストロング 168 | ヒラヒラ 169 | シュープリーム 170 | 台無し 171 | シュープリーム 172 | 物好き 173 | ファンタスティック 174 | 175 | 176 | 助数词: 177 | 間 178 | クール 179 | 180 | 181 | 感叹词 182 | もう 183 | 184 | 185 | 接续词 186 | じゃない 187 | 188 | # 150913 189 | 190 | 名词: 191 | 有様 192 | 真っ青 193 | ナンパ 194 | 格好 195 | ド 196 | 了承 197 | 落書き 198 | スリー 199 | スリーインワン 200 | スリーディー 201 | ファスナー 202 | ウエストゴム 203 | コルセット 204 | 犬耳 205 | 猫耳 206 | ウサギ耳 207 | シフト 208 | シエル 209 | 210 | 211 | 单宾サ变名词: 212 | 了承 213 | シフト 214 | ファイア 215 | ドロップ 216 | ドラッグ 217 | 218 | 219 | 单宾及物动词: 220 | 取り合わせ 221 | 222 | 223 | 单宾不及物动词: 224 | 生き別れる 225 | 226 | 227 | 副词: 228 | 途端 229 | 途端に 230 | きゅん 231 | にゅいっ 232 | はぁはぁ 233 | 234 | 235 | 形容词: 236 | 凛々しい 237 | 238 | 239 | の形态的形容动词: 240 | 真っ青 241 | 格好 242 | 243 | 244 | 非の形态的形容动词: 245 | リッチ 246 | 247 | # 150908 248 | 249 | 名词: 250 | 骸 251 | 最上位 252 | 見事 253 | 手持無沙汰 254 | ピュア 255 | トンファー 256 | インスタレーション 257 | この世界 258 | 259 | 无宾サ变名词: 260 | リングイン 261 | 262 | 单宾サ变名词: 263 | トランスレート 264 | 露出 265 | クンクン 266 | クンカクンカ 267 | くんくん 268 | くんかくんか 269 | フォロー 270 | パワーアップ 271 | 分担 272 | マスター 273 | 一夜漬け 274 | 275 | 单宾及物动词: 276 | 引き留める 277 | 遣り合う 278 | 殉じる 279 | 入れ違う 280 | 取り潰す 281 | 手渡す 282 | 引く 283 | 引き結ぶ 284 | 洩らす 285 | 宛てがう 286 | 充てがう 287 | 御座す 288 | まぐわう 289 | 流離う 290 | 誘い合わす 291 | 292 | 副词: 293 | ニヤァっ 294 | ニヤっ 295 | コトリ 296 | ファササ 297 | ファサファサ 298 | パパパ 299 | パパッ 300 | パパパッ 301 | ほうっ 302 | ドクンッ 303 | スチャッ 304 | ホワン 305 | カツンカツン 306 | カツンッ 307 | カツンッカツンッ 308 | プチュ 309 | パッカリ 310 | ニチャアッ 311 | プシッ 312 | ブルン 313 | ズン 314 | ニチュ 315 | ニュルンッ 316 | カッカ 317 | カッカッ 318 | キュポッ 319 | バサバサ 320 | バサッ 321 | バサバサッ 322 | ガツン 323 | ズベーッ 324 | カァっ 325 | フシュー 326 | チロッ 327 | ベーッ 328 | キーキー 329 | ずしずし 330 | ヌボーッ 331 | ニチュリ 332 | ジュルジュル 333 | レロレロ 334 | バンッ 335 | クピクピ 336 | ギロッ 337 | ニヘラニヘラ 338 | 見事 339 | ゲホゲホ 340 | ふらっ 341 | パコッ 342 | ばくばく 343 | コクッ 344 | グスグズ 345 | ボキッ 346 | カクカク 347 | キツキツ 348 | 349 | 形容词: 350 | 微笑ましい 351 | 辛い 352 | 出来の悪い 353 | 354 | の形态的形容动词: 355 | すっぽんぽん 356 | 仲間想い 357 | 見事 358 | 病気持ち 359 | 年相応 360 | 一番 361 | 362 | 非の形态的形容动词: 363 | 欧たる 364 | ピュア 365 | 366 | 成语: 367 | 杞憂 368 | 369 | # EOF 370 | -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.CCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.CCT -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DB1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DB1 -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DC1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DC1 -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DDB: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DDB -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DIC: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.DIC -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/Jcuser.ctl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/Jcuser.ctl -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/JcUserdic/╬┤├ⁿ├√.JCT: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/dict/jb/@najizhimo/JcUserdic/╬┤├ⁿ├√.JCT -------------------------------------------------------------------------------- /lib/dict/jb/@najizhimo/URL.txt: -------------------------------------------------------------------------------- 1 | http://pan.baidu.com/s/1pJJzRar 2 | 597332265@qq.com -------------------------------------------------------------------------------- /lib/textractor/TextractorCLI.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/textractor/TextractorCLI.exe -------------------------------------------------------------------------------- /lib/textractor/texthook.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/lib/textractor/texthook.dll -------------------------------------------------------------------------------- /src/common/ApplicationBuilder.ts: -------------------------------------------------------------------------------- 1 | export default class ApplicationBuilder { 2 | private middlewares: Array> = [] 3 | 4 | public use (middleware: yuki.Middleware) { 5 | this.middlewares.push(middleware) 6 | } 7 | 8 | public run (initContext: T) { 9 | this.iterator(initContext, 0) 10 | } 11 | 12 | private iterator (context: T, index: number) { 13 | if (index === this.middlewares.length) return 14 | 15 | this.middlewares[index].process(context, (newContext) => { 16 | this.iterator(newContext, index + 1) 17 | }) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/common/IpcTypes.ts: -------------------------------------------------------------------------------- 1 | enum IpcTypes { 2 | MAIN_PAGE_LOAD_FINISHED = 'main-page-load-finished', 3 | REQUEST_INSERT_HOOK = 'request-insert-hook', 4 | HAS_INSERTED_HOOK = 'has-inserted-hook', 5 | REQUEST_REMOVE_HOOK = 'request-remove-hook', 6 | HAS_REMOVED_HOOK = 'has-removed-hook', 7 | HAS_HOOK_TEXT = 'has-hook-text', 8 | REQUEST_CONFIG = 'request-config', 9 | HAS_CONFIG = 'has-config', 10 | REQUEST_SAVE_CONFIG = 'request-save-config', 11 | HAS_SAVED_CONFIG = 'has-saved-config', 12 | REQUEST_NEW_GAME_PATH = 'request-new-game-path', 13 | HAS_NEW_GAME_PATH = 'has-new-game-path', 14 | REQUEST_ADD_GAME = 'request-add-game', 15 | HAS_ADDED_GAME = 'has-added-game', 16 | REQUEST_REMOVE_GAME = 'request-remove-game', 17 | HAS_REMOVED_GAME = 'has-removed-game', 18 | REQUEST_RUN_GAME = 'request-run-game', 19 | HAS_RUNNING_GAME = 'has-running-game', 20 | REQUEST_TRANSLATION = 'request-translation', 21 | HAS_TRANSLATION = 'has-translation', 22 | APP_EXIT = 'app-exit', 23 | REQUEST_SAVE_TRANSLATOR_GUI = 'request-save-translator-gui', 24 | REQUEST_PATH_WITH_FILE = 'request-path-with-file', 25 | HAS_PATH_WITH_FILE = 'has-path-with-file', 26 | HAS_DOWNLOAD_PROGRESS = 'has-download-progress', 27 | HAS_DOWNLOAD_COMPLETE = 'has-download-complete', 28 | REQUEST_PAUSE_DOWNLOAD = 'request-pause-download', 29 | REQUEST_RESUME_DOWNLOAD = 'request-resume-download', 30 | REQUEST_ABORT_DOWNLOAD = 'request-abort-download', 31 | REQUEST_DOWNLOAD_LIBRARY = 'request-download-library', 32 | HAS_NEW_DEBUG_MESSAGE = 'has-new-debug-message', 33 | GAME_ABORTED = 'game-aborted', 34 | REQUEST_DICT = 'request-dict', 35 | HAS_DICT = 'has-dict', 36 | REQUEST_PROCESSES = 'request-processes', 37 | HAS_PROCESSES = 'has-processes', 38 | RELOAD_CONFIG = 'reload-config' 39 | } 40 | 41 | export default IpcTypes 42 | -------------------------------------------------------------------------------- /src/common/locales.json: -------------------------------------------------------------------------------- 1 | { 2 | "zh": { 3 | "YUKIGalgameTranslator": "Galgame翻译器", 4 | "myGames": "我的游戏", 5 | "addGame": "添加游戏", 6 | "applicationSettings": "设置", 7 | "localeChangers": "区域转换器", 8 | "localeChanger": "区域转换器", 9 | "applicationLibraries": "程序库", 10 | "translators": "翻译器", 11 | "aboutYUKI": "关于YUKI", 12 | "ok": "确定", 13 | "cancel": "取消", 14 | "save": "保存", 15 | "reset": "重置", 16 | "name": "名称", 17 | "delete": "删除", 18 | "path": "路径", 19 | "enable": "启用", 20 | "download": "下载", 21 | "translatorSettings": "翻译器设置", 22 | "inputSpecialCode": "输入特殊码", 23 | "specialCode": "特殊码", 24 | "add": "添加", 25 | "prompt": "提示", 26 | "saved": "已保存", 27 | "toggleDevTools": "切换开发人员工具", 28 | "debugMessages": "调试信息", 29 | "debugMsg": "调试信息", 30 | "description": "说明", 31 | "window": "窗口" 32 | }, 33 | "en": { 34 | "YUKIGalgameTranslator": "Galgame Translator", 35 | "myGames": "My Games", 36 | "addGame": "Add Game", 37 | "applicationSettings": "Settings", 38 | "localeChangers": "Locl Chngrs", 39 | "localeChanger": "Locale Changer", 40 | "applicationLibraries": "App Libraries", 41 | "translators": "Translators", 42 | "aboutYUKI": "About YUKI", 43 | "ok": "OK", 44 | "cancel": "Cancel", 45 | "save": "Save", 46 | "reset": "Reset", 47 | "name": "Name", 48 | "delete": "Delete", 49 | "path": "Path", 50 | "enable": "Enable", 51 | "download": "Download", 52 | "translatorSettings": "Translator Settings", 53 | "inputSpecialCode": "Input Special Code", 54 | "specialCode": "Special Code", 55 | "add": "Add", 56 | "prompt": "Prompt", 57 | "saved": "Saved", 58 | "toggleDevTools": "Toggle Dev Tools", 59 | "debugMessages": "Debug Messages", 60 | "debugMsg": "Debug Msg", 61 | "description": "Description", 62 | "window": "Window" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/common/vuetify.ts: -------------------------------------------------------------------------------- 1 | import '@mdi/font/css/materialdesignicons.css' 2 | import 'vuetify-dialog/dist/vuetify-dialog.min.css' 3 | 4 | import Vue from 'vue' 5 | import VuetifyDialog from 'vuetify-dialog' 6 | import Vuetify from 'vuetify/lib' 7 | 8 | Vue.use(Vuetify) 9 | 10 | const vuetify = new Vuetify({ 11 | icons: { 12 | iconfont: 'mdi' 13 | } 14 | }) 15 | 16 | Vue.use(VuetifyDialog, { 17 | context: { 18 | vuetify 19 | } 20 | }) 21 | 22 | export default vuetify 23 | -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YUKI Launcher 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 13 | <% } %> 14 | 15 | 16 | 17 |
18 | 19 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/main/BaseGame.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:game') 2 | import { EventEmitter } from 'events' 3 | import Hooker from './Hooker' 4 | import { registerProcessExitCallback } from './Win32' 5 | 6 | export default abstract class BaseGame extends EventEmitter { 7 | protected pids: number[] 8 | 9 | constructor () { 10 | super() 11 | this.pids = [] 12 | } 13 | 14 | public abstract start (): void 15 | 16 | public getPids () { 17 | return this.pids 18 | } 19 | 20 | public abstract getInfo (): yuki.Game 21 | 22 | protected afterGetPids () { 23 | this.injectProcessByPid() 24 | this.registerProcessExitCallback() 25 | this.emit('started', this) 26 | } 27 | 28 | private injectProcessByPid () { 29 | this.pids.map((pid) => Hooker.getInstance().injectProcess(pid)) 30 | } 31 | 32 | private registerProcessExitCallback () { 33 | registerProcessExitCallback(this.pids, () => { 34 | this.emit('exited', this) 35 | }) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/Downloader.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as request from 'request' 3 | const progress = require('request-progress') 4 | 5 | export default class Downloader { 6 | private endCallback: () => void 7 | private errorCallback: (err: Error) => void 8 | private downloadRequest: request.Request | undefined 9 | private progressCallback: (state: RequestProgress.ProgressState) => void | undefined 10 | 11 | constructor (private fileUrl: string, private saveToPath: string) { 12 | this.progressCallback = () => { return } 13 | this.errorCallback = () => { return } 14 | this.endCallback = () => { return } 15 | } 16 | 17 | public start () { 18 | this.downloadRequest = request.get(this.fileUrl) 19 | progress(this.downloadRequest) 20 | .on('progress', (state: RequestProgress.ProgressState) => { 21 | this.progressCallback(state) 22 | }) 23 | .on('error', (err: Error) => { 24 | this.errorCallback(err) 25 | }) 26 | .on('end', () => { 27 | this.endCallback() 28 | }) 29 | .pipe(fs.createWriteStream(this.saveToPath)) 30 | return this 31 | } 32 | 33 | public pause () { 34 | if (!this.downloadRequest) return 35 | this.downloadRequest.pause() 36 | } 37 | public resume () { 38 | if (!this.downloadRequest) return 39 | this.downloadRequest.resume() 40 | } 41 | public abort () { 42 | if (!this.downloadRequest) return 43 | this.downloadRequest.abort() 44 | 45 | if (fs.existsSync(this.saveToPath)) { 46 | fs.unlinkSync(this.saveToPath) 47 | } 48 | 49 | this.errorCallback(new Error('download aborted')) 50 | } 51 | 52 | public onProgress (callback: (state: RequestProgress.ProgressState) => void) { 53 | this.progressCallback = callback 54 | return this 55 | } 56 | public onEnd (callback: () => void) { 57 | this.endCallback = callback 58 | return this 59 | } 60 | public onError (callback: (err: Error) => void) { 61 | this.errorCallback = callback 62 | return this 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/DownloaderFactory.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import { unlinkSync } from 'fs' 3 | import * as path from 'path' 4 | import IpcTypes from '../common/IpcTypes' 5 | import ConfigManager from './config/ConfigManager' 6 | import Downloader from './Downloader' 7 | const extract = require('extract-zip') 8 | const debug = require('debug') 9 | 10 | export default class DownloaderFactory { 11 | public static LIBRARY_BASE_REPO = '' 12 | public static LIBRARY_BASE_STORE_PATH = '' 13 | 14 | public static init () { 15 | this.LIBRARY_BASE_REPO = ConfigManager.getInstance() 16 | .get('default').librariesRepoUrl 17 | this.LIBRARY_BASE_STORE_PATH = path.resolve(global.__baseDir, 'lib') 18 | debug('yuki:downloader:factory')('library base repo -> %s', this.LIBRARY_BASE_REPO) 19 | debug('yuki:downloader:factory')('library base store path -> %s', this.LIBRARY_BASE_STORE_PATH) 20 | } 21 | 22 | public static makeLibraryDownloader (packName: string): Downloader { 23 | return new Downloader( 24 | `${this.LIBRARY_BASE_REPO}${packName}.zip`, 25 | `${this.LIBRARY_BASE_STORE_PATH}\\${packName}.zip` 26 | ).onProgress((state) => { 27 | debug('yuki:downloader:library')('[%s] downloading -> %O', packName, state) 28 | ipcMain.emit(IpcTypes.HAS_DOWNLOAD_PROGRESS, packName, state) 29 | }).onError((err) => { 30 | debug('yuki:downloader:library')('[%s] download error !> %s', packName, err) 31 | ipcMain.emit(IpcTypes.HAS_DOWNLOAD_COMPLETE, packName, err.toString()) 32 | }).onEnd(() => { 33 | debug('yuki:downloader:library')('[%s] download complete', packName) 34 | extract( 35 | `${this.LIBRARY_BASE_STORE_PATH}\\${packName}.zip`, 36 | { dir: this.LIBRARY_BASE_STORE_PATH }, 37 | (err: Error) => { 38 | if (err) { 39 | debug('yuki:downloader:library')('[%s] unzip error !> %s', packName, err) 40 | return 41 | } 42 | 43 | debug('yuki:downloader:library')('[%s] unzip complete', packName) 44 | unlinkSync(`${this.LIBRARY_BASE_STORE_PATH}\\${packName}.zip`) 45 | ipcMain.emit(IpcTypes.HAS_DOWNLOAD_COMPLETE, packName) 46 | } 47 | ) 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/Game.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | const debug = require('debug')('yuki:game') 3 | import BaseGame from './BaseGame' 4 | import ConfigManager from './config/ConfigManager' 5 | 6 | export default class Game extends BaseGame { 7 | private static readonly TIMEOUT = 1000 8 | private static readonly MAX_RESET_TIME = 10 9 | private execString: string 10 | private path: string 11 | private code: string 12 | private name: string 13 | private localeChanger: string 14 | private exeName: string 15 | 16 | constructor (game: yuki.Game) { 17 | super() 18 | this.path = game.path 19 | this.execString = '' 20 | this.pids = [] 21 | this.code = game.code 22 | this.name = game.name 23 | this.localeChanger = game.localeChanger 24 | this.exeName = '' 25 | } 26 | 27 | public start () { 28 | this.execGameProcess() 29 | this.registerHookerWithPid() 30 | } 31 | 32 | public getInfo (): yuki.Game { 33 | return { 34 | name: this.name, 35 | code: this.code, 36 | path: this.path, 37 | localeChanger: this.localeChanger 38 | } 39 | } 40 | 41 | private execGameProcess () { 42 | this.getRawExecStringOrDefault() 43 | this.replaceExecStringTokensWithActualValues() 44 | debug('exec string: %s', this.execString) 45 | exec(this.execString) 46 | } 47 | 48 | private getRawExecStringOrDefault () { 49 | const localeChangers = ConfigManager.getInstance().get( 50 | 'default' 51 | ).localeChangers 52 | if (this.localeChanger) { 53 | debug( 54 | 'choose %s as locale changer', 55 | localeChangers[this.localeChanger].name 56 | ) 57 | this.execString = localeChangers[this.localeChanger].exec 58 | return 59 | } 60 | debug('no locale changer chosed. use %GAME_PATH%') 61 | this.execString = '%GAME_PATH%' 62 | } 63 | 64 | private replaceExecStringTokensWithActualValues () { 65 | this.execString = this.execString.replace('%GAME_PATH%', `"${this.path}"`) 66 | } 67 | 68 | private async registerHookerWithPid () { 69 | this.exeName = this.path.substring(this.path.lastIndexOf('\\') + 1) 70 | debug('finding pid of %s...', this.exeName) 71 | try { 72 | await this.findPids() 73 | } catch (e) { 74 | debug('could not find game %s. abort', this.exeName) 75 | this.emit('abort') 76 | this.emit('exited') 77 | return 78 | } 79 | this.afterGetPids() 80 | } 81 | 82 | private findPids () { 83 | return new Promise((resolve, reject) => { 84 | let retryTimes = 0 85 | const pidGetterInterval = setInterval(() => { 86 | exec( 87 | `tasklist /nh /fo csv /fi "imagename eq ${this.exeName}"`, 88 | (err, stdout, stderr) => { 89 | if (err) throw err 90 | 91 | if (retryTimes >= Game.MAX_RESET_TIME) { 92 | clearInterval(pidGetterInterval) 93 | reject() 94 | } 95 | if (this.findsPidsIn(stdout)) { 96 | clearInterval(pidGetterInterval) 97 | this.pids = this.parsePidsFrom(stdout) 98 | debug('found game. pids %o', this.pids) 99 | resolve() 100 | } else { 101 | retryTimes++ 102 | debug('could not find game. retry times...', retryTimes) 103 | } 104 | } 105 | ) 106 | }, Game.TIMEOUT) 107 | }) 108 | } 109 | 110 | private findsPidsIn (value: string) { 111 | return value.startsWith('"') 112 | } 113 | 114 | private parsePidsFrom (value: string) { 115 | const pids: number[] = [] 116 | 117 | const regexResult = value.match(/"[^"]+"/g) 118 | if (!regexResult) return [] 119 | 120 | for (let i = 0; i < regexResult.length; i++) { 121 | if (i % 5 !== 1) continue 122 | 123 | pids.push(parseInt(regexResult[i].replace('"', ''), 10)) 124 | } 125 | return pids 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/GameFromProcess.ts: -------------------------------------------------------------------------------- 1 | import BaseGame from './BaseGame' 2 | 3 | export default class GameFromProcess extends BaseGame { 4 | private process: yuki.Process 5 | 6 | constructor (process: yuki.Process) { 7 | super() 8 | this.process = process 9 | this.pids = [process.pid] 10 | } 11 | 12 | public getInfo (): yuki.Game { 13 | return { 14 | name: this.process.name.replace('.exe', ''), 15 | code: '', 16 | path: '', 17 | localeChanger: '' 18 | } 19 | } 20 | 21 | public start () { 22 | this.afterGetPids() 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/Hooker.ts: -------------------------------------------------------------------------------- 1 | import IpcTypes from '../common/IpcTypes' 2 | const debug = require('debug')('yuki:hooker') 3 | import * as path from 'path' 4 | import { Textractor } from 'textractor-wrapper' 5 | import ApplicationBuilder from '../common/ApplicationBuilder' 6 | import ConfigManager from './config/ConfigManager' 7 | import FilterMiddleware from './middlewares/FilterMiddleware' 8 | import MecabMiddleware from './middlewares/MeCabMiddleware' 9 | import PublishMiddleware from './middlewares/PublishMiddleware' 10 | import TextInterceptorMiddleware from './middlewares/TextInterceptorMiddleware' 11 | import TextMergerMiddleware from './middlewares/TextMergerMiddleware' 12 | import TextModifierMiddleware from './middlewares/TextModifierMiddleware' 13 | 14 | let applicationBuilder: ApplicationBuilder 15 | 16 | interface IPublisherMap { 17 | ['thread-output']: PublishMiddleware 18 | } 19 | 20 | export default class Hooker { 21 | public static getInstance () { 22 | if (!this.instance) { 23 | this.instance = new Hooker() 24 | } 25 | return this.instance 26 | } 27 | private static instance: Hooker | undefined 28 | 29 | private hooker: Textractor 30 | 31 | private publisherMap: IPublisherMap = { 32 | 'thread-output': new PublishMiddleware(IpcTypes.HAS_HOOK_TEXT) 33 | } 34 | 35 | private constructor () { 36 | const absolutePath = path.join( 37 | global.__baseDir, 38 | 'lib/textractor/TextractorCLI.exe' 39 | ) 40 | debug('trying to access CLI exe at %s', absolutePath) 41 | this.hooker = new Textractor(absolutePath) 42 | this.buildApplication() 43 | this.initHookerCallbacks() 44 | this.hooker.start() 45 | } 46 | 47 | public rebuild () { 48 | delete require.cache[require.resolve('mecab-ffi')] 49 | this.buildApplication() 50 | } 51 | 52 | public subscribe (on: keyof IPublisherMap, webContents: Electron.WebContents) { 53 | if (!this.publisherMap[on]) { 54 | debug('trying to register unknown event %s', on) 55 | } else { 56 | this.publisherMap[on].subscribe(webContents) 57 | } 58 | } 59 | 60 | public unsubscribe (on: string, webContents: Electron.WebContents) { 61 | if (!this.publisherMap[on]) { 62 | debug('trying to unregister unknown event %s', on) 63 | } else { 64 | this.publisherMap[on].unsubscribe(webContents) 65 | } 66 | } 67 | 68 | public injectProcess (pid: number) { 69 | debug('injecting process %d...', pid) 70 | this.hooker.attach(pid) 71 | debug('process %d injected', pid) 72 | } 73 | 74 | public insertHook (pid: number, code: string) { 75 | debug('inserting hook %s to process %d...', code, pid) 76 | this.hooker.hook(pid, code) 77 | debug(`hook %s inserted into process %d`, code, pid) 78 | } 79 | 80 | private buildApplication () { 81 | applicationBuilder = new ApplicationBuilder() 82 | // applicationBuilder.use(new TextMergerMiddleware()) 83 | applicationBuilder.use( 84 | new TextMergerMiddleware( 85 | ConfigManager.getInstance().get('texts').merger 86 | ) 87 | ) 88 | applicationBuilder.use( 89 | new TextInterceptorMiddleware( 90 | ConfigManager.getInstance().get('texts').interceptor 91 | ) 92 | ) 93 | applicationBuilder.use( 94 | new TextModifierMiddleware( 95 | ConfigManager.getInstance().get('texts').modifier 96 | ) 97 | ) 98 | applicationBuilder.use( 99 | new MecabMiddleware( 100 | ConfigManager.getInstance().get('default').mecab 101 | ) 102 | ) 103 | applicationBuilder.use(new FilterMiddleware()) 104 | applicationBuilder.use(this.publisherMap['thread-output']) 105 | 106 | debug('application builded') 107 | } 108 | 109 | private initHookerCallbacks () { 110 | this.hooker.on('output', (output) => { 111 | applicationBuilder.run(output) 112 | }) 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/Processes.ts: -------------------------------------------------------------------------------- 1 | import { exec } from 'child_process' 2 | const debug = require('debug')('yuki:processes') 3 | 4 | export default class Processes { 5 | public static async get () { 6 | return new Promise((resolve, reject) => { 7 | exec(`${Processes.CHCP_COMMAND} & ${Processes.TASK_LIST_COMMAND}`, 8 | (err, stdout, stderr) => { 9 | if (err) { 10 | debug('exec failed !> %s', err) 11 | reject() 12 | return 13 | } 14 | 15 | if (this.findsProcessIn(stdout)) { 16 | const result = this.parseProcessesFrom(stdout) 17 | debug('get %d processes', result.length) 18 | resolve(result) 19 | } else { 20 | debug('exec failed. no process') 21 | reject() 22 | } 23 | } 24 | ) 25 | }) 26 | } 27 | private static CHCP_COMMAND = 'chcp 65001' 28 | private static TASK_LIST_COMMAND = 'tasklist /nh /fo csv /fi "sessionname eq Console"' 29 | 30 | private static findsProcessIn (value: string) { 31 | return value.indexOf('"') !== -1 32 | } 33 | 34 | private static parseProcessesFrom (value: string) { 35 | const processes: yuki.Processes = [] 36 | 37 | const regexResult = value.match(/"([^"]+)"/g) 38 | if (!regexResult) return [] 39 | 40 | let onePair: yuki.Process = { name: '', pid: -1 } 41 | for (let i = 0; i < regexResult.length; i++) { 42 | if (i % 5 === 0) {// process name 43 | onePair.name = regexResult[i].substr(1, regexResult[i].length - 2) 44 | } else if (i % 5 === 1) {// process id 45 | onePair.pid = parseInt(regexResult[i].substr(1, regexResult[i].length - 2), 10) 46 | processes.push(onePair) 47 | onePair = { name: '', pid: -1 } 48 | } 49 | } 50 | 51 | return processes 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/TranslatorWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import BaseGame from './BaseGame' 3 | import ConfigManager from './config/ConfigManager' 4 | import Game from './Game' 5 | import Hooker from './Hooker' 6 | const debug = require('debug')('yuki:translatorWindow') 7 | const ElectronVibrancy = require('electron-vibrancy') 8 | 9 | export default class TranslatorWindow { 10 | private readonly URL = 11 | process.env.NODE_ENV === 'development' 12 | ? `http://localhost:9081/translator.html` 13 | : `file://${__dirname}/translator.html` 14 | 15 | private window!: Electron.BrowserWindow 16 | private game!: BaseGame 17 | 18 | private isRealClose = false 19 | private config!: yuki.Config.Gui['translatorWindow'] 20 | 21 | constructor () { 22 | this.create() 23 | } 24 | 25 | public getWindow () { 26 | return this.window 27 | } 28 | 29 | public close () { 30 | this.isRealClose = true 31 | this.unsubscribeHookerEvents() 32 | this.window.close() 33 | } 34 | 35 | public setGame (game: BaseGame) { 36 | this.game = game 37 | } 38 | 39 | public getGameInfo (): yuki.Game { 40 | return this.game.getInfo() 41 | } 42 | 43 | private create () { 44 | this.config = ConfigManager.getInstance().get('gui') 45 | .translatorWindow 46 | this.window = new BrowserWindow({ 47 | webPreferences: { 48 | defaultFontFamily: { 49 | standard: 'Microsoft Yahei UI', 50 | serif: 'Microsoft Yahei UI', 51 | sansSerif: 'Microsoft Yahei UI' 52 | } 53 | }, 54 | show: false, 55 | alwaysOnTop: this.config.alwaysOnTop, 56 | transparent: true, 57 | frame: false 58 | }) 59 | 60 | debug( 61 | 'alwaysOnTop -> %s', 62 | ConfigManager.getInstance().get('gui').translatorWindow 63 | .alwaysOnTop 64 | ) 65 | 66 | this.window.on('ready-to-show', () => { 67 | // choose translucent as default, unless assigning transparent explicitly 68 | if (this.config.renderMode !== 'transparent') { 69 | ElectronVibrancy.SetVibrancy(this.window, 0) 70 | } 71 | 72 | debug('subscribing hooker events...') 73 | this.subscribeHookerEvents() 74 | debug('hooker events subscribed') 75 | this.window.show() 76 | }) 77 | 78 | this.window.on('close', (event) => { 79 | if (!this.isRealClose) { 80 | event.preventDefault() 81 | this.window.hide() 82 | } 83 | 84 | debug('saving translator window bounds -> %o', this.window.getBounds()) 85 | debug( 86 | 'saving translator window alwaysOnTop -> %s', 87 | this.window.isAlwaysOnTop() 88 | ) 89 | ConfigManager.getInstance().set('gui', { 90 | ...ConfigManager.getInstance().get('gui'), 91 | translatorWindow: { 92 | ...ConfigManager.getInstance().get('gui') 93 | .translatorWindow, 94 | bounds: this.window.getBounds(), 95 | alwaysOnTop: this.window.isAlwaysOnTop() 96 | } 97 | }) 98 | }) 99 | 100 | this.window.setBounds( 101 | ConfigManager.getInstance().get('gui').translatorWindow 102 | .bounds 103 | ) 104 | 105 | this.window.loadURL(this.URL) 106 | } 107 | 108 | private subscribeHookerEvents () { 109 | Hooker.getInstance().subscribe('thread-output', this.window.webContents) 110 | } 111 | 112 | private unsubscribeHookerEvents () { 113 | Hooker.getInstance().unsubscribe('thread-output', this.window.webContents) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/main/Win32.ts: -------------------------------------------------------------------------------- 1 | import * as ffi from 'ffi' 2 | const debug = require('debug')('yuki:win32') 3 | 4 | const SYNCHRONIZE = 0x00100000 5 | const FALSE = 0 6 | const INFINITE = 0xffffffff 7 | 8 | const knl32 = ffi.Library('kernel32.dll', { 9 | OpenProcess: ['uint32', ['uint32', 'int', 'uint32']], 10 | WaitForSingleObject: ['uint32', ['uint32', 'uint32']] 11 | }) 12 | 13 | export function registerProcessExitCallback ( 14 | pids: number[], 15 | callback: () => void 16 | ): void { 17 | doRegister(pids, callback, 0) 18 | } 19 | 20 | function doRegister (pids: number[], callback: () => void, index: number) { 21 | if (index === pids.length) { 22 | callback() 23 | return 24 | } 25 | 26 | debug('registering process exit callback at pid %d...', pids[index]) 27 | 28 | const hProc = knl32.OpenProcess(SYNCHRONIZE, FALSE, pids[index]) 29 | debug('process handle: %d', hProc) 30 | 31 | knl32.WaitForSingleObject.async(hProc, INFINITE, () => { 32 | doRegister(pids, callback, index + 1) 33 | }) 34 | debug('process exit callback registered') 35 | } 36 | -------------------------------------------------------------------------------- /src/main/config/Config.ts: -------------------------------------------------------------------------------- 1 | import { app, ipcMain } from 'electron' 2 | import * as fs from 'fs' 3 | import * as jsonfile from 'jsonfile' 4 | import * as path from 'path' 5 | import IpcTypes from '../../common/IpcTypes' 6 | const debug = require('debug')('yuki:config') 7 | 8 | abstract class Config { 9 | private static readonly FILE_OPTIONS = { 10 | EOL: '\r\n', 11 | spaces: 2 12 | } 13 | 14 | protected config: any 15 | 16 | protected filePath!: string 17 | protected isSaving: boolean = false 18 | 19 | public init () { 20 | this.filePath = path.resolve(global.__baseDir, `config/${this.getFilename()}.json`) 21 | this.load() 22 | this.save() 23 | debug('%s loaded with pre-save', this.filePath) 24 | this.registerWatchCallback() 25 | return this 26 | } 27 | 28 | public load () { 29 | let fileContent 30 | try { 31 | fileContent = jsonfile.readFileSync(this.filePath) 32 | } catch (e) { 33 | debug('%s loads failed !> %s', this.filePath, e) 34 | fileContent = {} 35 | } 36 | this.config = { 37 | ...this.getDefaultObject(), 38 | ...fileContent 39 | } 40 | } 41 | 42 | public save () { 43 | try { 44 | jsonfile.writeFileSync(this.filePath, this.config, Config.FILE_OPTIONS) 45 | debug('%s saved', this.filePath) 46 | } catch (e) { 47 | debug('%s saves failed !> %s', this.filePath, e) 48 | } 49 | } 50 | 51 | public get () { 52 | return this.config 53 | } 54 | 55 | public set (cfg: any) { 56 | this.config = cfg 57 | this.save() 58 | } 59 | 60 | public abstract getFilename (): string 61 | 62 | protected abstract getDefaultObject (): object 63 | 64 | private registerWatchCallback () { 65 | fs.watch(this.filePath, {}, () => { 66 | if (this.isSaving) return 67 | try { 68 | debug('%s changed. reloading...', this.getFilename()) 69 | this.load() 70 | ipcMain.emit(IpcTypes.RELOAD_CONFIG, this.getFilename()) 71 | } catch (e) { 72 | return 73 | } 74 | }) 75 | } 76 | } 77 | 78 | export default Config 79 | -------------------------------------------------------------------------------- /src/main/config/ConfigManager.ts: -------------------------------------------------------------------------------- 1 | interface INameToConfigMap { 2 | [configName: string]: Config 3 | } 4 | 5 | import Config from './Config' 6 | import DefaultConfig from './DefaultConfig' 7 | import GamesConfig from './GamesConfig' 8 | import GuiConfig from './GuiConfig' 9 | import TextsConfig from './TextsConfig' 10 | const debug = require('debug')('yuki:configManager') 11 | 12 | export default class ConfigManager { 13 | public static getInstance (): ConfigManager { 14 | if (!this.instance) { 15 | this.instance = new ConfigManager() 16 | } 17 | return this.instance 18 | } 19 | private static instance: ConfigManager | undefined 20 | 21 | public nameToConfigMap: INameToConfigMap = { 22 | default: new DefaultConfig().init(), 23 | games: new GamesConfig().init(), 24 | texts: new TextsConfig().init(), 25 | gui: new GuiConfig().init() 26 | } 27 | 28 | public get (configName: string): T { 29 | try { 30 | return this.nameToConfigMap[configName].get() 31 | } catch (e) { 32 | debug('no config named %s. return default', configName) 33 | return this.nameToConfigMap.default.get() 34 | } 35 | } 36 | 37 | public set (configName: string, cfg: T): void { 38 | try { 39 | return this.nameToConfigMap[configName].set(cfg) 40 | } catch (e) { 41 | debug('no config named %s', configName) 42 | } 43 | } 44 | 45 | public save (configName: string): void { 46 | try { 47 | return this.nameToConfigMap[configName].save() 48 | } catch (e) { 49 | debug('no config named %s', configName) 50 | } 51 | } 52 | 53 | public getFilename (configName: string): string { 54 | try { 55 | return this.nameToConfigMap[configName].getFilename() 56 | } catch (e) { 57 | debug('no config named %s', configName) 58 | return 'default' 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/config/DefaultConfig.ts: -------------------------------------------------------------------------------- 1 | import Config from './Config' 2 | 3 | export default class DefaultConfig extends Config { 4 | public getFilename (): string { 5 | return 'config' 6 | } 7 | protected getDefaultObject (): yuki.Config.Default { 8 | return { 9 | localeChangers: { 10 | localeEmulator: { name: 'Locale Emulator', enable: false, exec: '' }, 11 | ntleas: { name: 'Ntleas', enable: false, exec: '' }, 12 | noChanger: { name: 'No Changer', enable: true, exec: '%GAME_PATH%' } 13 | }, 14 | onlineApis: [ 15 | { 16 | enable: true, 17 | external: true, 18 | jsFile: 'config\\youdaoApi.js', 19 | name: '有道' 20 | }, 21 | { 22 | enable: false, 23 | method: 'POST', 24 | name: '谷歌', 25 | requestBodyFormat: 'X{"q": %TEXT%, "sl": "ja", "tl": "zh-CN"}', 26 | responseBodyPattern: 'Rclass="t0">([^<]*)<', 27 | url: 'https://translate.google.cn/m' 28 | }, 29 | { 30 | enable: false, 31 | method: 'POST', 32 | name: '彩云', 33 | requestBodyFormat: 34 | 'J{"source": %TEXT%, "trans_type": "ja2zh", ' + 35 | '"request_id": "demo", "detect": "true"}', 36 | requestHeaders: '{"X-Authorization": "token 3975l6lr5pcbvidl6jl2"}', 37 | responseBodyPattern: 'J%RESPONSE%.target', 38 | url: 'https://api.interpreter.caiyunai.com/v1/translator' 39 | }, 40 | { 41 | enable: true, 42 | external: true, 43 | jsFile: 'config\\qqApi.js', 44 | name: '腾讯' 45 | }, 46 | { 47 | enable: false, 48 | external: true, 49 | jsFile: 'config\\tencentApi.js', 50 | name: '腾讯云' 51 | }, 52 | { 53 | enable: false, 54 | external: true, 55 | jsFile: 'config\\azureApi.js', 56 | name: 'Azure' 57 | }, 58 | { 59 | enable: false, 60 | external: true, 61 | jsFile: 'config\\baiduApi.js', 62 | name: '百度' 63 | }, 64 | { 65 | enable: false, 66 | external: true, 67 | jsFile: 'config\\newBaiduApi.js', 68 | name: '百度开放平台' 69 | } 70 | ], 71 | translators: { jBeijing: { enable: false, path: '', dictPath: '' } }, 72 | dictionaries: { lingoes: { enable: false, path: '' } }, 73 | mecab: { enable: false, path: '' }, 74 | librariesRepoUrl: 75 | 'https://github.com/project-yuki/libraries/raw/master/_pack/', 76 | language: 'zh' 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/config/GamesConfig.ts: -------------------------------------------------------------------------------- 1 | import Config from './Config' 2 | 3 | export default class GamesConfig extends Config { 4 | public getFilename (): string { 5 | return 'games' 6 | } 7 | 8 | public get () { 9 | return this.config.games 10 | } 11 | 12 | public set (cfg: any) { 13 | this.config.games = cfg 14 | this.save() 15 | } 16 | 17 | protected getDefaultObject (): { games: yuki.Config.Games } { 18 | return { games: [] } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/config/GuiConfig.ts: -------------------------------------------------------------------------------- 1 | import { screen } from 'electron' 2 | import Config from './Config' 3 | 4 | export default class GuiConfig extends Config { 5 | public getFilename (): string { 6 | return 'gui' 7 | } 8 | 9 | protected getDefaultObject (): yuki.Config.Gui { 10 | const displaySize = screen.getPrimaryDisplay().size 11 | const mainWindowWidthRatio = 0.75 12 | const mainWindowHeightRatio = 0.8 13 | const translatorWindowWidthRatio = 0.65 14 | const translatorWindowHeightRatio = 0.25 15 | 16 | return { 17 | mainWindow: { 18 | bounds: { 19 | width: Math.trunc(displaySize.width * mainWindowWidthRatio), 20 | height: Math.trunc(displaySize.height * mainWindowHeightRatio), 21 | x: Math.trunc(displaySize.width * ((1 - mainWindowWidthRatio) / 2)), 22 | y: Math.trunc(displaySize.height * ((1 - mainWindowHeightRatio) / 2)) 23 | } 24 | }, 25 | translatorWindow: { 26 | bounds: { 27 | width: Math.trunc(displaySize.width * translatorWindowWidthRatio), 28 | height: Math.trunc(displaySize.height * translatorWindowHeightRatio), 29 | x: Math.trunc( 30 | displaySize.width * ((1 - translatorWindowWidthRatio) / 2) 31 | ), 32 | y: Math.trunc(displaySize.height * 0.05) 33 | }, 34 | alwaysOnTop: true, 35 | originalText: { 36 | fontSize: 24, 37 | color: 'white' 38 | }, 39 | translationText: { 40 | fontSize: 18, 41 | color: 'white', 42 | margin: 18 43 | }, 44 | background: '#000000BD', 45 | renderMode: 'translucent', 46 | mecab: { 47 | showRomaji: false 48 | }, 49 | autoHideTitlebar: false 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/config/TextsConfig.ts: -------------------------------------------------------------------------------- 1 | import Config from './Config' 2 | 3 | export default class TextsConfig extends Config { 4 | public getFilename (): string { 5 | return 'texts' 6 | } 7 | protected getDefaultObject (): yuki.Config.Texts { 8 | return { 9 | interceptor: { 10 | shouldBeIgnore: [ 11 | 'value', 12 | 'sys', 13 | '\u00020', 14 | 'windowbtn', 15 | '00_プロローグ1', 16 | 'menu', 17 | 'WndDisp' 18 | ], 19 | ignoreAsciiOnly: false, 20 | maxLength: 1000 21 | }, 22 | modifier: { 23 | removeAscii: false, 24 | deduplicate: false, 25 | delineBreak: false 26 | }, 27 | merger: { 28 | enable: true, 29 | timeout: 500 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/index.dev.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | // Install `electron-debug` with `devtron` 9 | require('electron-debug')({ showDevTools: true }) 10 | 11 | // Install `vue-devtools` 12 | require('electron').app.on('ready', () => { 13 | require('vue-devtools').install() 14 | }) 15 | 16 | // Require `main` process to boot app 17 | require('./index') 18 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import { app, BrowserWindow, Menu, Tray } from 'electron' 3 | import * as fs from 'fs' 4 | import * as path from 'path' 5 | import ConfigManager from './config/ConfigManager' 6 | import DownloaderFactory from './DownloaderFactory' 7 | import setupIpc from './setup/Ipc' 8 | import DictManager from './translate/DictManager' 9 | const debug = require('debug')('yuki:app') 10 | 11 | // check & make ./config folder 12 | { 13 | if (!fs.existsSync('config\\')) { 14 | fs.mkdirSync('config') 15 | debug('created ./config folder') 16 | } 17 | } 18 | 19 | if (process.env.NODE_ENV !== 'development') { 20 | global.__static = require('path') 21 | .join(__dirname, '/static') 22 | .replace(/\\/g, '\\\\') 23 | } 24 | 25 | debug('__dirname: %s', path.resolve(__dirname)) 26 | 27 | global.__baseDir = path.resolve( 28 | __dirname, 29 | process.env.NODE_ENV !== 'development' ? '../../../..' : '../..' 30 | ) 31 | debug('basePath: %s', global.__baseDir) 32 | 33 | global.__appDir = path.resolve(__dirname, '../..') 34 | debug('appPath: %s', global.__appDir) 35 | 36 | const iconPath = path.join(global.__appDir, 'build/icons/icon.png') 37 | 38 | let mainWindow: Electron.BrowserWindow | null 39 | 40 | let tray: Electron.Tray | null 41 | 42 | const mainWinURL = 43 | process.env.NODE_ENV === 'development' 44 | ? `http://localhost:9080` 45 | : `file://${__dirname}/index.html` 46 | 47 | debug('mainWinURL: %s', mainWinURL) 48 | 49 | function openWindow () { 50 | if (!mainWindow) { 51 | createWindow() 52 | } else if (!mainWindow.isVisible()) { 53 | mainWindow.show() 54 | } 55 | } 56 | 57 | function createWindow () { 58 | debug('creating window...') 59 | 60 | /** 61 | * Initial main window options 62 | */ 63 | mainWindow = new BrowserWindow({ 64 | useContentSize: true, 65 | webPreferences: { 66 | defaultFontFamily: { 67 | standard: 'Microsoft Yahei UI', 68 | serif: 'Microsoft Yahei UI', 69 | sansSerif: 'Microsoft Yahei UI' 70 | } 71 | }, 72 | icon: iconPath, 73 | frame: false, 74 | show: false 75 | }) 76 | 77 | mainWindow.loadURL(mainWinURL) 78 | 79 | mainWindow.once('ready-to-show', () => { 80 | if (!mainWindow) return 81 | 82 | mainWindow.show() 83 | }) 84 | 85 | mainWindow.on('close', () => { 86 | if (!mainWindow) return 87 | 88 | debug('saving main window bounds -> %o', mainWindow.getBounds()) 89 | ConfigManager.getInstance().set('gui', { 90 | ...ConfigManager.getInstance().get('gui'), 91 | mainWindow: { 92 | bounds: mainWindow.getBounds() 93 | } 94 | }) 95 | }) 96 | 97 | mainWindow.on('closed', () => { 98 | mainWindow = null 99 | }) 100 | 101 | mainWindow.setBounds( 102 | ConfigManager.getInstance().get('gui').mainWindow.bounds 103 | ) 104 | 105 | tray = new Tray(iconPath) 106 | const contextMenu = Menu.buildFromTemplate([ 107 | { 108 | label: '打开主界面', 109 | type: 'normal', 110 | click: () => { 111 | openWindow() 112 | } 113 | }, 114 | { 115 | label: '退出', 116 | type: 'normal', 117 | click: () => { 118 | app.quit() 119 | } 120 | } 121 | ]) 122 | tray.setToolTip('yuki') 123 | tray.setContextMenu(contextMenu) 124 | tray.on('click', () => { 125 | openWindow() 126 | }) 127 | 128 | setupIpc(mainWindow) 129 | } 130 | 131 | app.on('ready', () => { 132 | DownloaderFactory.init() 133 | DictManager.init( 134 | ConfigManager.getInstance().get('default').dictionaries 135 | ) 136 | createWindow() 137 | }) 138 | 139 | app.on('activate', () => { 140 | if (!mainWindow) { 141 | createWindow() 142 | } 143 | }) 144 | 145 | /** 146 | * Auto Updater 147 | * 148 | * Uncomment the following code below and install `electron-updater` to 149 | * support auto updating. Code Signing with a valid certificate is required. 150 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 151 | */ 152 | 153 | /* 154 | import { autoUpdater } from 'electron-updater' 155 | 156 | autoUpdater.on('update-downloaded', () => { 157 | autoUpdater.quitAndInstall() 158 | }) 159 | 160 | app.on('ready', () => { 161 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 162 | }) 163 | */ 164 | -------------------------------------------------------------------------------- /src/main/middlewares/FilterMiddleware.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:filter') 2 | 3 | export default class FilterMiddleware 4 | implements yuki.Middleware { 5 | public process ( 6 | context: yuki.TextOutputObject, 7 | next: (newContext: yuki.TextOutputObject) => void 8 | ) { 9 | debug('[%d] %s', context.handle, context.text) 10 | context.code = `/${context.code}` 11 | next(context) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/middlewares/MeCabMiddleware.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs' 2 | import * as path from 'path' 3 | const debug = require('debug')('yuki:mecab') 4 | const toHiragana = require('wanakana').toHiragana 5 | const toRomaji = require('wanakana').toRomaji 6 | 7 | interface IMeCab { 8 | parseSync: (text: string) => string[][] 9 | } 10 | 11 | export default class MecabMiddleware 12 | implements yuki.Middleware { 13 | /** 14 | * Role type 15 | * See: https://answers.yahoo.com/question/index?qid=20110805070212AAdpWZf 16 | * See: https://gist.github.com/neubig/2555399 17 | */ 18 | public static readonly KANJI_TO_ABBR_MAP = { 19 | 人名: 'm', // name 20 | 地名: 'mp', // place 21 | 名詞: 'n', // noun 22 | 数詞: 'num', // number 23 | 代名詞: 'pn', // pronoun 24 | 動詞: 'v', // verb 25 | 形状詞: 'a', // http://d.hatena.ne.jp/taos/20090701/p1 26 | 連体詞: 'adn', // adnominal, http://en.wiktionary.org/wiki/連体詞 27 | 形容詞: 'adj', // adjective 28 | 副詞: 'adv', // adverb 29 | 助詞: 'p', // 動詞 = particle 30 | 助動詞: 'aux', // 助動詞 = auxiliary verb 31 | 接尾辞: 'suf', // suffix 32 | 接頭辞: 'pref', // prefix 33 | 感動詞: 'int', // interjection 34 | 接続詞: 'conj', // conjunction 35 | 補助記号: 'punct', // punctuation 36 | 記号: 'w' // letters 37 | // ROLE_PHRASE: 'x' 38 | } 39 | 40 | public static readonly ABBR_TO_COLOR_MAP = { 41 | m: '#a7ffeb', 42 | mp: '#84ffff', 43 | n: '#80d8ff', 44 | num: '#b9f6ca', 45 | pn: '#d500f9', 46 | v: '#ff9e80', 47 | a: '#bbdefb', 48 | adn: '#e1bee7', 49 | adj: '#ce93d8', 50 | adv: '#e1bee7', 51 | p: '#ffeb3b', 52 | aux: '#fff176', 53 | suf: '#fdd835', 54 | pref: '#fbc02d', 55 | int: '#ffcdd2', 56 | conj: '#ff8a65', 57 | punct: '#d32f2f', 58 | w: '#bcaaa4' 59 | } 60 | 61 | public static isMeCabString (mstring: string): boolean { 62 | return mstring.startsWith('$') 63 | } 64 | 65 | public static stringToObject (mstring: string): yuki.MeCabPatterns { 66 | if (!this.isMeCabString(mstring)) return [] 67 | 68 | const validString = mstring.substring(1) 69 | const result: yuki.MeCabPatterns = [] 70 | validString.split('|').forEach((value) => { 71 | const aWord = value.split(',') 72 | result.push({ word: aWord[0], abbr: aWord[1], kana: aWord[2] }) 73 | }) 74 | return result 75 | } 76 | 77 | public static objectToOriginalText (patterns: yuki.MeCabPatterns): string { 78 | let result = '' 79 | for (const pattern of patterns) { 80 | result += pattern.word 81 | } 82 | return result 83 | } 84 | 85 | public static kanaToRomaji (kana: string): string { 86 | return toRomaji(kana) 87 | } 88 | 89 | private mecab: IMeCab | undefined 90 | 91 | constructor (config: yuki.Config.Libraries['mecab']) { 92 | if ( 93 | !config.enable || 94 | !fs.existsSync(path.join(config.path, 'libmecab.dll')) 95 | ) { 96 | debug('disabled') 97 | return 98 | } 99 | 100 | process.env.PATH += `;${config.path}` 101 | try { 102 | this.mecab = require('mecab-ffi') 103 | debug('enabled') 104 | } catch (e) { 105 | debug('enable failed !> %s', e) 106 | } 107 | } 108 | 109 | public process ( 110 | context: yuki.TextOutputObject, 111 | next: (newContext: yuki.TextOutputObject) => void 112 | ) { 113 | if (!this.mecab) { 114 | next(context) 115 | return 116 | } 117 | 118 | const results = this.mecab.parseSync(context.text) 119 | const usefulResult = [] 120 | let toMergeLetters = '' 121 | for (const result of results) { 122 | const abbr = MecabMiddleware.KANJI_TO_ABBR_MAP[result[1]] 123 | 124 | if (abbr === 'w') { 125 | toMergeLetters += result[0] 126 | continue 127 | } 128 | 129 | if (abbr !== 'w' && toMergeLetters !== '') { 130 | usefulResult.push( 131 | `${toMergeLetters},w,` 132 | ) 133 | toMergeLetters = '' 134 | } 135 | 136 | let kana = toHiragana(result[8]) 137 | if (kana === result[0]) kana = '' 138 | usefulResult.push( 139 | `${result[0]},${abbr},${kana}` 140 | ) 141 | } 142 | 143 | if (toMergeLetters !== '') { 144 | usefulResult.push( 145 | `${toMergeLetters},w,` 146 | ) 147 | } 148 | 149 | context.text = `$${usefulResult.join('|')}` 150 | next(context) 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/middlewares/PublishMiddleware.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:publish') 2 | 3 | export default class PublishMiddleware 4 | implements yuki.Middleware { 5 | private subscribers: Electron.WebContents[] = [] 6 | private type: string 7 | constructor (type: string) { 8 | this.type = type 9 | } 10 | 11 | public subscribe (webContents: Electron.WebContents) { 12 | if (this.subscribers.find((value) => value === webContents)) { 13 | debug( 14 | 'webContents %s already subscribed type %s', 15 | webContents.getTitle(), 16 | this.type 17 | ) 18 | } else { 19 | this.subscribers.push(webContents) 20 | debug( 21 | 'webContents %s successfully subscribed type %s', 22 | webContents.getTitle(), 23 | this.type 24 | ) 25 | } 26 | } 27 | 28 | public unsubscribe (webContents: Electron.WebContents) { 29 | if (!this.subscribers.find((value) => value === webContents)) { 30 | debug( 31 | 'webContents %s has not subscribed type %s', 32 | webContents.getTitle(), 33 | this.type 34 | ) 35 | } else { 36 | this.subscribers = this.subscribers.filter( 37 | (subscriber) => subscriber !== webContents 38 | ) 39 | debug( 40 | 'webContents %s successfully unsubscribed type %s', 41 | webContents.getTitle(), 42 | this.type 43 | ) 44 | } 45 | } 46 | 47 | public process (context: yuki.TextOutputObject) { 48 | for (const subscriber of this.subscribers) { 49 | subscriber.send(this.type, context) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/middlewares/TextInterceptorMiddleware.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:textInterceptor') 2 | 3 | export default class TextInterceptorMiddleware 4 | implements yuki.Middleware { 5 | private static DEFAULT_MAX_LENGTH = 1000 6 | 7 | private maxLength: number 8 | private shouldBeIgnorePatterns: string[] 9 | private ignoreAsciiOnly: boolean 10 | 11 | constructor (config: yuki.Config.Texts['interceptor']) { 12 | this.shouldBeIgnorePatterns = config.shouldBeIgnore 13 | this.ignoreAsciiOnly = config.ignoreAsciiOnly 14 | this.maxLength = config.maxLength 15 | ? config.maxLength 16 | : TextInterceptorMiddleware.DEFAULT_MAX_LENGTH 17 | debug('initialized') 18 | } 19 | 20 | public process ( 21 | context: yuki.TextOutputObject, 22 | next: (newContext: yuki.TextOutputObject) => void 23 | ) { 24 | if (this.textShouldBeIgnore(context.text)) return 25 | if (this.ignoreAsciiOnly && this.isAsciiOnly(context.text)) return 26 | 27 | next(context) 28 | } 29 | 30 | public textShouldBeIgnore (text: string): boolean { 31 | return this.isTooLong(text) || this.containsShouldBeIgnorePattern(text) 32 | } 33 | 34 | private isTooLong (text: string) { 35 | return text.length > this.maxLength 36 | } 37 | 38 | private containsShouldBeIgnorePattern (text: string) { 39 | for (const pattern of this.shouldBeIgnorePatterns) { 40 | if (text.indexOf(pattern) > -1) { 41 | return true 42 | } 43 | } 44 | return false 45 | } 46 | 47 | private isAsciiOnly (text: string) { 48 | return /^[\x00-\xFF]*$/.test(text) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/middlewares/TextMergerMiddleware.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:merger') 2 | 3 | interface ITextStore { 4 | [handle: number]: string[] 5 | } 6 | 7 | interface IThreadStore { 8 | [handle: number]: yuki.TextOutputObject | undefined 9 | } 10 | 11 | export default class TextMergerMiddleware 12 | implements yuki.Middleware { 13 | public static DEFAULT_TIMEOUT = 500 14 | 15 | private textStore: ITextStore = {} 16 | private threadStore: IThreadStore = {} 17 | private enable: boolean 18 | private timeout: number 19 | 20 | constructor (config: yuki.Config.Texts['merger']) { 21 | this.enable = config.enable 22 | this.timeout = config.timeout 23 | ? config.timeout 24 | : TextMergerMiddleware.DEFAULT_TIMEOUT 25 | debug('initialized', this.enable) 26 | } 27 | 28 | public process ( 29 | context: yuki.TextOutputObject, 30 | next: (newContext: yuki.TextOutputObject) => void 31 | ) { 32 | if (!this.enable) { 33 | this.textStore[context.handle] = [] 34 | this.textStore[context.handle].push(context.text) 35 | this.threadStore[context.handle] = context 36 | next(context) 37 | return 38 | } 39 | 40 | if (!this.isStoreEmpty(context.handle)) { 41 | this.textStore[context.handle].push(context.text) 42 | return 43 | } 44 | 45 | this.textStore[context.handle] = [] 46 | this.textStore[context.handle].push(context.text) 47 | this.threadStore[context.handle] = context 48 | setTimeout(() => { 49 | context.text = this.textStore[context.handle] 50 | .join('') 51 | .replace(/[\r\n]/g, '') 52 | delete this.textStore[context.handle] 53 | this.threadStore[context.handle] = undefined 54 | next(context) 55 | }, this.timeout) 56 | } 57 | 58 | private isStoreEmpty (handle: number): boolean { 59 | return this.threadStore[handle] === undefined 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/middlewares/TextModifierMiddleware.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:textInterceptor') 2 | 3 | export default class TextInterceptorMiddleware 4 | implements yuki.Middleware { 5 | private removeAscii: boolean 6 | private deduplicate: boolean 7 | private delineBreak: boolean 8 | 9 | constructor (config: yuki.Config.Texts['modifier']) { 10 | this.removeAscii = config.removeAscii 11 | this.deduplicate = config.deduplicate 12 | this.delineBreak = config.delineBreak 13 | debug('initialized') 14 | } 15 | 16 | public process ( 17 | context: yuki.TextOutputObject, 18 | next: (newContext: yuki.TextOutputObject) => void 19 | ) { 20 | context.text = context.text.replace(/[\x00-\x20]/g, '') 21 | context.text = context.text.replace(/_t.*?\//g, '') 22 | 23 | if (this.removeAscii) { 24 | context.text = context.text.replace(/[\x00-\xFF]+/g, '') 25 | if (context.text === '') return 26 | } 27 | if (this.deduplicate) { 28 | context.text = context.text.replace(/([^]+?)\1+/g, '$1') 29 | if (context.text === '') return 30 | } 31 | if (this.delineBreak) { 32 | context.text = context.text.replace(/_r/g, '') 33 | context.text = context.text.replace(/
/g, '') 34 | context.text = context.text.replace(/#n/g, '') 35 | context.text = context.text.replace(/\s+/g, '') 36 | if (context.text === '') return 37 | } 38 | 39 | next(context) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/translate/Api.ts: -------------------------------------------------------------------------------- 1 | const request = require('request') 2 | import { Options } from 'request' 3 | const debug = require('debug')('yuki:api') 4 | import * as vm from 'vm' 5 | 6 | export default class Api implements yuki.Translator { 7 | private config: yuki.Config.OnlineApiItem 8 | private requestOptions: Options 9 | private responseVmContext: vm.Context = vm.createContext({ 10 | response: '', 11 | result: '' 12 | }) 13 | 14 | constructor (config: yuki.Config.OnlineApiItem) { 15 | this.config = config 16 | if ( 17 | !this.config.url || 18 | !this.config.method || 19 | !this.config.requestBodyFormat || 20 | !this.config.responseBodyPattern 21 | ) { 22 | debug( 23 | '[%s] config not contains enough information. ignore', 24 | this.config.name 25 | ) 26 | throw new TypeError() 27 | } 28 | this.requestOptions = { 29 | url: this.config.url, 30 | method: this.config.method, 31 | headers: { 32 | 'User-Agent': 33 | 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:62.0) ' + 34 | 'Gecko/20100101 Firefox/62.0' 35 | } 36 | } 37 | } 38 | 39 | public translate (text: string, callback: (translation: string) => void) { 40 | this.generateRequestBody(text) 41 | this.getResponseBody((body) => { 42 | const result = this.parseResponse(body) 43 | callback(result) 44 | }) 45 | } 46 | 47 | public isEnable () { 48 | return this.config.enable 49 | } 50 | 51 | public setEnable (isEnable: boolean) { 52 | this.config.enable = isEnable 53 | } 54 | 55 | public getName () { 56 | return this.config.name 57 | } 58 | 59 | private generateRequestBody (text: string) { 60 | if (!this.config.requestBodyFormat || !this.config.responseBodyPattern) { 61 | return 62 | } 63 | const requestBodyString = this.config.requestBodyFormat.replace( 64 | '%TEXT%', 65 | `"${text}"` 66 | ) 67 | if (this.config.requestHeaders) { 68 | this.requestOptions.headers = JSON.parse(this.config.requestHeaders) 69 | } 70 | if (this.config.requestBodyFormat.startsWith('X')) { 71 | this.requestOptions.form = JSON.parse(requestBodyString.substring(1)) 72 | } else if (this.config.requestBodyFormat.startsWith('J')) { 73 | this.requestOptions.json = JSON.parse(requestBodyString.substring(1)) 74 | } else { 75 | debug( 76 | '[%s] no such request body type: %s', 77 | this.config.name, 78 | this.config.requestBodyFormat.substring(0, 1) 79 | ) 80 | } 81 | } 82 | 83 | private getResponseBody (callback: (body: any) => void) { 84 | request(this.requestOptions, (error: Error, response: any, body: any) => { 85 | if (error) debug('[%s error] %s', this.config.name, error) 86 | 87 | callback(body) 88 | }) 89 | } 90 | 91 | private parseResponse (body: string): string { 92 | if (!this.config.responseBodyPattern) return '' 93 | 94 | if (this.config.responseBodyPattern.startsWith('J')) { 95 | return this.parseResponseByJsObject(body) 96 | } else if (this.config.responseBodyPattern.startsWith('R')) { 97 | return this.fixEscapeCharacters( 98 | this.parseResponseByRegExp(body) 99 | ) 100 | } else { 101 | debug( 102 | '[%s] no such response parser type: %s', 103 | this.config.name, 104 | this.config.responseBodyPattern.substring(0, 1) 105 | ) 106 | return '' 107 | } 108 | } 109 | 110 | private parseResponseByJsObject (body: string | object): string { 111 | if (!this.config.responseBodyPattern) return '' 112 | 113 | debug('[%s] get raw response: %o', this.config.name, body) 114 | if (typeof body === 'string') body = JSON.parse(body) 115 | this.responseVmContext.response = body 116 | const scriptString = this.config.responseBodyPattern 117 | .substring(1) 118 | .replace('%RESPONSE%', `result = response`) 119 | try { 120 | vm.runInNewContext(scriptString, this.responseVmContext) 121 | } catch (e) { 122 | return `ERR: ${e}` 123 | } 124 | return this.responseVmContext.result 125 | } 126 | 127 | private parseResponseByRegExp (body: string): string { 128 | if (!this.config.responseBodyPattern) return '' 129 | 130 | const pattern = new RegExp(this.config.responseBodyPattern.substring(1)) 131 | const response = pattern.exec(body) 132 | if (response) { 133 | return response[1] 134 | } else { 135 | return '' 136 | } 137 | } 138 | 139 | private fixEscapeCharacters (body: string): string { 140 | return body 141 | .replace(/"/g, '"') 142 | .replace(/"/g, '"') 143 | .replace(/'/g, "'") 144 | .replace(/'/g, "'") 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main/translate/DictManager.ts: -------------------------------------------------------------------------------- 1 | import ConfigManager from '../config/ConfigManager' 2 | import LingoesDict from './LingoesDict' 3 | 4 | export default class DictManager { 5 | public static init (config: yuki.Config.Dictionaries) { 6 | this.lingoes = new LingoesDict(config.lingoes) 7 | } 8 | 9 | public static find (options: yuki.DictOptions, callback: (result: yuki.DictResult) => void) { 10 | if (options.dict !== 'lingoes') callback({ found: false }) 11 | 12 | this.lingoes.find(options.word, callback) 13 | } 14 | private static lingoes: LingoesDict 15 | } 16 | -------------------------------------------------------------------------------- /src/main/translate/ExternalApi.ts: -------------------------------------------------------------------------------- 1 | import * as crypto from 'crypto' 2 | import * as fs from 'fs' 3 | import * as path from 'path' 4 | import * as request from 'request-promise-native' 5 | import * as vm from 'vm' 6 | const debug = require('debug')('yuki:api') 7 | 8 | export default class ExternalApi implements yuki.Translator { 9 | private config: yuki.Config.OnlineApiItem 10 | private responseVmContext!: vm.Context 11 | private scriptString: string = '' 12 | private absolutePath: string = '' 13 | 14 | constructor (config: yuki.Config.OnlineApiItem) { 15 | this.config = config 16 | if (!this.config.jsFile) { 17 | debug( 18 | '[%s] config not contains enough information. ignore', 19 | this.config.name 20 | ) 21 | throw new TypeError() 22 | } 23 | this.loadExternalJsFile() 24 | this.createVmContext() 25 | this.registerWatchCallback() 26 | } 27 | 28 | public translate (text: string, callback: (translation: string) => void) { 29 | this.responseVmContext.text = text 30 | this.responseVmContext.callback = callback 31 | try { 32 | vm.runInContext(this.scriptString, this.responseVmContext, { 33 | displayErrors: true 34 | }) 35 | } catch (e) { 36 | debug('[%s] runtime error !> %s', this.config.name, e.stack) 37 | } 38 | } 39 | 40 | public isEnable () { 41 | return this.config.enable 42 | } 43 | 44 | public setEnable (isEnable: boolean) { 45 | this.config.enable = isEnable 46 | } 47 | 48 | public getName () { 49 | return this.config.name 50 | } 51 | 52 | private loadExternalJsFile () { 53 | if (!this.config.jsFile) return 54 | 55 | this.absolutePath = path.join(global.__baseDir, this.config.jsFile) 56 | try { 57 | this.scriptString = fs.readFileSync(this.absolutePath, 'utf8') 58 | debug('external file %s loaded', this.absolutePath) 59 | } catch (e) { 60 | debug('external file %s loads failed !> %s', this.absolutePath, e) 61 | } 62 | } 63 | 64 | private createVmContext () { 65 | this.responseVmContext = vm.createContext({ 66 | Request: request, 67 | text: '', 68 | md5: (data: string, encoding: crypto.HexBase64Latin1Encoding) => { 69 | const hash = crypto.createHash('md5') 70 | return hash.update(data).digest(encoding) 71 | }, 72 | crypto: { 73 | createHash: crypto.createHash, 74 | createHmac: crypto.createHmac 75 | }, 76 | callback: undefined 77 | }) 78 | } 79 | 80 | private registerWatchCallback () { 81 | fs.watch(this.absolutePath, {}, () => { 82 | debug('[%s] script file changed. reloading...', this.config.name) 83 | this.loadExternalJsFile() 84 | this.createVmContext() 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/translate/JBeijing.ts: -------------------------------------------------------------------------------- 1 | import * as ffi from 'ffi' 2 | import * as fs from 'fs' 3 | import * as path from 'path' 4 | import * as ref from 'ref' 5 | const debug = require('debug')('yuki:jbeijing') 6 | 7 | export default class JBeijing { 8 | public static readonly DICT_PATH = 'lib\\dict\\jb\\' 9 | 10 | // REFERENCE: Viusal Novel Reader jbjct.py 11 | public static readonly TEXT_BUFFER_SIZE = 3000 12 | // Magic number! See WideCharToMultiByte used by DJC_OpenAllUserDic_Unicode in JBJCT.dll 13 | // First call: ESI 14 | // Second call: ESI + 0x408 15 | // Third call: ESI + 0x810 16 | public static readonly USERDIC_PATH_SIZE = 0x408 // sizeof(char) 17 | public static readonly MAX_USERDIC_COUNT = 3 // maximum number of user-defined dic 18 | public static readonly USERDIC_BUFFER_SIZE = 19 | 2 * JBeijing.USERDIC_PATH_SIZE * JBeijing.MAX_USERDIC_COUNT // 1548, sizeof(char) 20 | 21 | private exePath: string 22 | private jbjct!: any 23 | private outBuffer!: Buffer 24 | private bufBuffer!: Buffer 25 | private bufferSize = ref.alloc(ref.types.int, JBeijing.TEXT_BUFFER_SIZE) 26 | private userdicBuffer: Buffer | undefined 27 | 28 | constructor (exePath: string) { 29 | this.exePath = exePath 30 | try { 31 | this.checkExePathAndThrow() 32 | this.checkAndMakeDictDir() 33 | process.env.PATH += `;${exePath}` 34 | this.initializeJbjct() 35 | } catch (e) { 36 | return 37 | } 38 | } 39 | 40 | public loadUserDic (dicPath?: string) { 41 | if (!dicPath) { 42 | dicPath = JBeijing.DICT_PATH 43 | } 44 | this.makeUserdicBuffer(dicPath) 45 | const statusCode = this.jbjct.DJC_OpenAllUserDic_Unicode( 46 | this.userdicBuffer, 47 | 0 48 | ) 49 | if (statusCode === 1 || statusCode === -255) { 50 | debug('user dict loaded') 51 | } else { 52 | debug('cannot load user dict. abort') 53 | } 54 | } 55 | 56 | public translate (text: string, destCodePage: number, callback: (translation: string) => void) { 57 | this.initializeBuffers() 58 | this.jbjct.JC_Transfer_Unicode.async( 59 | 0, 60 | 932, 61 | destCodePage, 62 | 1, 63 | 1, 64 | Buffer.from(`${text}\0`, 'ucs2'), 65 | this.outBuffer, 66 | ref.ref(this.bufferSize), 67 | this.bufBuffer, 68 | ref.ref(this.bufferSize), 69 | () => { 70 | callback(ref.reinterpretUntilZeros(this.outBuffer, 2).toString('ucs2')) 71 | } 72 | ) 73 | } 74 | 75 | private checkExePathAndThrow () { 76 | if (!fs.existsSync(path.join(this.exePath, 'JBJCT.dll'))) { 77 | debug('there is no jbeijing translator in path %s. abort', this.exePath) 78 | throw new Error() 79 | } 80 | } 81 | 82 | private checkAndMakeDictDir () { 83 | if (!fs.existsSync(JBeijing.DICT_PATH)) { 84 | debug('user dict path not exists. ignore') 85 | } 86 | } 87 | 88 | private initializeJbjct () { 89 | this.jbjct = ffi.Library('JBJCT.dll', { 90 | JC_Transfer_Unicode: [ 91 | 'int', 92 | [ 93 | 'uint', 94 | 'uint', 95 | 'uint', 96 | 'int', 97 | 'int', 98 | ref.types.CString, 99 | ref.types.CString, 100 | ref.refType(ref.types.int), 101 | ref.types.CString, 102 | ref.refType(ref.types.int) 103 | ] 104 | ], 105 | DJC_OpenAllUserDic_Unicode: ['int', [ref.types.CString, 'int']], 106 | DJC_CloseAllUserDic: ['void', ['int']] 107 | }) 108 | } 109 | 110 | private initializeBuffers () { 111 | this.outBuffer = Buffer.alloc(JBeijing.TEXT_BUFFER_SIZE * 2) 112 | this.bufBuffer = Buffer.alloc(JBeijing.TEXT_BUFFER_SIZE * 2) 113 | } 114 | 115 | private makeUserdicBuffer (basePath: string) { 116 | this.userdicBuffer = Buffer.alloc(JBeijing.USERDIC_BUFFER_SIZE, 0) 117 | const userdicPaths = this.findAvailableUserdicPaths(basePath) 118 | for (let i = 0; i < userdicPaths.length; i++) { 119 | if (i >= JBeijing.MAX_USERDIC_COUNT) break 120 | if (userdicPaths[i].length > JBeijing.USERDIC_PATH_SIZE / 2) { 121 | debug("user dict path is to long: %s. didn't load it", userdicPaths[i]) 122 | continue 123 | } 124 | userdicPaths[i] = path.join(userdicPaths[i], 'Jcuser') 125 | this.userdicBuffer.write( 126 | userdicPaths[i], 127 | JBeijing.USERDIC_PATH_SIZE * i, 128 | JBeijing.USERDIC_PATH_SIZE, 129 | 'ucs2' 130 | ) 131 | } 132 | debug('trying to load user dict from %O', userdicPaths) 133 | } 134 | 135 | private findAvailableUserdicPaths (basePath: string): string[] { 136 | const paths: string[] = [] 137 | this.walk(basePath, paths) 138 | return paths 139 | } 140 | 141 | private walk (basePath: string, out: string[]) { 142 | const dirList = fs.readdirSync(basePath) 143 | dirList.forEach((item) => { 144 | if ( 145 | fs.statSync(path.join(basePath, item)).isFile() && 146 | item.toLowerCase() === 'jcuser.dic' 147 | ) { 148 | out.push(basePath) 149 | } 150 | }) 151 | 152 | dirList.forEach((item) => { 153 | if (fs.statSync(path.join(basePath, item)).isDirectory()) { 154 | this.walk(path.join(basePath, item), out) 155 | } 156 | }) 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/main/translate/JBeijingAdapter.ts: -------------------------------------------------------------------------------- 1 | import { existsSync } from 'fs' 2 | import JBeijing from './JBeijing' 3 | 4 | export default class JBeijingAdapter implements yuki.Translator { 5 | private config: yuki.Config.JBeijing 6 | private jb: JBeijing 7 | 8 | constructor (config: yuki.Config.JBeijing) { 9 | this.config = config 10 | this.jb = new JBeijing(config.path) 11 | if (config.dictPath && existsSync(config.dictPath)) { 12 | this.jb.loadUserDic(config.dictPath) 13 | } 14 | } 15 | 16 | public translate (text: string, callback: (translation: string) => void) { 17 | this.jb.translate(text, this.config.traditionalChinese ? 950 : 936, callback) 18 | } 19 | 20 | public isEnable (): boolean { 21 | return this.config.enable 22 | } 23 | public setEnable (isEnable: boolean): void { 24 | this.config.enable = isEnable 25 | } 26 | public getName (): string { 27 | return 'JBeijing' 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/translate/TranslationManager.ts: -------------------------------------------------------------------------------- 1 | import Api from './Api' 2 | import JBeijingAdapter from './JBeijingAdapter' 3 | const debug = require('debug')('yuki:translationManager') 4 | import ExternalApi from './ExternalApi' 5 | 6 | export default class TranslationManager { 7 | public static getInstance (): TranslationManager { 8 | if (!this.instance) { 9 | this.instance = new TranslationManager() 10 | } 11 | return this.instance 12 | } 13 | private static instance: TranslationManager | undefined 14 | 15 | private apis: yuki.Translator[] = [] 16 | 17 | public initializeApis ( 18 | apis: yuki.Config.Default['onlineApis'] 19 | ): TranslationManager { 20 | this.apis = [] 21 | for (const api of apis) { 22 | try { 23 | if (api.external && api.jsFile) { 24 | this.apis[api.name] = new ExternalApi(api) 25 | } else { 26 | this.apis[api.name] = new Api(api) 27 | } 28 | } catch (e) { 29 | continue 30 | } 31 | } 32 | return this 33 | } 34 | 35 | public initializeTranslators ( 36 | translators: yuki.Config.Default['translators'] 37 | ) { 38 | if (translators.jBeijing && translators.jBeijing.enable) { 39 | const jb = new JBeijingAdapter(translators.jBeijing) 40 | this.apis[jb.getName()] = jb 41 | } 42 | } 43 | 44 | public translate ( 45 | text: string, 46 | callback: (translation: yuki.Translations['translations']) => void 47 | ) { 48 | let toTranslateCount = 0 49 | for (const key in this.apis) { 50 | if (this.apis[key].isEnable()) { 51 | toTranslateCount++ 52 | this.apis[key].translate(text, (translation) => { 53 | debug('[%s] -> %s', this.apis[key].getName(), translation) 54 | callback({ 55 | [this.apis[key].getName()]: translation 56 | }) 57 | }) 58 | } 59 | } 60 | if (toTranslateCount === 0) { 61 | callback({}) 62 | } 63 | } 64 | 65 | public translateAll ( 66 | text: string, 67 | callback: (translations: yuki.Translations) => void 68 | ) { 69 | let toTranslateCount = 0 70 | let finishedCount = 0 71 | const result: yuki.Translations = { original: text, translations: {} } 72 | for (const key in this.apis) { 73 | if (this.apis[key].isEnable()) { 74 | toTranslateCount++ 75 | this.apis[key].translate(text, (translation) => { 76 | result.translations[key] = translation 77 | finishedCount++ 78 | if (finishedCount === toTranslateCount) { 79 | callback(result) 80 | } 81 | }) 82 | } 83 | } 84 | if (toTranslateCount === 0) { 85 | callback(result) 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 31 | 32 | 82 | -------------------------------------------------------------------------------- /src/renderer/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/src/renderer/assets/.gitkeep -------------------------------------------------------------------------------- /src/renderer/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/src/renderer/assets/icon.png -------------------------------------------------------------------------------- /src/renderer/components/AboutPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 39 | 40 | 42 | -------------------------------------------------------------------------------- /src/renderer/components/AddGamePage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "chooseGamePath": "选择游戏路径", 6 | "gameName": "游戏名称", 7 | "gamePath": "游戏路径", 8 | "nextStep": "下一步", 9 | "pleaseInputSpecialCodeEmptyIfNotNeeded": "请输入特殊码(如果无需则为空)", 10 | "specialCode": "特殊码", 11 | "prevStep": "上一步", 12 | "finish": "完成", 13 | "gameAdded": "添加成功!" 14 | }, 15 | "en": { 16 | "chooseGamePath": "Choose Game Path", 17 | "gameName": "Game Name", 18 | "gamePath": "Game Path", 19 | "nextStep": "Next", 20 | "pleaseInputSpecialCodeEmptyIfNotNeeded": "Please input special code (empty if not needed)", 21 | "specialCode": "Special Code", 22 | "prevStep": "Prev", 23 | "finish": "Finish", 24 | "gameAdded": "Game Added!" 25 | } 26 | } 27 | 28 | 29 | 60 | 61 | 153 | 154 | 156 | -------------------------------------------------------------------------------- /src/renderer/components/AppSidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 92 | 93 | 102 | 103 | 110 | -------------------------------------------------------------------------------- /src/renderer/components/DebugMessagesPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "log": "日志", 6 | "maxColumns": "最大行数", 7 | "copyToClipboard": "复制到剪贴板", 8 | "logCopied": "已复制日志" 9 | }, 10 | "en": { 11 | "log": "Log", 12 | "maxColumns": "Max Columns", 13 | "copyToClipboard": "Copy To Clipboard", 14 | "logCopied": "Log Copied" 15 | } 16 | } 17 | 18 | 19 | 34 | 35 | 103 | 104 | 109 | -------------------------------------------------------------------------------- /src/renderer/components/DownloadProgress.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 45 | 46 | 61 | -------------------------------------------------------------------------------- /src/renderer/components/GamesPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "nothingHere": "什么都没有呢(っ °Д °;)っ", 6 | "goAddSomeGames": "快去添加游戏吧~ヾ(•ω•`)o", 7 | "startFromProcess": "从进程启动", 8 | "start": "启动" 9 | }, 10 | "en": { 11 | "nothingHere": "Hmmm nothing here (っ °Д °;)っ", 12 | "goAddSomeGames": "Go add some games now~ヾ(•ω•`)o", 13 | "startFromProcess": "Start From Process", 14 | "start": "Start" 15 | } 16 | } 17 | 18 | 19 | 75 | 76 | 134 | 135 | 140 | -------------------------------------------------------------------------------- /src/renderer/components/PageContent.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | 30 | -------------------------------------------------------------------------------- /src/renderer/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | 42 | 56 | -------------------------------------------------------------------------------- /src/renderer/components/SettingsPage.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /src/renderer/components/TranslatorSettings.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "underConstruction": "开发中m( _ _ )m", 6 | "pleaseModifyConfigurationFile": "请通过修改配置文件 config\\config.json 来配置翻译器", 7 | "defaultTranslators": "(默认提供腾讯,百度,谷歌,有道,彩云翻译)" 8 | }, 9 | "en": { 10 | "underConstruction": "Under construction m( _ _ )m", 11 | "pleaseModifyConfigurationFile": "Please modify configuration file config\\config.json to configure translators", 12 | "defaultTranslators": "(Defualt translators are QQ, Baidu, Google, YouDao. These are all Japanese-Chinese translators)" 13 | } 14 | } 15 | 16 | 17 | 31 | 32 | 51 | 52 | 61 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | import IpcTypes from '../common/IpcTypes' 3 | 4 | import vuetify from '../common/vuetify' 5 | 6 | import axios from 'axios' 7 | import Vue from 'vue' 8 | 9 | import App from './App.vue' 10 | import router from './router' 11 | import store from './store' 12 | 13 | import VueI18n from 'vue-i18n' 14 | Vue.use(VueI18n) 15 | 16 | import 'xterm/css/xterm.css' 17 | 18 | if (!process.env.IS_WEB) { 19 | Vue.use(require('vue-electron')) 20 | } 21 | (Vue as any).http = Vue.prototype.$http = axios 22 | Vue.config.productionTip = false 23 | 24 | let locale = 'zh' 25 | const callback = (event: Electron.Event, name: string, cfg: any) => { 26 | locale = cfg.language 27 | next() 28 | } 29 | ipcRenderer.once(IpcTypes.HAS_CONFIG, callback) 30 | ipcRenderer.send(IpcTypes.REQUEST_CONFIG, 'default') 31 | 32 | function next () { 33 | const i18n = new VueI18n({ 34 | locale, 35 | messages: { 36 | zh: { gameAborted: '游戏运行失败,请参考调试信息' }, 37 | en: { gameAborted: 'Game aborted. Please refer to debug messages' } 38 | } 39 | }) 40 | 41 | const vue = new Vue({ 42 | vuetify, 43 | router, 44 | store, 45 | i18n, 46 | render: (h) => h(App) 47 | }).$mount('#app') 48 | 49 | ipcRenderer.on( 50 | IpcTypes.HAS_CONFIG, 51 | (event: Electron.Event, name: string, cfgs: object) => { 52 | store.dispatch('Config/setConfig', { name, cfgs }) 53 | } 54 | ) 55 | ipcRenderer.on( 56 | IpcTypes.HAS_NEW_DEBUG_MESSAGE, 57 | (event: Electron.Event, message: string) => { 58 | store.commit('Gui/NEW_DEBUG_MESSAGE', { value: message }) 59 | } 60 | ) 61 | ipcRenderer.on( 62 | IpcTypes.GAME_ABORTED, 63 | () => { 64 | vue.$dialog.notify.error(vue.$i18n.t('gameAborted').toString()) 65 | store.commit('Gui/SET_GAME_STARTING_ENDED', { value: true }) 66 | } 67 | ) 68 | ipcRenderer.on( 69 | IpcTypes.HAS_RUNNING_GAME, 70 | () => { 71 | store.commit('Gui/SET_GAME_STARTING_ENDED', { value: true }) 72 | } 73 | ) 74 | ipcRenderer.on( 75 | IpcTypes.HAS_PROCESSES, 76 | (event: Electron.Event, processes: yuki.Processes) => { 77 | store.commit('Gui/SET_PROCESSES', { value: processes }) 78 | } 79 | ) 80 | } 81 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import AboutPage from '@/components/AboutPage.vue' 5 | import AddGamePage from '@/components/AddGamePage.vue' 6 | import DebugMessagesPage from '@/components/DebugMessagesPage.vue' 7 | import GamesPage from '@/components/GamesPage.vue' 8 | import LibrarySettings from '@/components/LibrarySettings.vue' 9 | import LocaleChangerSettings from '@/components/LocaleChangerSettings.vue' 10 | import SettingsPage from '@/components/SettingsPage.vue' 11 | import TranslatorSettings from '@/components/TranslatorSettings.vue' 12 | 13 | Vue.use(Router) 14 | 15 | export default new Router({ 16 | routes: [ 17 | { path: '', redirect: '/games' }, 18 | { path: '/games', component: GamesPage }, 19 | { path: '/addgame', component: AddGamePage }, 20 | { 21 | path: '/settings', 22 | component: SettingsPage, 23 | children: [ 24 | { 25 | path: 'localechanger', 26 | component: LocaleChangerSettings 27 | }, 28 | { 29 | path: 'library', 30 | component: LibrarySettings 31 | }, 32 | { 33 | path: 'translator', 34 | component: TranslatorSettings 35 | } 36 | ] 37 | }, 38 | { path: '/debugMessages', component: DebugMessagesPage }, 39 | { path: '/about', component: AboutPage } 40 | ] 41 | }) 42 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as Vuex from 'vuex' 3 | import modules from './modules' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | modules, 9 | strict: process.env.NODE_ENV !== 'production' 10 | }) 11 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Config.ts: -------------------------------------------------------------------------------- 1 | const debug = require('debug')('yuki:config') 2 | import { Commit } from 'vuex' 3 | 4 | const configState: any = { 5 | default: {}, 6 | games: [], 7 | librariesBaseStorePath: '' 8 | } 9 | 10 | const mutations = { 11 | SET_CONFIG (state: yuki.ConfigState, payload: { name: string; cfgs: any }) { 12 | switch (payload.name) { 13 | case 'default': 14 | state.default = payload.cfgs 15 | break 16 | case 'games': 17 | state.games = payload.cfgs 18 | break 19 | case 'librariesBaseStorePath': 20 | state.librariesBaseStorePath = payload.cfgs 21 | break 22 | default: 23 | debug('invalid config name: %s', payload.name) 24 | break 25 | } 26 | } 27 | } 28 | 29 | const actions = { 30 | setConfig ( 31 | { commit }: { commit: Commit }, 32 | { name, cfgs }: { name: string; cfgs: any } 33 | ) { 34 | debug('[%s] get from main process -> %O', name, cfgs) 35 | commit('SET_CONFIG', { name, cfgs }) 36 | if (name === 'games') { 37 | if (cfgs.length === 0) { 38 | commit('Gui/SET_NO_GAME', { value: true }, { root: true }) 39 | } else { 40 | commit('Gui/SET_NO_GAME', { value: false }, { root: true }) 41 | } 42 | } 43 | } 44 | } 45 | 46 | export default { 47 | state: configState, 48 | mutations, 49 | actions 50 | } 51 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Gui.ts: -------------------------------------------------------------------------------- 1 | const MAX_DEBUG_MESSAGES_COLUMNS = 1000 2 | 3 | const guiState: yuki.GuiState = { 4 | noGame: false, 5 | debugMessages: [], 6 | isGameStartingEnded: false, 7 | processes: [] 8 | } 9 | 10 | const getters = { 11 | getProcessesWithText: (state: yuki.GuiState) => () => { 12 | const processesWithText: yuki.Processes = [...state.processes]; 13 | (processesWithText as yuki.ProcessesWithText).forEach((value) => { 14 | value.text = `${value.pid} - ${value.name}` 15 | }) 16 | return processesWithText 17 | } 18 | } 19 | 20 | const mutations = { 21 | SET_NO_GAME (state: yuki.GuiState, payload: { value: boolean }) { 22 | state.noGame = payload.value 23 | }, 24 | NEW_DEBUG_MESSAGE (state: yuki.GuiState, payload: { value: string }) { 25 | state.debugMessages.push(payload.value) 26 | if (state.debugMessages.length > MAX_DEBUG_MESSAGES_COLUMNS) { 27 | state.debugMessages.shift() 28 | } 29 | }, 30 | SET_GAME_STARTING_ENDED (state: yuki.GuiState, payload: { value: boolean }) { 31 | state.isGameStartingEnded = payload.value 32 | }, 33 | SET_PROCESSES (state: yuki.GuiState, payload: { value: yuki.Processes }) { 34 | state.processes = payload.value 35 | } 36 | } 37 | 38 | export default { 39 | state: guiState, 40 | mutations, 41 | getters 42 | } 43 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = (require as any).context('.', false, /\.ts$/) 7 | const modules = {} 8 | 9 | files.keys().forEach((key: string) => { 10 | if (key === './index.ts') return 11 | modules[key.replace(/(\.\/|\.ts)/g, '')] = files(key).default 12 | modules[key.replace(/(\.\/|\.ts)/g, '')].namespaced = true 13 | }) 14 | 15 | export default modules 16 | -------------------------------------------------------------------------------- /src/translator.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | YUKI Galgame Translator 6 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 7 | 8 | 13 | <% } %> 14 | 15 | 16 | 17 |
18 | 19 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/translator/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/project-yuki/YUKI/cef2d7e37e2ded300226ebf21dde9831390e9da3/src/translator/assets/.gitkeep -------------------------------------------------------------------------------- /src/translator/class-component-hooks.ts: -------------------------------------------------------------------------------- 1 | // class-component-hooks.js 2 | import Component from 'vue-class-component' 3 | 4 | // Register the router hooks with their names 5 | Component.registerHooks([ 6 | 'beforeRouteEnter', 7 | 'beforeRouteLeave', 8 | 'beforeRouteUpdate' // for vue-router 2.2+ 9 | ]) 10 | -------------------------------------------------------------------------------- /src/translator/common/Window.ts: -------------------------------------------------------------------------------- 1 | import { remote } from 'electron' 2 | 3 | export function updateWindowHeight ( 4 | component: Vue.default, addBodyHeight: boolean, offset: number) { 5 | let newHeight = offset 6 | if (addBodyHeight) newHeight += document.body.offsetHeight 7 | const window = remote.getCurrentWindow() 8 | const width = window.getSize()[0] 9 | if (component) { 10 | if (newHeight > 640) { 11 | component.$nextTick(() => { 12 | component.$store.dispatch('View/setWindowTooHigh', true) 13 | }) 14 | } else { 15 | component.$nextTick(() => { 16 | component.$store.dispatch('View/setWindowTooHigh', false) 17 | }) 18 | } 19 | } 20 | window.setSize(width, newHeight) 21 | return newHeight 22 | } 23 | -------------------------------------------------------------------------------- /src/translator/components/HookSettings.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "addHook": "加载钩子", 6 | "invalidHookFormat": "特殊码格式不正确" 7 | }, 8 | "en": { 9 | "addHook": "Add Hook", 10 | "invalidHookFormat": "Invalid hook format" 11 | } 12 | } 13 | 14 | 15 | 42 | 43 | 129 | 130 | 135 | -------------------------------------------------------------------------------- /src/translator/components/HookSettingsHookInfo.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | { 4 | "zh": { 5 | "waitForTexts...": "等待文本获取...", 6 | "choose": "选择", 7 | "lastText": "最新文本" 8 | }, 9 | "en": { 10 | "waitForTexts...": "Wait for texts...", 11 | "choose": "Choose", 12 | "lastText": "Last Text" 13 | } 14 | } 15 | 16 | 17 | 50 | 51 | 99 | 100 | 117 | -------------------------------------------------------------------------------- /src/translator/components/HooksPage.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /src/translator/components/TextDisplay.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 43 | 44 | 57 | -------------------------------------------------------------------------------- /src/translator/components/Titlebar.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | 109 | 110 | 151 | -------------------------------------------------------------------------------- /src/translator/main.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, remote } from 'electron' 2 | import IpcTypes from '../common/IpcTypes' 3 | 4 | import './class-component-hooks' 5 | 6 | import vuetify from '../common/vuetify' 7 | 8 | import axios from 'axios' 9 | import Vue from 'vue' 10 | 11 | import App from './App.vue' 12 | import router from './router' 13 | import store from './store' 14 | 15 | import VueI18n from 'vue-i18n' 16 | Vue.use(VueI18n) 17 | 18 | if (!process.env.IS_WEB) { 19 | Vue.use(require('vue-electron')) 20 | } 21 | (Vue as any).http = Vue.prototype.$http = axios 22 | Vue.config.productionTip = false 23 | 24 | let locale = 'zh' 25 | const callback = (event: Electron.Event, name: string, cfg: any) => { 26 | locale = cfg.language 27 | next() 28 | } 29 | ipcRenderer.once(IpcTypes.HAS_CONFIG, callback) 30 | ipcRenderer.send(IpcTypes.REQUEST_CONFIG, 'default') 31 | 32 | function next () { 33 | const i18n = new VueI18n({ 34 | locale 35 | }) 36 | 37 | new Vue({ 38 | vuetify, 39 | router, 40 | store, 41 | i18n, 42 | render: (h) => h(App) 43 | }).$mount('#app') 44 | 45 | ipcRenderer.on( 46 | IpcTypes.HAS_HOOK_TEXT, 47 | (event: Electron.Event, hook: yuki.TextOutputObject) => { 48 | if ((store.state as any).View.pauseNewText) return 49 | 50 | if (!remote.getCurrentWindow().isVisible()) { 51 | remote.getCurrentWindow().show() 52 | } 53 | const text = hook.text 54 | delete hook.text 55 | store.dispatch('Hooks/setHookTextOrPatterns', { hook, text }) 56 | } 57 | ) 58 | ipcRenderer.on( 59 | IpcTypes.HAS_CONFIG, 60 | (event: Electron.Event, name: string, cfgs: object) => { 61 | store.dispatch('Config/setConfig', { name, cfgs }) 62 | } 63 | ) 64 | ipcRenderer.on( 65 | IpcTypes.HAS_TRANSLATION, 66 | (event: Electron.Event, message: yuki.TranslationMessage) => { 67 | store.dispatch('Hooks/mergeTranslation', message) 68 | } 69 | ) 70 | ipcRenderer.on( 71 | IpcTypes.HAS_DICT, 72 | (event: Electron.Event, message: yuki.DictResult) => { 73 | store.dispatch('View/setDict', message) 74 | } 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/translator/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | 4 | import HooksPage from '@/components/HooksPage.vue' 5 | import SettingsPage from '@/components/SettingsPage.vue' 6 | import TranslatePage from '@/components/TranslatePage.vue' 7 | 8 | Vue.use(Router) 9 | 10 | const routes = [ 11 | { path: '', component: TranslatePage }, 12 | { path: '/translate', component: TranslatePage }, 13 | { path: '/hooks', component: HooksPage }, 14 | { path: '/settings', component: SettingsPage } 15 | ] 16 | 17 | export default new Router({ 18 | routes 19 | }) 20 | -------------------------------------------------------------------------------- /src/translator/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import * as Vuex from 'vuex' 3 | import modules from './modules' 4 | 5 | Vue.use(Vuex) 6 | 7 | export default new Vuex.Store({ 8 | modules, 9 | strict: process.env.NODE_ENV !== 'production' 10 | }) 11 | -------------------------------------------------------------------------------- /src/translator/store/modules/Config.ts: -------------------------------------------------------------------------------- 1 | import { Commit } from 'vuex' 2 | const debug = require('debug')('yuki:translatorWindow') 3 | import { ipcRenderer, remote } from 'electron' 4 | import IpcTypes from '../../../common/IpcTypes' 5 | 6 | let isSavingConfig = false 7 | 8 | const configState: any = { 9 | default: {}, 10 | game: { 11 | name: '', 12 | code: '', 13 | path: '', 14 | localeChanger: '' 15 | }, 16 | gui: { 17 | originalText: { 18 | fontSize: 0, 19 | color: '' 20 | }, 21 | translationText: { 22 | fontSize: 0, 23 | color: '', 24 | margin: 0 25 | }, 26 | background: '', 27 | autoHideTitlebar: false, 28 | mecab: { 29 | showRomaji: false 30 | } 31 | } 32 | } 33 | 34 | const getters = { 35 | getOriginalText: (state: yuki.TranslatorConfigState) => () => { 36 | return state.gui.originalText 37 | }, 38 | getTranslationText: (state: yuki.TranslatorConfigState) => () => { 39 | return state.gui.translationText 40 | }, 41 | getBackgroundColor: (state: yuki.TranslatorConfigState) => () => { 42 | return state.gui.background 43 | }, 44 | getAutoHideTitlebar: (state: yuki.TranslatorConfigState) => () => { 45 | return state.gui.autoHideTitlebar 46 | }, 47 | getMecab: (state: yuki.TranslatorConfigState) => () => { 48 | return state.gui.mecab 49 | } 50 | } 51 | 52 | const mutations = { 53 | SET_CONFIG ( 54 | state: yuki.TranslatorConfigState, 55 | payload: { name: string; cfgs: any } 56 | ) { 57 | switch (payload.name) { 58 | case 'default': 59 | state.default = payload.cfgs 60 | break 61 | case 'game': 62 | state.game = payload.cfgs 63 | break 64 | case 'gui': 65 | state.gui = payload.cfgs.translatorWindow 66 | break 67 | default: 68 | debug('invalid config name: %s', payload.name) 69 | break 70 | } 71 | }, 72 | SET_ORIGINAL_TEXT_SIZE ( 73 | state: yuki.TranslatorConfigState, 74 | payload: { size: number } 75 | ) { 76 | state.gui.originalText = { 77 | ...state.gui.originalText, 78 | fontSize: payload.size 79 | } 80 | }, 81 | SET_TRANSLATION_TEXT_SIZE ( 82 | state: yuki.TranslatorConfigState, 83 | payload: { size: number } 84 | ) { 85 | state.gui.translationText = { 86 | ...state.gui.translationText, 87 | fontSize: payload.size 88 | } 89 | }, 90 | SET_TRANSLATION_TEXT_MARGIN ( 91 | state: yuki.TranslatorConfigState, 92 | payload: { margin: number } 93 | ) { 94 | state.gui.translationText = { 95 | ...state.gui.translationText, 96 | margin: payload.margin 97 | } 98 | }, 99 | SET_BACKGROUND_COLOR ( 100 | state: yuki.TranslatorConfigState, 101 | payload: { color: { hex8: string } } 102 | ) { 103 | state.gui.background = payload.color.hex8 104 | }, 105 | SET_MECAB_SHOW_ROMAJI ( 106 | state: yuki.TranslatorConfigState, 107 | payload: { value: boolean } 108 | ) { 109 | state.gui.mecab.showRomaji = payload.value 110 | }, 111 | SET_AUTO_HIDE_TITLEBAR ( 112 | state: yuki.TranslatorConfigState, 113 | payload: { value: boolean } 114 | ) { 115 | state.gui.autoHideTitlebar = payload.value 116 | }, 117 | SAVE_GUI_CONFIG (state: yuki.TranslatorConfigState) { 118 | if (!isSavingConfig) { 119 | setTimeout(() => { 120 | ipcRenderer.send(IpcTypes.REQUEST_SAVE_TRANSLATOR_GUI, { 121 | ...state.gui, 122 | bounds: remote.getCurrentWindow().getBounds(), 123 | alwaysOnTop: remote.getCurrentWindow().isAlwaysOnTop() 124 | }) 125 | isSavingConfig = false 126 | }, 1000) 127 | isSavingConfig = true 128 | } 129 | } 130 | } 131 | 132 | const actions = { 133 | setConfig ( 134 | { commit }: { commit: Commit }, 135 | { name, cfgs }: { name: string; cfgs: any } 136 | ) { 137 | commit('SET_CONFIG', { name, cfgs }) 138 | if (name === 'game') { 139 | commit('Hooks/INIT_DISPLAY_HOOK', { code: cfgs.code }, { root: true }) 140 | ipcRenderer.send(IpcTypes.REQUEST_INSERT_HOOK, cfgs.code) 141 | } 142 | } 143 | } 144 | 145 | export default { 146 | state: configState, 147 | getters, 148 | mutations, 149 | actions 150 | } 151 | -------------------------------------------------------------------------------- /src/translator/store/modules/View.ts: -------------------------------------------------------------------------------- 1 | import { Commit } from 'vuex' 2 | 3 | const viewState: yuki.TranslatorViewState = { 4 | isButtonsShown: true, 5 | isWindowTooHigh: false, 6 | pauseNewText: false, 7 | dict: {}, 8 | isGetDictResult: false 9 | } 10 | 11 | const mutations = { 12 | SET_BUTTONS_SHOWN ( 13 | state: yuki.TranslatorViewState, 14 | payload: { value: boolean } 15 | ) { 16 | state.isButtonsShown = payload.value 17 | }, 18 | SET_WINDOW_TOO_HIGH ( 19 | state: yuki.TranslatorViewState, 20 | payload: { value: boolean } 21 | ) { 22 | state.isWindowTooHigh = payload.value 23 | }, 24 | SET_PAUSE_NEW_TEXT ( 25 | state: yuki.TranslatorViewState, 26 | payload: { value: boolean } 27 | ) { 28 | state.pauseNewText = payload.value 29 | }, 30 | SET_DICT ( 31 | state: yuki.TranslatorViewState, 32 | payload: { value: yuki.DictResult } 33 | ) { 34 | state.dict = payload.value 35 | state.isGetDictResult = true 36 | }, 37 | CLEAR_DICT ( 38 | state: yuki.TranslatorViewState 39 | ) { 40 | state.isGetDictResult = false 41 | } 42 | } 43 | 44 | const actions = { 45 | setButtonsShown ({ commit }: { commit: Commit }, value: boolean) { 46 | commit('SET_BUTTONS_SHOWN', { value }) 47 | }, 48 | setWindowTooHigh ({ commit }: { commit: Commit }, value: boolean) { 49 | commit('SET_WINDOW_TOO_HIGH', { value }) 50 | }, 51 | setPauseNewText ({ commit }: { commit: Commit }, value: boolean) { 52 | commit('SET_PAUSE_NEW_TEXT', { value }) 53 | }, 54 | setDict ({ commit }: { commit: Commit }, value: yuki.DictResult) { 55 | commit('SET_DICT', { value }) 56 | }, 57 | clearDict ({ commit }: { commit: Commit }) { 58 | commit('CLEAR_DICT') 59 | } 60 | } 61 | 62 | export default { 63 | state: viewState, 64 | mutations, 65 | actions 66 | } 67 | -------------------------------------------------------------------------------- /src/translator/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = (require as any).context('.', false, /\.ts$/) 7 | const modules = {} 8 | 9 | files.keys().forEach((key: string) => { 10 | if (key === './index.ts') return 11 | modules[key.replace(/(\.\/|\.ts)/g, '')] = files(key).default 12 | modules[key.replace(/(\.\/|\.ts)/g, '')].namespaced = true 13 | }) 14 | 15 | export default modules 16 | -------------------------------------------------------------------------------- /src/types/common.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace yuki { 2 | export interface Middleware { 3 | process: (context: T, next: (newContext: T) => void) => void 4 | } 5 | 6 | export type MeCabPattern = { 7 | word: string; 8 | abbr: string; 9 | kana: string; 10 | } 11 | 12 | export type MeCabPatterns = Array 13 | 14 | export interface DictResult { 15 | found?: boolean 16 | word?: string 17 | content?: LingoesPattern 18 | } 19 | 20 | export interface DictOptions { 21 | dict: string 22 | word: string 23 | } 24 | 25 | export interface LingoesPattern { 26 | kana?: Array 27 | definitions?: Array<{ 28 | partOfSpeech: string, 29 | explanations: Array<{ 30 | content: string, 31 | example: { 32 | sentence: string, 33 | content: string 34 | } 35 | }> 36 | }> 37 | } 38 | 39 | export interface Process { 40 | name: string, 41 | pid: number 42 | } 43 | export type Processes = Process[] 44 | export interface ProcessWithText extends Process { 45 | text: string 46 | } 47 | export type ProcessesWithText = ProcessWithText[] 48 | } 49 | -------------------------------------------------------------------------------- /src/types/config.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace yuki { 2 | namespace Config { 3 | export interface Config {} 4 | 5 | export interface LocaleChangerItems { 6 | [id: string]: LocaleChangerItem 7 | } 8 | 9 | export interface LocaleChangerItem { 10 | name: string 11 | enable: boolean 12 | exec: string 13 | } 14 | 15 | export interface OnlineApiItem { 16 | name: string 17 | enable: boolean 18 | url?: string 19 | method?: string 20 | needSession?: boolean 21 | requestBodyFormat?: string 22 | requestHeaders?: string 23 | responseBodyPattern?: string 24 | external?: boolean 25 | jsFile?: string 26 | } 27 | 28 | export interface JBeijing { 29 | enable: boolean 30 | path: string 31 | dictPath?: string 32 | traditionalChinese?: boolean 33 | } 34 | 35 | export interface LibraryItem { 36 | enable: boolean 37 | path: string 38 | } 39 | 40 | export interface Default extends Libraries, Config { 41 | language: string 42 | localeChangers: LocaleChangerItems 43 | onlineApis: OnlineApiItem[] 44 | dictionaries: Dictionaries 45 | } 46 | 47 | export interface Libraries { 48 | librariesRepoUrl: string 49 | mecab: LibraryItem 50 | translators: { 51 | jBeijing: JBeijing 52 | } 53 | } 54 | 55 | export interface Dictionaries { 56 | lingoes: LibraryItem 57 | } 58 | 59 | export interface Texts extends Config { 60 | interceptor: { 61 | shouldBeIgnore: string[]; 62 | ignoreAsciiOnly: boolean; 63 | maxLength: number; 64 | } 65 | modifier: { 66 | removeAscii: boolean; 67 | deduplicate: boolean; 68 | delineBreak: boolean; 69 | } 70 | merger: { 71 | enable: boolean; 72 | timeout: number; 73 | } 74 | } 75 | 76 | export interface Gui extends Config { 77 | mainWindow: { 78 | bounds: Electron.Rectangle; 79 | } 80 | translatorWindow: { 81 | bounds: Electron.Rectangle; 82 | alwaysOnTop: boolean; 83 | originalText: FontStyle; 84 | translationText: TranslationTextStyle; 85 | background: string; 86 | renderMode: 'transparent' | 'translucent'; 87 | mecab: { 88 | showRomaji: boolean; 89 | } 90 | autoHideTitlebar: boolean; 91 | } 92 | } 93 | 94 | export interface Games extends Array, Config {} 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/types/ext.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace RequestProgress { 2 | export interface ProgressState { 3 | // Overall percent (between 0 to 1) 4 | percent: number, 5 | // The download speed in bytes/sec 6 | speed: number, 7 | size: { 8 | // The total payload size in bytes 9 | total: number, 10 | // The transferred payload size in bytes 11 | transferred: number 12 | }, 13 | time: { 14 | // The total elapsed seconds since the start (3 decimals) 15 | elapsed: string, 16 | // The remaining seconds to finish (3 decimals) 17 | remaining: string 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | interface Global { 3 | __static: string 4 | __baseDir: string 5 | __appDir: string 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/muse-ui.d.ts: -------------------------------------------------------------------------------- 1 | import { CreateElement, PluginFunction, VNode } from 'vue' 2 | 3 | export type MessageModel = 'alert' | 'prompt' | 'confirm' 4 | export type MessageType = '' | 'success' | 'info' | 'warning' | 'error' 5 | type CreateElementFunc = (h: CreateElement) => VNode 6 | type BeforeCloseFunction = ( 7 | result: boolean, 8 | modal: any, 9 | close: () => void 10 | ) => any 11 | interface ValidatorResult { 12 | valid: boolean 13 | message: string 14 | } 15 | interface MessageReturn { 16 | result: boolean 17 | value?: string | number 18 | } 19 | type Validator = (value: string | number) => ValidatorResult 20 | export interface MessageOptions { 21 | successIcon?: string 22 | infoIcon?: string 23 | warningIcon?: string 24 | errorIcon?: string 25 | title?: string 26 | icon?: string 27 | iconSize?: number 28 | mode?: MessageModel 29 | type?: MessageType 30 | content?: string | CreateElementFunc 31 | width?: number | string 32 | maxWidth?: number | string 33 | className?: string 34 | transition?: string 35 | beforeClose?: BeforeCloseFunction 36 | okLabel?: string 37 | cancelLabel?: string 38 | inputType?: string 39 | inputPlaceholder?: string 40 | inputValue?: string | number 41 | validator?: Validator 42 | } 43 | 44 | declare module 'vue/types/vue' { 45 | export interface Vue { 46 | $message (options: MessageOptions): Promise 47 | $alert (content: string, options: MessageOptions): Promise 48 | $alert ( 49 | content: string, 50 | title: string, 51 | options: MessageOptions 52 | ): Promise 53 | $confirm (content: string, options: MessageOptions): Promise 54 | $confirm ( 55 | content: string, 56 | title: string, 57 | options: MessageOptions 58 | ): Promise 59 | $prompt (content: string, options: MessageOptions): Promise 60 | $prompt ( 61 | content: string, 62 | title: string, 63 | options: MessageOptions 64 | ): Promise 65 | } 66 | } 67 | 68 | export type ToastPosition = 69 | | 'top' 70 | | 'top-start' 71 | | 'top-end' 72 | | 'bottom' 73 | | 'bottom-start' 74 | | 'bottom-end' 75 | export interface ToastAction { 76 | action: string | VNode 77 | click: (id: string) => any 78 | } 79 | export interface ToastOptions { 80 | message?: string 81 | time?: number 82 | position?: ToastPosition 83 | close?: boolean 84 | icon?: string 85 | actions?: ToastAction[] 86 | color?: string 87 | textColor?: string 88 | closeIcon?: string 89 | successIcon?: string 90 | infoIcon?: string 91 | warningIcon?: string 92 | errorIcon?: string 93 | } 94 | 95 | export interface Toast { 96 | install: PluginFunction 97 | config (options: ToastOptions): ToastOptions 98 | message (options: ToastOptions): string 99 | success (message: string): string 100 | success (options: ToastOptions): string 101 | info (message: string): string 102 | info (options: ToastOptions): string 103 | warning (message: string): string 104 | warning (options: ToastOptions): string 105 | error (message: string): string 106 | error (options: ToastOptions): string 107 | close (id: string): void 108 | } 109 | 110 | export default Toast 111 | 112 | declare module 'vue/types/vue' { 113 | interface Vue { 114 | $toast: Toast 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/types/store.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace yuki { 2 | export interface SettingsState { 3 | localeChangers: TempLocaleChangerItem[] 4 | } 5 | 6 | type TempLocaleChangerItem = yuki.Config.LocaleChangerItem & { id: string } 7 | 8 | export interface TextOutputObject extends TextThread { 9 | text: string 10 | } 11 | 12 | export interface TextThread { 13 | handle: number 14 | pid: number 15 | addr: number 16 | ctx: number 17 | ctx2: number 18 | name: string 19 | code: string 20 | } 21 | 22 | export interface TranslationMessage { 23 | id: number 24 | translation: Translations['translations'] 25 | } 26 | 27 | export interface Game { 28 | name: string 29 | path: string 30 | code: string 31 | localeChanger: string 32 | } 33 | export interface ConfigState { 34 | default: yuki.Config.Default 35 | games: Game[] 36 | librariesBaseStorePath: string 37 | } 38 | 39 | export interface GuiState { 40 | noGame: boolean 41 | debugMessages: string[], 42 | isGameStartingEnded: boolean, 43 | processes: Processes 44 | } 45 | 46 | export interface TranslatorHookState { 47 | isMecabEnable: boolean 48 | hookInfos: TextThread[] 49 | texts: { 50 | [handle: string]: string[]; 51 | } 52 | patterns: { 53 | [handle: string]: yuki.MeCabPatterns[]; 54 | } 55 | currentDisplayHookIndex: number 56 | translations: { 57 | [handle: string]: Array; 58 | } 59 | toDisplayHookCode: string 60 | } 61 | 62 | export interface TranslatorConfigState { 63 | default: yuki.Config.Default 64 | game: Game 65 | gui: { 66 | originalText: FontStyle; 67 | translationText: TranslationTextStyle; 68 | background: string; 69 | mecab: { 70 | showRomaji: boolean; 71 | } 72 | autoHideTitlebar: boolean; 73 | } 74 | } 75 | 76 | export interface FontStyle { 77 | fontSize: number 78 | color: string 79 | } 80 | 81 | export interface TranslationTextStyle extends FontStyle { 82 | margin: number 83 | } 84 | 85 | export interface TranslatorViewState { 86 | isButtonsShown: boolean 87 | isWindowTooHigh: boolean 88 | pauseNewText: boolean 89 | dict: DictResult 90 | isGetDictResult: boolean 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/types/translation.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace yuki { 2 | export interface Translations { 3 | original: string 4 | translations: { 5 | [apiName: string]: string; 6 | } 7 | } 8 | 9 | export interface Translator { 10 | translate (text: string, callback: (translation: string) => void): void 11 | isEnable (): boolean 12 | setEnable (isEnable: boolean): void 13 | getName (): string 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/types/vue-component.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "__static": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/e2e/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Set BABEL_ENV to use proper env config 4 | process.env.BABEL_ENV = "test"; 5 | 6 | // Enable use of ES6+ on required files 7 | require("babel-register")({ 8 | ignore: /node_modules/ 9 | }); 10 | 11 | // Attach Chai APIs to global scope 12 | const { expect, should, assert } = require("chai"); 13 | global.expect = expect; 14 | global.should = should; 15 | global.assert = assert; 16 | 17 | // Require all JS files in `./specs` for Mocha to consume 18 | require("require-dir")("./specs"); 19 | -------------------------------------------------------------------------------- /test/e2e/specs/Launch.spec.js: -------------------------------------------------------------------------------- 1 | import utils from "../utils"; 2 | 3 | describe("Launch", function() { 4 | beforeEach(utils.beforeEach); 5 | afterEach(utils.afterEach); 6 | 7 | it("shows the proper application title", function() { 8 | return this.app.client.getTitle().then(title => { 9 | expect(title).to.equal("electron-vue-test"); 10 | }); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /test/e2e/utils.js: -------------------------------------------------------------------------------- 1 | import electron from "electron"; 2 | import { Application } from "spectron"; 3 | 4 | export default { 5 | afterEach() { 6 | this.timeout(10000); 7 | 8 | if (this.app && this.app.isRunning()) { 9 | return this.app.stop(); 10 | } 11 | }, 12 | beforeEach() { 13 | this.timeout(10000); 14 | this.app = new Application({ 15 | path: electron, 16 | args: ["dist/electron/main.js"], 17 | startTimeout: 10000, 18 | waitTimeout: 10000 19 | }); 20 | 21 | return this.app.start(); 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /test/unit/karma.main.conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const merge = require("webpack-merge"); 5 | const webpack = require("webpack"); 6 | 7 | const baseConfig = require("../../.electron-vue/webpack.main.config"); 8 | const projectRoot = path.resolve(__dirname, "../../src/main"); 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = "test"; 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: "#inline-source-map", 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | "process.env.NODE_ENV": '"testing"' 18 | }) 19 | ] 20 | }); 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry; 24 | // delete webpackConfig.externals; 25 | // delete webpackConfig.output.libraryTarget; 26 | 27 | module.exports = config => { 28 | config.set({ 29 | browsers: ["visibleElectron"], 30 | client: { 31 | useIframe: false 32 | }, 33 | coverageIstanbulReporter: { 34 | dir: path.join(__dirname, "./coverage"), 35 | fixWebpackSourcePaths: true, 36 | reporters: ["text-summary", "html", "lcovonly"] 37 | }, 38 | customLaunchers: { 39 | visibleElectron: { 40 | base: "Electron", 41 | flags: ["--show"] 42 | } 43 | }, 44 | frameworks: ["mocha", "chai"], 45 | files: ["./main.js"], 46 | preprocessors: { 47 | "./main.js": ["webpack", "sourcemap"] 48 | }, 49 | reporters: ["spec", "coverage-istanbul"], 50 | singleRun: true, 51 | webpack: webpackConfig, 52 | webpackMiddleware: { 53 | noInfo: true 54 | } 55 | }); 56 | }; 57 | -------------------------------------------------------------------------------- /test/unit/karma.renderer.conf.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | const merge = require("webpack-merge"); 5 | const webpack = require("webpack"); 6 | 7 | const baseConfig = require("../../.electron-vue/webpack.renderer.config"); 8 | const projectRoot = path.resolve(__dirname, "../../src/renderer"); 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = "test"; 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: "#inline-source-map", 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | "process.env.NODE_ENV": '"testing"' 18 | }) 19 | ] 20 | }); 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry; 24 | delete webpackConfig.externals; 25 | delete webpackConfig.output.libraryTarget; 26 | 27 | module.exports = config => { 28 | config.set({ 29 | browsers: ["visibleElectron"], 30 | client: { 31 | useIframe: false 32 | }, 33 | coverageIstanbulReporter: { 34 | dir: path.join(__dirname, "./coverage"), 35 | reporters: ['text-summary', 'html', 'lcovonly'] 36 | }, 37 | customLaunchers: { 38 | visibleElectron: { 39 | base: "Electron", 40 | flags: ["--show"] 41 | } 42 | }, 43 | frameworks: ["mocha", "chai"], 44 | files: ["./renderer.js"], 45 | preprocessors: { 46 | "./renderer.js": ["webpack", "sourcemap"] 47 | }, 48 | reporters: ["spec", "coverage-istanbul"], 49 | singleRun: true, 50 | webpack: webpackConfig, 51 | webpackMiddleware: { 52 | noInfo: true 53 | } 54 | }); 55 | }; 56 | -------------------------------------------------------------------------------- /test/unit/main.js: -------------------------------------------------------------------------------- 1 | require("module").globalPaths.push( 2 | require("path").resolve(__dirname, "../../node_modules") 3 | ); 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context("./specs/main", true, /\.spec$/); 7 | testsContext.keys().forEach(testsContext); 8 | -------------------------------------------------------------------------------- /test/unit/renderer.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | Vue.config.devtools = false; 3 | Vue.config.productionTip = false; 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context("./specs/renderer", true, /\.spec$/); 7 | testsContext.keys().forEach(testsContext); 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context( 13 | "../../src/renderer", 14 | true, 15 | /^\.\/(?!main(\.js)?$)/ 16 | ); 17 | srcContext.keys().forEach(srcContext); 18 | -------------------------------------------------------------------------------- /test/unit/specs/main/Api.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import Api from '../../../../src/main/translate/Api' 3 | import TranslationManager from '../../../../src/main/translate/TranslationManager' 4 | 5 | describe('Api', () => { 6 | it('gets translation from form and parses with regex', (done) => { 7 | const googleCN = new Api({ 8 | name: 'googleCN', 9 | url: 'https://translate.google.cn/m', 10 | method: 'POST', 11 | requestBodyFormat: 'X{"q": %TEXT%, "sl": "ja", "hl": "zh-CN"}', 12 | responseBodyPattern: 'Rclass="t0">([^<]*)<' 13 | }) 14 | 15 | return googleCN.translate( 16 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 17 | (translation) => { 18 | try { 19 | expect(translation).to.equal('如果捕获了尤马坤,则为210日元。我知道了 ...') 20 | } catch (e) { 21 | return done(e) 22 | } 23 | done() 24 | } 25 | ) 26 | }).timeout(5000) 27 | 28 | it('combines multiple translations into yuki.Translations object', (done) => { 29 | const apis = [ 30 | { 31 | name: 'googleCN', 32 | url: 'https://translate.google.cn/m', 33 | method: 'POST', 34 | requestBodyFormat: 'X{"q": %TEXT%, "sl": "ja", "hl": "zh-CN"}', 35 | responseBodyPattern: 'Rclass="t0">([^<]*)<', 36 | enable: true 37 | }, 38 | { 39 | enable: true, 40 | method: 'POST', 41 | name: 'caiyun', 42 | requestBodyFormat: 43 | 'J{"source": %TEXT%, "trans_type": "ja2zh", "request_id": "demo", "detect": "true"}', 44 | requestHeaders: '{"X-Authorization": "token 3975l6lr5pcbvidl6jl2"}', 45 | responseBodyPattern: 'J%RESPONSE%.target', 46 | url: 'https://api.interpreter.caiyunai.com/v1/translator' 47 | } 48 | ] 49 | 50 | TranslationManager.getInstance() 51 | .initializeApis(apis) 52 | .translateAll( 53 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 54 | (translations) => { 55 | // tslint:disable-next-line: no-console 56 | console.log(translations) 57 | try { 58 | expect(translations.original).to.equal('悠真くんを攻略すれば210円か。なるほどなぁ…') 59 | expect(translations.translations.googleCN).to.equal('如果捕获了尤马坤,则为210日元。我知道了 ...') 60 | expect(translations.translations.caiyun).to.be.oneOf([ 61 | '攻下悠真的话是210日元吗。 原来如此', 62 | "ERR: TypeError: Cannot read property 'target' of undefined" 63 | ]) 64 | } catch (e) { 65 | return done(e) 66 | } 67 | done() 68 | } 69 | ) 70 | }).timeout(5000) 71 | 72 | it('returns no translation if there is no enabled api', (done) => { 73 | const apis = [ 74 | { 75 | name: 'baidu', 76 | url: 'https://fanyi.baidu.com/transapi', 77 | method: 'POST', 78 | requestBodyFormat: 'X{"query": %TEXT%, "from": "jp", "to": "zh"}', 79 | responseBodyPattern: 'J%RESPONSE%.data[0].dst', 80 | enable: false 81 | }, 82 | { 83 | name: 'googleCN', 84 | url: 'https://translate.google.cn/m', 85 | method: 'POST', 86 | requestBodyFormat: 'X{"q": %TEXT%, "sl": "ja", "hl": "zh-CN"}', 87 | responseBodyPattern: 'Rclass="t0">([^<]*)<', 88 | enable: false 89 | } 90 | ] 91 | 92 | TranslationManager.getInstance() 93 | .initializeApis(apis) 94 | .translateAll( 95 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 96 | (translations) => { 97 | try { 98 | // tslint:disable-next-line: no-console 99 | console.log(translations) 100 | expect(translations).to.deep.equal({ 101 | original: '悠真くんを攻略すれば210円か。なるほどなぁ…', 102 | translations: {} 103 | }) 104 | } catch (e) { 105 | return done(e) 106 | } 107 | done() 108 | } 109 | ) 110 | }) 111 | 112 | it('returns translations for any enabled apis', (done) => { 113 | const apis = [ 114 | { 115 | name: 'googleCN', 116 | url: 'https://translate.google.cn/m', 117 | method: 'POST', 118 | requestBodyFormat: 'X{"q": %TEXT%, "sl": "ja", "hl": "zh-CN"}', 119 | responseBodyPattern: 'Rclass="t0">([^<]*)<', 120 | enable: true 121 | }, 122 | { 123 | enable: false, 124 | method: 'POST', 125 | name: 'caiyun', 126 | requestBodyFormat: 'J{"source": %TEXT%, "trans_type": "ja2zh", ' + 127 | '"request_id": "web_fanyi", "os_type": "web", ' + 128 | '"dict": "false", "cached": "false", "replaced": "false"}', 129 | responseBodyPattern: 'J%RESPONSE%.target', 130 | url: 'https://api.interpreter.caiyunai.com/v1/translator' 131 | } 132 | ] 133 | 134 | TranslationManager.getInstance() 135 | .initializeApis(apis) 136 | .translateAll( 137 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 138 | (translations) => { 139 | try { 140 | expect(translations).to.deep.equal({ 141 | original: '悠真くんを攻略すれば210円か。なるほどなぁ…', 142 | translations: { 143 | googleCN: '如果捕获了尤马坤,则为210日元。我知道了 ...' 144 | } 145 | }) 146 | } catch (e) { 147 | return done(e) 148 | } 149 | done() 150 | } 151 | ) 152 | }).timeout(5000) 153 | }) 154 | -------------------------------------------------------------------------------- /test/unit/specs/main/ApplicationBuilder.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-reference 2 | /// 3 | 4 | import { expect } from 'chai' 5 | import ApplicationBuilder from '../../../../src/common/ApplicationBuilder' 6 | 7 | class SubstringMiddleware implements yuki.Middleware { 8 | public process (context: string, next: (newContext: string) => void) { 9 | if (context.length < 1) next(context) 10 | else next(context.substring(0, context.length - 1)) 11 | } 12 | } 13 | 14 | // tslint:disable-next-line: max-classes-per-file 15 | export class ExpectMiddleware implements yuki.Middleware { 16 | private expectValue: string 17 | private done: () => void 18 | 19 | constructor (expectValue: string, done: () => void) { 20 | this.expectValue = expectValue 21 | this.done = done 22 | } 23 | 24 | public process (context: string, next: (newContext: string) => void) { 25 | expect(context).to.deep.equal(this.expectValue) 26 | this.done() 27 | } 28 | } 29 | 30 | describe('ApplicationBuilder', () => { 31 | it('runs one middleware correctly', (done) => { 32 | const applicationBuilder = new ApplicationBuilder() 33 | 34 | applicationBuilder.use(new SubstringMiddleware()) 35 | applicationBuilder.use( 36 | new ExpectMiddleware('ボクに選択の余地は無かった', done) 37 | ) 38 | applicationBuilder.run('ボクに選択の余地は無かった。') 39 | }) 40 | 41 | it('runs multiple middlewares correctly', (done) => { 42 | const applicationBuilder = new ApplicationBuilder() 43 | 44 | applicationBuilder.use(new SubstringMiddleware()) 45 | applicationBuilder.use(new SubstringMiddleware()) 46 | applicationBuilder.use(new SubstringMiddleware()) 47 | applicationBuilder.use(new ExpectMiddleware('A', done)) 48 | applicationBuilder.run('ABCD') 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /test/unit/specs/main/Config.spec.ts: -------------------------------------------------------------------------------- 1 | const ConfigInjector = require('inject-loader!../../../../src/main/config/Config') 2 | import { expect } from 'chai' 3 | 4 | describe('Config', () => { 5 | let fileWritten 6 | 7 | beforeEach(() => { 8 | fileWritten = false 9 | }) 10 | 11 | const expected = { 12 | test: { 13 | id: 1, 14 | content: '『我跟喜欢成人游戏一样喜欢你』-「高坂桐乃」' 15 | } 16 | } 17 | 18 | const expectedModified = { 19 | test: { 20 | id: 1, 21 | content: 22 | '『如果分手的恋人还能做朋友,要不从没爱过,要不还在爱着。』-「九ちのセカィ」' 23 | }, 24 | added: true 25 | } 26 | 27 | it('loads if file exists', () => { 28 | const Config = makeLoadTestingConfig() 29 | Config.prototype.getFilename = () => 'valid/path/name' 30 | // tslint:disable-next-line: no-empty 31 | Config.prototype.getDefaultObject = () => {} 32 | 33 | const testConfig = new Config().init() 34 | 35 | // tslint:disable-next-line: no-console 36 | console.log(testConfig.get()) 37 | expect(testConfig.get()).to.deep.equal(expected) 38 | }) 39 | 40 | const makeLoadTestingConfig = () => 41 | ConfigInjector({ 42 | fs: { 43 | existsSync: () => true, 44 | watch: () => '' 45 | }, 46 | jsonfile: { 47 | readFileSync: () => expected 48 | }, 49 | path: { 50 | resolve: () => 'valid/path/name' 51 | } 52 | }).default 53 | 54 | it('saves default if file not exist', () => { 55 | const Config = makeSaveDefaultTestingConfig() 56 | Config.prototype.getFilename = () => 'invalid/path/name' 57 | Config.prototype.getDefaultObject = () => expected 58 | 59 | const testConfig = new Config().init() 60 | 61 | expect(fileWritten).to.equal(true) 62 | expect(testConfig.get()).to.deep.equal(expected) 63 | }) 64 | 65 | const makeSaveDefaultTestingConfig = () => 66 | ConfigInjector({ 67 | jsonfile: { 68 | writeFileSync: () => { 69 | fileWritten = true 70 | } 71 | }, 72 | fs: { 73 | watch: () => '' 74 | }, 75 | path: { 76 | resolve: () => 'invalid/path/name' 77 | } 78 | }).default 79 | 80 | it('saves after calling save()', () => { 81 | const Config = makeSaveTestingConfig() 82 | Config.prototype.getFilename = () => 'valid/path/name' 83 | // tslint:disable-next-line: no-empty 84 | Config.prototype.getDefaultObject = () => {} 85 | 86 | const testConfig = new Config().init() 87 | 88 | testConfig.get().test.content = 89 | '『如果分手的恋人还能做朋友,要不从没爱过,要不还在爱着。』-「九ちのセカィ」' 90 | testConfig.get().added = true 91 | testConfig.save() 92 | 93 | expect(fileWritten).to.equal(true) 94 | }) 95 | 96 | it('sets & saves after calling set()', () => { 97 | const Config = makeSaveTestingConfig() 98 | Config.prototype.getFilename = () => 'valid/path/name' 99 | // tslint:disable-next-line: no-empty 100 | Config.prototype.getDefaultObject = () => {} 101 | 102 | const testConfig = new Config().init() 103 | 104 | testConfig.set(expectedModified) 105 | }) 106 | 107 | const makeSaveTestingConfig = () => 108 | ConfigInjector({ 109 | fs: { 110 | existsSync: () => true, 111 | watch: () => '' 112 | }, 113 | jsonfile: { 114 | readFileSync: () => expected, 115 | writeFileSync: (filePath, obj) => { 116 | fileWritten = true 117 | expect(obj).to.deep.equal(expectedModified) 118 | } 119 | }, 120 | path: { 121 | resolve: () => 'valid/path/name' 122 | } 123 | }).default 124 | }) 125 | -------------------------------------------------------------------------------- /test/unit/specs/main/Downloader.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import { existsSync, unlinkSync } from 'fs' 3 | import { join } from 'path' 4 | import Downloader from '../../../../src/main/Downloader' 5 | 6 | describe('Downloader', () => { 7 | it('downloads file into specific path', (done) => { 8 | const filename = 'favicon.ico' 9 | const filePath = join(__dirname, '../../temp', filename) 10 | new Downloader( 11 | `https://www.bilibili.com/${filename}`, 12 | filePath 13 | ) 14 | .onError((err) => { 15 | done(err) 16 | }) 17 | .onEnd(() => { 18 | expect(existsSync(filePath)).to.equal(true) 19 | unlinkSync(filePath) 20 | done() 21 | }) 22 | .start() 23 | }).timeout(5000) 24 | 25 | it('deletes local file when explicitly call abort()', (done) => { 26 | const filename = 'favicon.ico' 27 | const filePath = join(__dirname, '../../temp', filename) 28 | const downloader = new Downloader( 29 | `https://www.bilibili.com/${filename}`, 30 | filePath 31 | ) 32 | .onError((err) => { 33 | expect(err.message).to.equal('download aborted') 34 | expect(existsSync(filePath)).to.equal(false) 35 | done() 36 | }) 37 | .start() 38 | 39 | setTimeout(() => { 40 | downloader.abort() 41 | }, 1) 42 | }).timeout(5000) 43 | }) 44 | -------------------------------------------------------------------------------- /test/unit/specs/main/ExternalApi.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai' 2 | import ExternalApi from '../../../../src/main/translate/ExternalApi' 3 | 4 | describe('ExternalApi', () => { 5 | before(() => { 6 | (global as any).__baseDir = __dirname 7 | }) 8 | 9 | it('gets translation from external JS file', (done) => { 10 | const qq = new ExternalApi({ 11 | enable: true, 12 | external: true, 13 | jsFile: '..\\..\\temp\\qqApi.js', 14 | name: 'qq' 15 | }) 16 | 17 | qq.translate( 18 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 19 | (translation) => { 20 | try { 21 | expect(translation).to.equal('攻略悠真的话是210日元啊。原来如此啊……') 22 | } catch (e) { 23 | return done(e) 24 | } 25 | done() 26 | } 27 | ) 28 | }).timeout(5000) 29 | 30 | it('keeps session when request multiple times', (done) => { 31 | const qq = new ExternalApi({ 32 | enable: true, 33 | external: true, 34 | jsFile: '..\\..\\temp\\qqApi.js', 35 | name: 'qq' 36 | }) 37 | 38 | qq.translate( 39 | '悠真くんを攻略すれば210円か。なるほどなぁ…', 40 | (translation1) => { 41 | try { 42 | expect(translation1).to.equal('攻略悠真的话是210日元啊。原来如此啊……') 43 | qq.translate( 44 | 'はいっ、今日は柚子の入浴剤が入ってました。お湯も少し白くて温泉みたいでしたよ?', 45 | (translation2) => { 46 | try { 47 | expect(translation2).to.equal( 48 | '是的,今天放了柚子的沐浴剂。热水也有点白,就像温泉一样呢?' 49 | ) 50 | done() 51 | } catch (e) { 52 | return done(e) 53 | } 54 | } 55 | ) 56 | } catch (e) { 57 | return done(e) 58 | } 59 | } 60 | ) 61 | }).timeout(5000) 62 | }) 63 | -------------------------------------------------------------------------------- /test/unit/specs/main/LingoesDict.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-reference 2 | /// 3 | 4 | import { expect } from 'chai' 5 | import * as path from 'path' 6 | import * as util from 'util' 7 | import LingoesDict from '../../../../src/main/translate/LingoesDict' 8 | 9 | describe('LingoesDict', () => { 10 | it('returns correct pattern if found', (done) => { 11 | const lingoes = new LingoesDict({ 12 | enable: true, 13 | path: path.resolve(__dirname, '../../../../../libraries/dict/lingoes/njcd.db') 14 | }) 15 | 16 | lingoes.find('1', (result) => { 17 | try { 18 | expect(result).to.deep.equal(JSON.parse('{"found":true,"word":"1","content":{"kana":["' + 19 | 'イチ·イツ","ひと·ひとつ·ひ"],"definitions":[{"partOfSpeech":"(H10","explanation' + 20 | 's":[{"content":"一,第一。","example":{"sentence":"一月(いちがつ)·一番(いちばん)·一' + 21 | '姫二太郎(いちひめにたろう)。","content":""}},{"content":"一个。","example":{"sente' + 22 | 'nce":"一々(いちいち)·一因(いちいん)·一個(いっこ)·一民間会社(いちみんかんがいしゃ)。","con' + 23 | 'tent":""}},{"content":"一次。","example":{"sentence":"一敗(いっぱい)·一面識(いちめ' + 24 | 'んしき)。","content":""}},{"content":"最优秀,最好,第一。","example":{"sentence":"' + 25 | '一流(いちりゅう)·一位(いちい)。","content":""}},{"content":"表示全体,都。","example"' + 26 | ':{"sentence":"一切(いっさい)·一族(いちぞく)·一座(いちざ)。","content":""}},{"content"' + 27 | ':"某,另外的,别的。","example":{"sentence":"一夜(いちや)·一説(いっせつ)·一書(いっしょ)。' + 28 | '","content":""}},{"content":"普通的。","example":{"sentence":"一介(いっかい)·一教' + 29 | '師(いちきょうし)·一読者(いちどくしゃ)。","content":""}},{"content":"相同,同样(合为一体)' + 30 | '。","example":{"sentence":"一概(いちがい)·一咽(いちよう)·同一(どういつ)·画一(かくいつ)。' + 31 | '","content":""}},{"content":"专一,一心。","example":{"sentence":"一念(いちねん)·' + 32 | '一途(いちず)·純一(じゅんいつ)·一意專心(いちいせんしん)。","content":""}},{"content":")仅,' + 33 | '只,一点儿。","example":{"sentence":"一寸(ちょっと·いっすん)·一瞬(いっしゅん)·一言(いちご' + 34 | 'ん)。","content":""}}]},{"partOfSpeech":"(H11","explanations":[{"content":")或。"' + 35 | ',"example":{"sentence":"一喜一憂(いっきいちゆう)·一得一失(いっとくいっしつ)。","content":' + 36 | '""}}]},{"partOfSpeech":"(H12","explanations":[{"content":")用于强调或调整语气(带有不' + 37 | '容忽视之意)。","example":{"sentence":"一見識(いっけんしき·いちけんしき)·一考察(いちこうさ' + 38 | 'つ)。","content":""}}]}]}}')) 39 | lingoes.close() 40 | done() 41 | } catch (e) { 42 | lingoes.close() 43 | return done(e) 44 | } 45 | }) 46 | }) 47 | 48 | it('returns correct pattern if found - 2', (done) => { 49 | const lingoes = new LingoesDict({ 50 | enable: true, 51 | path: path.resolve(__dirname, '../../../../../libraries/dict/lingoes/njcd.db') 52 | }) 53 | 54 | lingoes.find('ゲーム', (result) => { 55 | try { 56 | expect(result).to.deep.equal({ 57 | found: true, 58 | word: 'ゲーム', 59 | content: { 60 | definitions: [ 61 | { 62 | partOfSpeech: undefined, 63 | explanations: [ 64 | { 65 | content: 'game游戏。比赛。', 66 | example: { sentence: '', content: '' } 67 | } 68 | ] 69 | } 70 | ] 71 | } 72 | }) 73 | lingoes.close() 74 | done() 75 | } catch (e) { 76 | lingoes.close() 77 | return done(e) 78 | } 79 | }) 80 | }) 81 | 82 | it('returns notfound if so', (done) => { 83 | const lingoes = new LingoesDict({ 84 | enable: true, 85 | path: path.resolve(__dirname, '../../../../../libraries/dict/lingoes/njcd.db') 86 | }) 87 | 88 | lingoes.find('キミ', (result) => { 89 | try { 90 | expect(result).to.deep.equal({ 91 | found: false 92 | }) 93 | lingoes.close() 94 | done() 95 | } catch (e) { 96 | lingoes.close() 97 | return done(e) 98 | } 99 | }) 100 | }) 101 | }) 102 | -------------------------------------------------------------------------------- /test/unit/specs/main/Mecab.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-reference 2 | /// 3 | 4 | import { expect } from 'chai' 5 | import * as path from 'path' 6 | import MecabMiddleware from '../../../../src/main/middlewares/MeCabMiddleware' 7 | 8 | describe('MeCab', () => { 9 | it('returns correct patterns', (done) => { 10 | const mecabMiddleware = new MecabMiddleware({ 11 | enable: true, 12 | path: path.resolve(__dirname, '../../../../../libraries/pos/mecab-ipadic') 13 | }) 14 | 15 | mecabMiddleware.process( 16 | { text: 'ボクに選択の余地は無かった。' }, 17 | (newContext) => { 18 | try { 19 | expect(newContext.text).to.deep.equal( 20 | '$ボク,n,ぼく|に,p,|選択,n,せんたく|の,p,|余地,n,よち|は,p,|無かっ,adj,なかっ|た,aux,|。,w,' 21 | ) 22 | } catch (e) { 23 | return done(e) 24 | } 25 | done() 26 | } 27 | ) 28 | }) 29 | 30 | it('converts mecab string to object', () => { 31 | expect( 32 | MecabMiddleware.stringToObject( 33 | '$ボク,n,ぼく|に,p,|選択,n,せんたく|の,p,|余地,n,よち|は,p,|無かっ,adj,なかっ|た,aux,|。,w,' 34 | ) 35 | ).to.deep.equal([ 36 | { word: 'ボク', abbr: 'n', kana: 'ぼく' }, 37 | { word: 'に', abbr: 'p', kana: '' }, 38 | { word: '選択', abbr: 'n', kana: 'せんたく' }, 39 | { word: 'の', abbr: 'p', kana: '' }, 40 | { word: '余地', abbr: 'n', kana: 'よち' }, 41 | { word: 'は', abbr: 'p', kana: '' }, 42 | { word: '無かっ', abbr: 'adj', kana: 'なかっ' }, 43 | { word: 'た', abbr: 'aux', kana: '' }, 44 | { word: '。', abbr: 'w', kana: '' } 45 | ]) 46 | 47 | expect( 48 | MecabMiddleware.stringToObject('ボクに選択の余地は無かった。') 49 | ).to.deep.equal([]) 50 | }) 51 | 52 | it('merges letter(w) patterns', (done) => { 53 | const mecabMiddleware = new MecabMiddleware({ 54 | enable: true, 55 | path: path.resolve(__dirname, '../../../../../libraries/pos/mecab-ipadic') 56 | }) 57 | 58 | mecabMiddleware.process( 59 | { text: 'ボクに選択の余地、無かった。。。!' }, 60 | (newContext) => { 61 | try { 62 | expect(newContext.text).to.deep.equal( 63 | '$ボク,n,ぼく|に,p,|選択,n,せんたく|の,p,|余地,n,よち|、,w,|無かっ,adj,なかっ|た,aux,|。。。!,w,' 64 | ) 65 | } catch (e) { 66 | return done(e) 67 | } 68 | done() 69 | } 70 | ) 71 | }) 72 | }) 73 | -------------------------------------------------------------------------------- /test/unit/specs/main/Processes.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-reference 2 | /// 3 | 4 | import { expect } from 'chai' 5 | import * as _ from 'lodash' 6 | import Processes from '../../../../src/main/Processes' 7 | 8 | describe('Processes', () => { 9 | it('returns processes that include explorer.exe', (done) => { 10 | Processes.get().then((result) => { 11 | try { 12 | // tslint:disable-next-line: no-unused-expression 13 | expect(_.some(result, { name: 'explorer.exe' })).to.be.true 14 | } catch (e) { 15 | return done(e) 16 | } 17 | done() 18 | }).catch(() => { 19 | done(new Error('rejected')) 20 | }) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /test/unit/specs/main/Texts.spec.ts: -------------------------------------------------------------------------------- 1 | // tslint:disable-next-line: no-reference 2 | /// 3 | 4 | import { expect } from 'chai' 5 | import TextModifierMiddleware from '../../../../src/main/middlewares/TextModifierMiddleware' 6 | 7 | describe('Texts', () => { 8 | it('removes ascii characters when .removeAscii = true', (done) => { 9 | const textModifierMiddleware = new TextModifierMiddleware({ 10 | removeAscii: true 11 | }) 12 | 13 | textModifierMiddleware.process( 14 | { text: 'ボクにaa選択のbb余地はcc無かった。' }, 15 | (newContext) => { 16 | try { 17 | expect(newContext.text).to.deep.equal('ボクに選択の余地は無かった。') 18 | } catch (e) { 19 | return done(e) 20 | } 21 | done() 22 | } 23 | ) 24 | }) 25 | 26 | it('removes duplicate characters when .deduplicate = true', (done) => { 27 | const textModifierMiddleware = new TextModifierMiddleware({ 28 | deduplicate: true 29 | }) 30 | 31 | textModifierMiddleware.process( 32 | { text: 'ボクに選択選択の余地はは無かった無かった。' }, 33 | (newContext) => { 34 | try { 35 | expect(newContext.text).to.deep.equal('ボクに選択の余地は無かった。') 36 | } catch (e) { 37 | return done(e) 38 | } 39 | done() 40 | } 41 | ) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /test/unit/specs/main/Win32.spec.ts: -------------------------------------------------------------------------------- 1 | const Win32Injector = require('inject-loader!../../../../src/main/Win32') 2 | import { expect } from 'chai' 3 | 4 | describe('Win32', () => { 5 | let callbackCalled 6 | 7 | beforeEach(() => { 8 | callbackCalled = false 9 | }) 10 | 11 | it('callbacks when registered process exited', () => { 12 | const registerProcessExitCallback = makeTestingProcessExitCallbackRegister() 13 | 14 | registerProcessExitCallback([PID], () => { 15 | callbackCalled = true 16 | }) 17 | 18 | assertCallbackCalledAfterMs(500) 19 | }) 20 | 21 | const makeTestingProcessExitCallbackRegister = () => 22 | Win32Injector({ 23 | ffi: { 24 | Library () { 25 | return { 26 | OpenProcess () { 27 | return 28 | }, 29 | WaitForSingleObject: { 30 | async (hProc, timeout, callback) { 31 | callback() 32 | } 33 | } 34 | } 35 | } 36 | } 37 | }).registerProcessExitCallback 38 | 39 | const PID = 100 40 | 41 | const assertCallbackCalledAfterMs = (timeout) => { 42 | setTimeout(() => { 43 | expect(callbackCalled).to.equal(true) 44 | }, timeout) 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /test/unit/specs/renderer/GamesPage.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | const GamesPage = require('@/components/GamesPage.vue').default 4 | import { expect } from 'chai' 5 | 6 | describe('GamesPage.vue', () => { 7 | let vm 8 | before(() => { 9 | const store = new Vuex.Store({ 10 | modules: { 11 | Config: { 12 | namespaced: true, 13 | state: { 14 | games: [ 15 | { 16 | code: '', 17 | name: '処女はお姉さまに恋してる3', 18 | path: 19 | 'D:\\Program Files\\処女はお姉さまに恋してる3\\処女はお姉さまに恋してる3.exe' 20 | } 21 | ] 22 | } 23 | } 24 | } 25 | }) 26 | vm = new Vue({ 27 | el: document.createElement('div'), 28 | render: (h) => h(GamesPage), 29 | store 30 | }).$mount() 31 | }) 32 | 33 | it('renders correct header', () => { 34 | expect(vm.$el.querySelector('.app-header').textContent).to.contain( 35 | '我的游戏' 36 | ) 37 | }) 38 | it('renders correct game card', () => { 39 | expect(vm.$el.querySelector('.v-card-title').textContent).to.contain( 40 | '処女はお姉さまに恋してる3' 41 | ) 42 | expect(vm.$el.querySelector('.v-card-text').textContent).to.contain( 43 | 'D:\\Program Files\\処女はお姉さまに恋してる3\\処女はお姉さまに恋してる3.exe' 44 | ) 45 | }) 46 | }) 47 | -------------------------------------------------------------------------------- /test/unit/temp/qqApi.js: -------------------------------------------------------------------------------- 1 | SESSION_URL = "https://fanyi.qq.com/" 2 | TRANSLATE_URL = 3 | "https://fanyi.qq.com/api/translate" 4 | USER_AGENT = 5 | "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36" 6 | 7 | var session 8 | var bv 9 | var requestTranslation 10 | var initSession 11 | var qtv 12 | var qtk 13 | 14 | if (!session) { 15 | session = Request.jar() 16 | 17 | requestTranslation = () => { 18 | return new Promise((resolve, reject) => { 19 | Request.post(TRANSLATE_URL, { 20 | jar: session, 21 | gzip: true, 22 | headers: { 23 | Referer: SESSION_URL, 24 | "User-Agent": USER_AGENT 25 | }, 26 | form: { 27 | source: "jp", 28 | target: "zh", 29 | sourceText: text, 30 | qtv: qtv, 31 | qtk: qtk 32 | } 33 | }) 34 | .then(body => { 35 | let sentences = JSON.parse(body).translate.records 36 | let result = ""; 37 | for (let i in sentences) { 38 | result += sentences[i].targetText 39 | } 40 | result = result.replace(/({[^}]*})|(\(\([^\)]*\)\))/g, '') 41 | if (result === '') initSession() 42 | else callback(result) 43 | }) 44 | .catch(err => { 45 | callback(qtv, qtk) 46 | }); 47 | }); 48 | }; 49 | 50 | initSession = () => { 51 | return Request.get(SESSION_URL, { 52 | jar: session, 53 | gzip: true, 54 | headers: { 55 | Referer: SESSION_URL, 56 | "User-Agent": USER_AGENT 57 | } 58 | }) 59 | .then(body => { 60 | qtv = /var qtv = "([^\"]+)";/.exec(body)[1] 61 | qtk = /var qtk = "([^\"]+)";/.exec(body)[1] 62 | }) 63 | .then(requestTranslation) 64 | }; 65 | 66 | initSession() 67 | } else { 68 | requestTranslation() 69 | } 70 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "types": ["vuetify"], 4 | "sourceMap": true, 5 | "strict": true, 6 | "target": "es6", 7 | "moduleResolution": "node", 8 | "experimentalDecorators": true, 9 | "emitDecoratorMetadata": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "removeComments": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "plugins": [ 16 | { 17 | "name": "typescript-tslint-plugin", 18 | "configFile": "tslint.json", 19 | "alwaysShowRuleFailuresAsWarnings": true 20 | } 21 | ] 22 | }, 23 | "include": ["src/**/*"], 24 | "exclude": ["node_modules", "**/*.spec.ts"] 25 | } 26 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["tslint:recommended", "tslint-config-standard"], 3 | "rules": { 4 | "no-var-requires": false, 5 | "object-literal-sort-keys": false, 6 | "no-floating-promises": false, 7 | "forin": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /types/vuetify.d.ts: -------------------------------------------------------------------------------- 1 | export = vuetify; 2 | declare class vuetify { 3 | static install(Vue: any, args: any): void; 4 | static installed: boolean; 5 | static version: string; 6 | constructor(preset: any); 7 | framework: any; 8 | installed: any; 9 | preset: any; 10 | init(root: any, ssrContext: any): void; 11 | use(Service: any): void; 12 | } 13 | --------------------------------------------------------------------------------