diff --git a/src/common/IpcTypes.ts b/src/common/IpcTypes.ts index f5c078b..c6041bb 100644 --- a/src/common/IpcTypes.ts +++ b/src/common/IpcTypes.ts @@ -16,6 +16,7 @@ enum IpcTypes { REQUEST_REMOVE_GAME = 'request-remove-game', HAS_REMOVED_GAME = 'has-removed-game', REQUEST_RUN_GAME = 'request-run-game', + HAS_RUNNING_GAME = 'has-running-game', REQUEST_TRANSLATION = 'request-translation', HAS_TRANSLATION = 'has-translation', APP_EXIT = 'app-exit', @@ -31,7 +32,9 @@ enum IpcTypes { HAS_NEW_DEBUG_MESSAGE = 'has-new-debug-message', GAME_ABORTED = 'game-aborted', REQUEST_DICT = 'request-dict', - HAS_DICT = 'has-dict' + HAS_DICT = 'has-dict', + REQUEST_PROCESSES = 'request-processes', + HAS_PROCESSES = 'has-processes' } export default IpcTypes diff --git a/src/main/BaseGame.ts b/src/main/BaseGame.ts new file mode 100644 index 0000000..173dc7f --- /dev/null +++ b/src/main/BaseGame.ts @@ -0,0 +1,37 @@ +const debug = require('debug')('yuki:game') +import { EventEmitter } from 'events' +import Hooker from './Hooker' +import { registerProcessExitCallback } from './Win32' + +export default abstract class BaseGame extends EventEmitter { + protected pids: number[] + + constructor () { + super() + this.pids = [] + } + + public abstract start (): void + + public getPids () { + return this.pids + } + + public abstract getInfo (): yuki.Game + + protected afterGetPids () { + this.injectProcessByPid() + this.registerProcessExitCallback() + this.emit('started', this) + } + + private injectProcessByPid () { + this.pids.map((pid) => Hooker.getInstance().injectProcess(pid)) + } + + private registerProcessExitCallback () { + registerProcessExitCallback(this.pids, () => { + this.emit('exited', this) + }) + } +} diff --git a/src/main/Game.ts b/src/main/Game.ts index 2ecc7cb..d2d25f7 100644 --- a/src/main/Game.ts +++ b/src/main/Game.ts @@ -1,17 +1,14 @@ import { exec } from 'child_process' const debug = require('debug')('yuki:game') -import { EventEmitter } from 'events' +import BaseGame from './BaseGame' import ConfigManager from './config/ConfigManager' -import Hooker from './Hooker' -import { registerProcessExitCallback } from './Win32' -export default class Game extends EventEmitter { +export default class Game extends BaseGame { private static readonly TIMEOUT = 1000 private static readonly MAX_RESET_TIME = 10 private execString: string private path: string private code: string - private pids: number[] private name: string private localeChanger: string private exeName: string @@ -27,15 +24,11 @@ export default class Game extends EventEmitter { this.exeName = '' } - public async start () { + public start () { this.execGameProcess() this.registerHookerWithPid() } - public getPids () { - return this.pids - } - public getInfo (): yuki.Game { return { name: this.name, @@ -83,9 +76,7 @@ export default class Game extends EventEmitter { this.emit('exited') return } - this.injectProcessByPid() - this.emit('started', this) - this.registerProcessExitCallback() + this.afterGetPids() } private findPids () { @@ -133,14 +124,4 @@ export default class Game extends EventEmitter { } return pids } - - private injectProcessByPid () { - this.pids.map((pid) => Hooker.getInstance().injectProcess(pid)) - } - - private registerProcessExitCallback () { - registerProcessExitCallback(this.pids, () => { - this.emit('exited', this) - }) - } } diff --git a/src/main/GameFromProcess.ts b/src/main/GameFromProcess.ts new file mode 100644 index 0000000..71a56e9 --- /dev/null +++ b/src/main/GameFromProcess.ts @@ -0,0 +1,24 @@ +import BaseGame from './BaseGame' + +export default class GameFromProcess extends BaseGame { + private process: yuki.Process + + constructor (process: yuki.Process) { + super() + this.process = process + this.pids = [process.pid] + } + + public getInfo (): yuki.Game { + return { + name: this.process.name.replace('.exe', ''), + code: '', + path: '', + localeChanger: '' + } + } + + public start () { + this.afterGetPids() + } +} diff --git a/src/main/Processes.ts b/src/main/Processes.ts new file mode 100644 index 0000000..99e3a4e --- /dev/null +++ b/src/main/Processes.ts @@ -0,0 +1,52 @@ +import { exec } from 'child_process' +const debug = require('debug')('yuki:processes') + +export default class Processes { + public static async get () { + return new Promise((resolve, reject) => { + exec(Processes.TASK_LIST_COMMAND, + (err, stdout, stderr) => { + if (err) { + debug('exec failed !> %s', err) + reject() + return + } + + if (this.findsProcessIn(stdout)) { + const result = this.parseProcessesFrom(stdout) + debug('get %d processes', result.length) + resolve(result) + } else { + debug('exec failed. no process') + reject() + } + } + ) + }) + } + private static TASK_LIST_COMMAND = 'tasklist /nh /fo csv /fi "sessionname eq Console"' + + private static findsProcessIn (value: string) { + return value.startsWith('"') + } + + private static parseProcessesFrom (value: string) { + const processes: yuki.Processes = [] + + const regexResult = value.match(/"([^"]+)"/g) + if (!regexResult) return [] + + let onePair: yuki.Process = { name: '', pid: -1 } + for (let i = 0; i < regexResult.length; i++) { + if (i % 5 === 0) {// process name + onePair.name = regexResult[i].substr(1, regexResult[i].length - 2) + } else if (i % 5 === 1) {// process id + onePair.pid = parseInt(regexResult[i].substr(1, regexResult[i].length - 2), 10) + processes.push(onePair) + onePair = { name: '', pid: -1 } + } + } + + return processes + } +} diff --git a/src/main/TranslatorWindow.ts b/src/main/TranslatorWindow.ts index d310a8d..630b3d0 100644 --- a/src/main/TranslatorWindow.ts +++ b/src/main/TranslatorWindow.ts @@ -1,4 +1,5 @@ import { BrowserWindow } from 'electron' +import BaseGame from './BaseGame' import ConfigManager from './config/ConfigManager' import Game from './Game' import Hooker from './Hooker' @@ -12,9 +13,10 @@ export default class TranslatorWindow { : `file://${__dirname}/translator.html` private window!: Electron.BrowserWindow - private game!: Game + private game!: BaseGame private isRealClose = false + private config!: yuki.Config.Gui['translatorWindow'] constructor () { this.create() @@ -30,7 +32,7 @@ export default class TranslatorWindow { this.window.close() } - public setGame (game: Game) { + public setGame (game: BaseGame) { this.game = game } @@ -39,6 +41,8 @@ export default class TranslatorWindow { } private create () { + this.config = ConfigManager.getInstance().get('gui') + .translatorWindow this.window = new BrowserWindow({ webPreferences: { defaultFontFamily: { @@ -48,8 +52,7 @@ export default class TranslatorWindow { } }, show: false, - alwaysOnTop: ConfigManager.getInstance().get('gui') - .translatorWindow.alwaysOnTop, + alwaysOnTop: this.config.alwaysOnTop, transparent: true, frame: false }) @@ -61,7 +64,10 @@ export default class TranslatorWindow { ) this.window.on('ready-to-show', () => { - ElectronVibrancy.SetVibrancy(this.window, 0) + // choose translucent as default, unless assigning transparent explicitly + if (this.config.renderMode !== 'transparent') { + ElectronVibrancy.SetVibrancy(this.window, 0) + } debug('subscribing hooker events...') this.subscribeHookerEvents() diff --git a/src/main/config/GuiConfig.ts b/src/main/config/GuiConfig.ts index 656c88f..3916b4d 100644 --- a/src/main/config/GuiConfig.ts +++ b/src/main/config/GuiConfig.ts @@ -41,7 +41,8 @@ export default class GuiConfig extends Config { color: 'white', margin: 18 }, - background: '#000000BD' + background: '#000000BD', + renderMode: 'translucent' } } } diff --git a/src/main/setup/Ipc.ts b/src/main/setup/Ipc.ts index b8dd922..ff2c284 100644 --- a/src/main/setup/Ipc.ts +++ b/src/main/setup/Ipc.ts @@ -3,15 +3,18 @@ import IpcTypes from '../../common/IpcTypes' const debug = require('debug')('yuki:ipc') import { extname } from 'path' import { format } from 'util' +import BaseGame from '../BaseGame' import ConfigManager from '../config/ConfigManager' import DownloaderFactory from '../DownloaderFactory' import Game from '../Game' +import GameFromProcess from '../GameFromProcess' import Hooker from '../Hooker' +import Processes from '../Processes' import DictManager from '../translate/DictManager' import TranslationManager from '../translate/TranslationManager' import TranslatorWindow from '../TranslatorWindow' -let runningGame: Game +let runningGame: BaseGame let translatorWindow: TranslatorWindow | null export default function (mainWindow: Electron.BrowserWindow) { @@ -38,11 +41,16 @@ export default function (mainWindow: Electron.BrowserWindow) { ipcMain.on( IpcTypes.REQUEST_RUN_GAME, - (event: Electron.Event, game: yuki.Game) => { - mainWindow.hide() - - runningGame = new Game(game) + (event: Electron.Event, game?: yuki.Game, process?: yuki.Process) => { + if (game) { + runningGame = new Game(game) + } else if (process) { + runningGame = new GameFromProcess(process) + } else return runningGame.on('started', () => { + mainWindow.hide() + mainWindow.webContents.send(IpcTypes.HAS_RUNNING_GAME) + if (translatorWindow) translatorWindow.close() translatorWindow = new TranslatorWindow() translatorWindow.setGame(runningGame) @@ -216,6 +224,17 @@ export default function (mainWindow: Electron.BrowserWindow) { DownloaderFactory.makeLibraryDownloader(packName).start() } ) + + ipcMain.on( + IpcTypes.REQUEST_PROCESSES, + (event: Electron.Event) => { + Processes.get().then((processes) => { + event.sender.send(IpcTypes.HAS_PROCESSES, processes) + }).catch(() => { + event.sender.send(IpcTypes.HAS_PROCESSES, []) + }) + } + ) } function sendConfig (configName: string, event: Electron.Event) { diff --git a/src/renderer/components/GamesPage.vue b/src/renderer/components/GamesPage.vue index 09e20e7..1d1094e 100644 --- a/src/renderer/components/GamesPage.vue +++ b/src/renderer/components/GamesPage.vue @@ -3,11 +3,15 @@ { "zh": { "nothingHere": "什么都没有呢(っ °Д °;)っ", - "goAddSomeGames": "快去添加游戏吧~ヾ(•ω•`)o" + "goAddSomeGames": "快去添加游戏吧~ヾ(•ω•`)o", + "startFromProcess": "从进程启动", + "start": "启动" }, "en": { "nothingHere": "Hmmm nothing here (っ °Д °;)っ", - "goAddSomeGames": "Go add some games now~ヾ(•ω•`)o" + "goAddSomeGames": "Go add some games now~ヾ(•ω•`)o", + "startFromProcess": "Start From Process", + "start": "Start" } } @@ -16,11 +20,48 @@
+ + + {{$t('startFromProcess')}} + + + + + + {{$t('startFromProcess')}} + + + + + + + + + + + {{$t('cancel')}} + {{$t('start')}} + + + +

{{$t('nothingHere')}}
{{$t('goAddSomeGames')}}

+ @@ -34,16 +75,14 @@ diff --git a/src/renderer/components/GamesPageGameCard.vue b/src/renderer/components/GamesPageGameCard.vue index b864f94..964c7a8 100644 --- a/src/renderer/components/GamesPageGameCard.vue +++ b/src/renderer/components/GamesPageGameCard.vue @@ -23,7 +23,7 @@ {{game.code}} {{game.path}} - + {{$t('run')}} mdi-play @@ -64,7 +64,8 @@ import Vue from 'vue' import { Component, - Prop + Prop, + Watch } from 'vue-property-decorator' import { namespace, @@ -87,10 +88,24 @@ export default class HookSettingsHookInfo extends Vue { public defaultConfig!: yuki.ConfigState['default'] @(namespace('Config').State('games')) public gamesConfig!: yuki.ConfigState['games'] + @(namespace('Gui').State('isGameStartingEnded')) + public isGameStartingEnded!: yuki.GuiState['isGameStartingEnded'] public selectedLocaleChanger: string = '' public code: string = '' public showExpansion: boolean = false + public showLoaders: boolean = false + + @Watch('isGameStartingEnded', { + immediate: true, + deep: true + }) + public checkGameStartingEnded (newValue: boolean) { + this.showLoaders = false + if (newValue === true) { + this.$store.commit('Gui/SET_GAME_STARTING_ENDED', { value: false }) + } + } public handleDeleteConfirm () { this.$dialog.confirm({ @@ -109,6 +124,7 @@ export default class HookSettingsHookInfo extends Vue { }) } public handleRunGame () { + this.showLoaders = true ipcRenderer.send(IpcTypes.REQUEST_RUN_GAME, this.game) } public handleDeleteGame () { diff --git a/src/renderer/components/LocaleChangerSettings.vue b/src/renderer/components/LocaleChangerSettings.vue index 5980573..f73b575 100644 --- a/src/renderer/components/LocaleChangerSettings.vue +++ b/src/renderer/components/LocaleChangerSettings.vue @@ -13,7 +13,8 @@ "editLocaleChanger": "编辑区域转换器", "actions": "操作", "escapePatterns": "转义段", - "gamePath": "游戏所在路径" + "gamePath": "游戏所在路径", + "noLineBreak": "禁止换行" }, "en": { "localeChangerSettings": "Locale Changer Settings", @@ -27,7 +28,8 @@ "editLocaleChanger": "Edit Locale Changer", "actions": "Actions", "escapePatterns": "Escape Patterns", - "gamePath": "The path where game is located" + "gamePath": "The path where game is located", + "noLineBreak": "No Line Break" } } @@ -92,6 +94,7 @@ auto-grow v-model="editedItem.exec" :label="$t('executionType')" + :rules="[noLineBreakRule]" > @@ -101,7 +104,7 @@ {{$t('cancel')}} - {{$t('ok')}} + {{$t('ok')}} @@ -128,6 +131,8 @@ type TempLocaleChangerItem = yuki.Config.LocaleChangerItem & { id: string } +type VuetifyRule = (v?: string) => (boolean | string) + @Component export default class LocaleChangerSettings extends Vue { public tableColumns: Array<{text: string, value: string, width?: number}> = [] @@ -145,7 +150,10 @@ export default class LocaleChangerSettings extends Vue { } public editedItem: TempLocaleChangerItem = { ...this.itemPattern } - public mounted () { + public noLineBreakRule!: VuetifyRule + public canSave: boolean = true + + public beforeMount () { this.tableColumns = [ { text: 'ID', @@ -170,6 +178,10 @@ export default class LocaleChangerSettings extends Vue { width: 96 } ] + this.noLineBreakRule = (v) => { + if ((v || '').indexOf('\n') === -1) return true + else return this.$i18n.t('noLineBreak').toString() + } } public closeDialog () { @@ -223,6 +235,15 @@ export default class LocaleChangerSettings extends Vue { } } + @Watch('editedItem.exec') + public checkCanSave (newValue: string) { + if (this.noLineBreakRule(newValue) === true) { + this.canSave = true + } else { + this.canSave = false + } + } + public setDefault (id: string) { const afterLocaleChangers = [] for (const localeChanger of this.tempLocaleChangers) { diff --git a/src/renderer/main.ts b/src/renderer/main.ts index eca3925..f3473d6 100644 --- a/src/renderer/main.ts +++ b/src/renderer/main.ts @@ -62,6 +62,19 @@ function next () { IpcTypes.GAME_ABORTED, () => { vue.$dialog.notify.error(vue.$i18n.t('gameAborted').toString()) + store.commit('Gui/SET_GAME_STARTING_ENDED', { value: true }) + } + ) + ipcRenderer.on( + IpcTypes.HAS_RUNNING_GAME, + () => { + store.commit('Gui/SET_GAME_STARTING_ENDED', { value: true }) + } + ) + ipcRenderer.on( + IpcTypes.HAS_PROCESSES, + (event: Electron.Event, processes: yuki.Processes) => { + store.commit('Gui/SET_PROCESSES', { value: processes }) } ) } diff --git a/src/renderer/store/modules/Gui.ts b/src/renderer/store/modules/Gui.ts index 574219f..795fb7d 100644 --- a/src/renderer/store/modules/Gui.ts +++ b/src/renderer/store/modules/Gui.ts @@ -2,7 +2,19 @@ const MAX_DEBUG_MESSAGES_COLUMNS = 1000 const guiState: yuki.GuiState = { noGame: false, - debugMessages: [] + debugMessages: [], + isGameStartingEnded: false, + processes: [] +} + +const getters = { + getProcessesWithText: (state: yuki.GuiState) => () => { + const processesWithText: yuki.Processes = [...state.processes]; + (processesWithText as yuki.ProcessesWithText).forEach((value) => { + value.text = `${value.pid} - ${value.name}` + }) + return processesWithText + } } const mutations = { @@ -14,10 +26,17 @@ const mutations = { if (state.debugMessages.length > MAX_DEBUG_MESSAGES_COLUMNS) { state.debugMessages.shift() } + }, + SET_GAME_STARTING_ENDED (state: yuki.GuiState, payload: { value: boolean }) { + state.isGameStartingEnded = payload.value + }, + SET_PROCESSES (state: yuki.GuiState, payload: { value: yuki.Processes }) { + state.processes = payload.value } } export default { state: guiState, - mutations + mutations, + getters } diff --git a/src/translator/App.vue b/src/translator/App.vue index 77af087..512ce3d 100644 --- a/src/translator/App.vue +++ b/src/translator/App.vue @@ -18,8 +18,20 @@
+
+ {{$t('translate')}} + {{$t('textHookSettings')}} + {{$t('translatorSettings')}} +
-
+
{{$t('translate')}} {{$t('textHookSettings')}} string @@ -118,6 +132,11 @@ export default class App extends Vue { const newHeight = document.body.offsetHeight + offset const window = remote.getCurrentWindow() const width = window.getSize()[0] + if (newHeight > 640) { + this.$store.dispatch('View/setWindowTooHigh', true) + } else { + this.$store.dispatch('View/setWindowTooHigh', false) + } window.setSize(width, newHeight) } @@ -133,7 +152,7 @@ export default class App extends Vue { public updated () { if (this.$router.currentRoute.path === '/translate') { - if (this.isButtonsShown) { + if (this.isButtonsShown && (!this.isWindowTooHigh)) { this.$nextTick(() => { this.updateWindowHeight(24) }) @@ -237,14 +256,19 @@ body { margin-top: 32px; } -#app #content #buttons { +#app #content #buttons-top { + width: 100%; + margin-left: 16px; +} + +#app #content #buttons-bottom { position: fixed; bottom: 0; width: 100%; left: 16px; } -#app #content #buttons .v-btn { +#app #content .buttons .v-btn { text-align: center; } @@ -258,4 +282,15 @@ body { overflow-x: hidden; overflow-y: scroll; } + +.fixed-scroll-margin-top { + margin: 0 auto; + padding: 16px; + position: fixed; + top: 64px; + width: 100%; + height: 88%; + overflow-x: hidden; + overflow-y: scroll; +} diff --git a/src/translator/components/HookSettings.vue b/src/translator/components/HookSettings.vue index 776d0f9..e548d61 100644 --- a/src/translator/components/HookSettings.vue +++ b/src/translator/components/HookSettings.vue @@ -13,7 +13,7 @@ @@ -100,6 +94,16 @@ export default class HookSettings extends Vue { public texts!: string[] @(namespace('Hooks').State('currentDisplayHookIndex')) public currentIndex!: number + @(namespace('View').State('isWindowTooHigh')) + public isWindowTooHigh!: boolean + + get classObject () { + return { + 'small-margin': true, + 'fixed-scroll': !this.isWindowTooHigh, + 'fixed-scroll-margin-top': this.isWindowTooHigh + } + } public openInputHookDialog () { this.openInputHook = true diff --git a/src/translator/components/SettingsPage.vue b/src/translator/components/SettingsPage.vue index 5737eea..b934546 100644 --- a/src/translator/components/SettingsPage.vue +++ b/src/translator/components/SettingsPage.vue @@ -37,83 +37,62 @@