├── .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 |
3 |
4 | YUKI Galgame 翻译器
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 中文 •
17 | English
18 |
19 |
20 | 
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 |
3 |
4 | YUKI - Yummy Utterance Knowledge Interface
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | 中文 •
17 | English
18 |
19 |
20 | 
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 |
2 |
3 |
4 |
5 |
6 |
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 |
4 |
5 |
6 |
7 |
8 | Yummy Utterance Knowledge Interface
9 | YUKI {{$t('YUKIGalgameTranslator')}}
10 | {{$t('toggleDevTools')}}
11 |
12 |
13 |
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 |
30 |
31 |
32 |
33 |
34 | {{$t('chooseGamePath')}}
35 |
36 | {{$t('chooseGamePath')}}
37 |
38 |
39 |
40 | {{$t('gameName')}}
41 |
42 | {{$t('gamePath')}}
43 |
44 |
45 | {{$t('nextStep')}}
46 |
47 |
48 | {{$t('inputSpecialCode')}}
49 |
50 | {{$t('pleaseInputSpecialCodeEmptyIfNotNeeded')}}
51 |
52 |
53 | {{$t('prevStep')}}
54 | {{$t('finish')}}
55 |
56 |
57 |
58 |
59 |
60 |
61 |
153 |
154 |
156 |
--------------------------------------------------------------------------------
/src/renderer/components/AppSidebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | YUKI
12 | {{$t('YUKIGalgameTranslator')}}
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | mdi-gamepad
23 |
24 |
25 | {{$t('myGames')}}
26 |
27 |
28 |
29 |
30 |
31 | mdi-plus-box-multiple
32 |
33 |
34 | {{$t('addGame')}}
35 |
36 |
37 |
38 |
39 |
40 |
41 | mdi-settings
42 |
43 |
44 | {{$t('applicationSettings')}}
45 |
46 |
47 |
48 |
49 |
50 | mdi-map-marker
51 |
52 | {{$t('localeChangers')}}
53 |
54 |
55 |
56 |
57 | mdi-apps
58 |
59 | {{$t('applicationLibraries')}}
60 |
61 |
62 |
63 |
64 | mdi-translate
65 |
66 | {{$t('translators')}}
67 |
68 |
69 |
70 |
71 |
72 | mdi-alert
73 |
74 |
75 | {{$t('debugMsg')}}
76 |
77 |
78 |
79 |
80 |
81 | mdi-information
82 |
83 |
84 | {{$t('aboutYUKI')}}
85 |
86 |
87 |
88 |
89 |
90 |
91 |
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 |
20 |
21 |
22 |
23 |
24 | {{$t('log')}}
25 |
26 | {{$t('maxColumns')}}: 1000
27 |
28 | {{$t('copyToClipboard')}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
103 |
104 |
109 |
--------------------------------------------------------------------------------
/src/renderer/components/DownloadProgress.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
{{ speedKB }} KB/s - {{ remainingTime }} s
6 |
{{ transferedSizeMB }} MB / {{ totalSizeMB }} MB
7 |
8 |
9 |
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 |
20 |
21 |
22 |
23 |
24 |
25 | {{$t('startFromProcess')}}
31 |
32 |
33 |
34 |
35 |
36 | {{$t('startFromProcess')}}
37 |
38 |
39 |
40 |
41 |
48 |
49 |
50 |
51 |
52 |
53 | {{$t('cancel')}}
54 | {{$t('start')}}
55 |
56 |
57 |
58 |
59 |
60 | {{$t('nothingHere')}}
61 |
62 | {{$t('goAddSomeGames')}}
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
134 |
135 |
140 |
--------------------------------------------------------------------------------
/src/renderer/components/PageContent.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
19 |
20 |
30 |
--------------------------------------------------------------------------------
/src/renderer/components/PageHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 |
41 |
42 |
56 |
--------------------------------------------------------------------------------
/src/renderer/components/SettingsPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
18 |
19 |
{{$t('save')}}
20 |
{{$t('reset')}}
21 |
{{$t('translatorSettings')}}
22 |
23 | {{$t('underConstruction')}}
24 |
25 | {{$t('pleaseModifyConfigurationFile')}}
26 |
27 | {{$t('defaultTranslators')}}
28 |
29 |
30 |
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 |
16 |
17 | {{$t('addHook')}}
18 |
19 |
20 |
21 |
22 | {{$t('inputSpecialCode')}}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{$t('cancel')}}
34 | {{$t('ok')}}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
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 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{hook.name}}
25 | {{hook.code}}
26 |
27 |
28 |
29 |
30 |
39 |
40 |
41 |
42 | {{$t('choose')}}
43 | mdi-check
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
99 |
100 |
117 |
--------------------------------------------------------------------------------
/src/translator/components/HooksPage.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
31 |
32 |
34 |
--------------------------------------------------------------------------------
/src/translator/components/TextDisplay.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{name}}
12 |
{{translation}}
21 |
22 |
23 |
24 |
25 |
43 |
44 |
57 |
--------------------------------------------------------------------------------
/src/translator/components/Titlebar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
YUKI GALGAME TRANSLATOR
4 |
13 | mdi-play
14 |
15 |
24 | mdi-pause
25 |
26 |
35 | mdi-lock
36 |
37 |
46 | mdi-lock-open-outline
47 |
48 |
57 | mdi-close
58 |
59 |
60 |
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 |
--------------------------------------------------------------------------------