├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .travis.txt ├── LICENSE.md ├── README.md ├── README_CN.md ├── app ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .vscode │ ├── launch.json │ └── settings.json ├── package-lock.json ├── package.json ├── public │ └── .gitkeep ├── resources │ └── .gitkeep ├── script │ ├── build.bat │ ├── build.sh │ ├── get-external-resources.ts │ ├── release-download-count.ts │ ├── rm.ts │ ├── source-count.ts │ └── tsconfig.json ├── src │ ├── css │ │ ├── game.css │ │ └── mishiro.css │ ├── res │ │ ├── banner.svg │ │ └── icon │ │ │ ├── 1024x1024.png │ │ │ ├── 128x128.png │ │ │ ├── 16x16.png │ │ │ ├── 24x24.png │ │ │ ├── 256x256.png │ │ │ ├── 32x32.png │ │ │ ├── 48x48.png │ │ │ ├── 512x512.png │ │ │ ├── 64x64.png │ │ │ ├── app.icns │ │ │ └── app.ico │ ├── ts │ │ ├── common │ │ │ ├── asar.ts │ │ │ ├── db.ts │ │ │ ├── get-path.ts │ │ │ └── util.ts │ │ ├── i18n │ │ │ ├── en-US.ts │ │ │ ├── ja-JP.ts │ │ │ └── zh-CN.ts │ │ ├── main.ts │ │ ├── main │ │ │ ├── config.ts │ │ │ ├── core.ts │ │ │ ├── get-gacha-data.ts │ │ │ ├── icon.ts │ │ │ ├── ipc.ts │ │ │ ├── log.ts │ │ │ ├── on-check-score.ts │ │ │ ├── on-game.ts │ │ │ ├── on-lyrics.ts │ │ │ ├── on-score.ts │ │ │ ├── open-score-window.ts │ │ │ ├── resolve-gacha-available.ts │ │ │ ├── signup.ts │ │ │ ├── tsconfig.json │ │ │ ├── typings │ │ │ │ └── global.d.ts │ │ │ └── updater.ts │ │ ├── renderer-back.ts │ │ ├── renderer-game.ts │ │ ├── renderer-score.ts │ │ ├── renderer.ts │ │ ├── renderer │ │ │ ├── audio.ts │ │ │ ├── back │ │ │ │ ├── batch-download.ts │ │ │ │ ├── get-event-data.ts │ │ │ │ ├── get-limited-card.ts │ │ │ │ ├── hash.ts │ │ │ │ ├── main-window-id.ts │ │ │ │ ├── on-master-read.ts │ │ │ │ ├── resolve-audio-manifest.ts │ │ │ │ ├── resolve-card-data.ts │ │ │ │ ├── resolve-chara-data.ts │ │ │ │ └── resolve-user-level.ts │ │ │ ├── calculator-data.ts │ │ │ ├── calculator.ts │ │ │ ├── check.ts │ │ │ ├── config.ts │ │ │ ├── developer-api.ts │ │ │ ├── game.ts │ │ │ ├── ipc-back.ts │ │ │ ├── ipc.ts │ │ │ ├── license.ts │ │ │ ├── log.ts │ │ │ ├── mishiro-commu.ts │ │ │ ├── mishiro-entry.ts │ │ │ ├── mishiro-gacha.ts │ │ │ ├── mishiro-game.ts │ │ │ ├── mishiro-home.ts │ │ │ ├── mishiro-idol.ts │ │ │ ├── mishiro-live.ts │ │ │ ├── mishiro-menu.ts │ │ │ ├── mishiro-update.ts │ │ │ ├── mishiro.ts │ │ │ ├── modal-about.ts │ │ │ ├── modal-batch-error.ts │ │ │ ├── modal-gacha-card.ts │ │ │ ├── modal-live-difficulty.ts │ │ │ ├── modal-mixin.ts │ │ │ ├── modal-score.ts │ │ │ ├── modal-version.ts │ │ │ ├── preload.ts │ │ │ ├── scoreviewer │ │ │ │ ├── flip-note.ts │ │ │ │ ├── global.ts │ │ │ │ ├── long-move-note.ts │ │ │ │ ├── long-note.ts │ │ │ │ ├── note.ts │ │ │ │ ├── score-viewer.ts │ │ │ │ └── tap-note.ts │ │ │ ├── socket.ts │ │ │ ├── store.ts │ │ │ ├── tab-small.ts │ │ │ ├── the-background.ts │ │ │ ├── the-footer.ts │ │ │ ├── the-player.ts │ │ │ ├── the-table.ts │ │ │ ├── the-toggle-button.ts │ │ │ ├── tsconfig.json │ │ │ ├── typings │ │ │ │ ├── main.d.ts │ │ │ │ ├── sfc.d.ts │ │ │ │ └── vue.d.ts │ │ │ ├── unpack-texture-2d.ts │ │ │ ├── updater.ts │ │ │ └── vue-global.ts │ │ ├── template │ │ │ ├── back.template.ts │ │ │ ├── game.template.ts │ │ │ ├── index.template.ts │ │ │ └── score.template.ts │ │ └── typings │ │ │ ├── global.d.ts │ │ │ ├── manifest.d.ts │ │ │ ├── non-webpack-require.d.ts │ │ │ └── preload.d.ts │ └── vue │ │ ├── Mishiro.vue │ │ ├── MishiroGame.vue │ │ ├── component │ │ ├── InputRadio.vue │ │ ├── InputText.vue │ │ ├── ProgressBar.vue │ │ ├── StaticTitleDot.vue │ │ ├── TabSmall.vue │ │ ├── TaskLoading.vue │ │ ├── TheBackground.vue │ │ ├── TheCombo.vue │ │ ├── TheFooter.vue │ │ ├── TheLiveGauge.vue │ │ ├── ThePlayer.vue │ │ ├── TheTable.vue │ │ ├── TheToggleButton.vue │ │ └── TheVersion.vue │ │ ├── modal │ │ ├── ModalAbout.vue │ │ ├── ModalAlert.vue │ │ ├── ModalBatchError.vue │ │ ├── ModalCalculator.vue │ │ ├── ModalGachaCard.vue │ │ ├── ModalGachaHistory.vue │ │ ├── ModalGachaInformation.vue │ │ ├── ModalLiveDifficulty.vue │ │ ├── ModalLiveResult.vue │ │ ├── ModalOption.vue │ │ ├── ModalScore.vue │ │ └── ModalVersion.vue │ │ └── view │ │ ├── MishiroCommu.vue │ │ ├── MishiroEntry.vue │ │ ├── MishiroGacha.vue │ │ ├── MishiroHome.vue │ │ ├── MishiroIdol.vue │ │ ├── MishiroLive.vue │ │ ├── MishiroMenu.vue │ │ └── MishiroUpdate.vue ├── tsconfig.base.json ├── tsconfig.json └── tyconfig.ts ├── dist ├── build.bat └── mishiro.nsi └── img ├── alipay.jpg └── screenshot.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [toyobayashi] 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Build 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | os: [windows-latest, ubuntu-latest, macos-13, macos-14] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '18.17.1' 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - name: Get npm cache directory 22 | id: npm-cache 23 | run: | 24 | echo "::set-output name=dir::$(npm config get cache)" 25 | 26 | - uses: actions/cache@v2 27 | with: 28 | path: ${{ steps.npm-cache.outputs.dir }} 29 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 30 | restore-keys: | 31 | ${{ runner.os }}-node- 32 | 33 | - name: Windows build 34 | if: ${{ matrix.os == 'windows-latest' }} 35 | shell: cmd 36 | run: | 37 | call .\app\script\build.bat 38 | 39 | - name: Linux build 40 | if: ${{ matrix.os == 'ubuntu-latest' }} 41 | shell: bash 42 | run: | 43 | chmod +x ./app/script/build.sh 44 | ./app/script/build.sh 45 | 46 | - name: macOS build 47 | if: ${{ contains(matrix.os, 'macos') }} 48 | shell: bash 49 | run: | 50 | chmod +x ./app/script/build.sh 51 | ./app/script/build.sh 52 | 53 | - name: Get assets 54 | if: ${{ startsWith(github.event.ref, 'refs/tags') }} 55 | run: cd app&&npm run get&&cd .. 56 | env: 57 | MISHIRO_NO_PROGRESS: '1' 58 | 59 | - name: Pack x64 60 | if: ${{ startsWith(github.event.ref, 'refs/tags') && matrix.os != 'macos-14' }} 61 | run: cd app&&npm run pack:x64&&cd .. 62 | 63 | - name: Pack ia32 64 | if: ${{ startsWith(github.event.ref, 'refs/tags') && matrix.os == 'windows-latest' }} 65 | run: cd app&&npm run pack:ia32&&cd .. 66 | 67 | - name: Pack arm64 68 | if: ${{ startsWith(github.event.ref, 'refs/tags') && matrix.os == 'macos-14' }} 69 | run: cd app&&npm run pack:arm64&&cd .. 70 | 71 | - name: Create release 72 | if: ${{ startsWith(github.event.ref, 'refs/tags') }} 73 | uses: toyobayashi/upload-release-assets@v3.0.0 74 | env: 75 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 76 | with: 77 | tag_name: ${{ github.event.after }} 78 | release_name: ${{ github.event.after }} 79 | draft: true 80 | prerelease: false 81 | assets: | 82 | ./dist/*.zip 83 | ./dist/*.deb 84 | ./dist/*.exe 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | !app/public/LICENSE.md 3 | dist/* 4 | !dist/mishiro.nsi 5 | !dist/mishiro.iss 6 | !dist/build.bat 7 | /config.json 8 | /app/.cache/* 9 | /app/local_resources 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /.travis.txt: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - lts/* 4 | os: 5 | - windows 6 | - linux 7 | - osx 8 | before_install: 9 | - if [ "$TRAVIS_OS_NAME" = "windows" ]; then choco install innosetup; fi 10 | - node -v 11 | - npm -v 12 | - npm install -g node-gyp@5 13 | - node-gyp -v 14 | - if [ "$TRAVIS_OS_NAME" = "windows" ]; then npm config set node_gyp "`npm prefix -g`\\node_modules\\node-gyp\\bin\\node-gyp.js"; else npm config set node_gyp "`npm prefix -g`/lib/node_modules/node-gyp/bin/node-gyp.js"; fi 15 | - node-gyp install --target=$(node -p require\(\'./app/package.json\'\).devDependencies.electron) --dist-url=https://atom.io/download/electron 16 | - cd ./app 17 | - rm -rf ./package-lock.json 18 | - npm config ls 19 | install: 20 | - npm install 21 | script: 22 | - npm run build 23 | before_deploy: 24 | - npm run get 25 | - npm run pack:x64 26 | - if [ "$TRAVIS_OS_NAME" = "windows" ]; then npm run pack:ia32; fi 27 | deploy: 28 | provider: releases 29 | api_key: 30 | secure: BuOGRX9GVcPFPNm/6aU5PRYUz76FGYZsJpQRP6Bv+RMd2ynzY+PJMHeWCSvqJJQ/Zm1PeuTelvY2tJPFbQt9+rNxCJzNv7O6TGxmpXlZcXmi1xhYQLbtFbw4r23D5yKKVBGFtAkTkuUASdDSpvk151HWVssRVTrONxwXuIRCSrVwGDIWD8U0CvIVDWkZZ+5ANrfboKwCYKWPfHB8yc9+vvrluetbIipnZYJTV36VDfY73aZTS23mreMNzOXe/j/3vQMVh5ju40oGS+u68K/RzrgcmfXbEyHk+oHpb64dd2L4pmzvUV3HSk/49ljdnftp4+wq3EwxS1aHfEz++wKkNanZb1yA4UdaSNVZauX29SAW2K8bxZclmKh/2DlWh5hlxQBvg9hISMOi0mWuzmtSNPwDQ/prD15Tw7cn478HWqGXccFzifrWwm7FJZApEOqIJ0Pg6Y7ike8gdK1VmX8229q9tHFp/jvdVxI10rJN7WGoL3qndVp9GvrVatXNE0sFTDtDuVN6CvcEhFpRPuXRZ3UFsCzHszt9MZ5DlPRWnUhqPk3Xo+0rZhZWisJlP3mK603WBs5dQnMSWe+9rOSNcqupBoVNpQREff1oUrJFhRbMdfn6TeP9cYyAvbZU2Qbvyr3hpHouLlnDnaB01q4q+IsoLThiDzPXZQTQi+ai6CM= 31 | file: 32 | - ../dist/*.zip 33 | - ../dist/*.deb 34 | - ../dist/*.exe 35 | file_glob: true 36 | skip_cleanup: true 37 | draft: true 38 | on: 39 | repo: toyobayashi/mishiro 40 | tags: true 41 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2017-2018 Toyobayashi 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without limitation 9 | the rights to use, copy, modify, merge, publish, distribute, 10 | and/or sublicense copies of the Software, and to permit 11 | persons to whom the Software is furnished to do so, subject 12 | to the following conditions: 13 | 14 | 1. The above copyright notice and this permission notice shall 15 | be included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 | # mishiro 2 | 3 | ## 下载 4 | 5 | * [从 Github 下载](https://github.com/toyobayashi/mishiro/releases) 6 | 7 | 8 | 注意: 9 | 10 | * 不推荐将 mishiro 安装或解压在包含汉字的路径下,可能会出现一些问题。 11 | 12 | * 如果遇到无法启动或其它任何报错,请尝试安装最新版本。 13 | 14 | 15 | ## 特性 16 | 17 | * 语言支持:中文 / 日本語 / English 18 | * [ HOME ] 拿资源。(unity3d, acb, bdb, mdb) 19 | * [ IDOL ] 查卡,拿卡面,拿角色语音。 20 | * [ COMMU ] 查P。 21 | * [ LIVE ] 拿背景音乐 / Live乐曲,谱面演示。 22 | * [ MENU ] 活动算分,设置等 23 | 24 | 谱面查看演示:[https://toyobayashi.github.io/mishiro-score-viewer/](https://toyobayashi.github.io/mishiro-score-viewer/) 25 | 仓库:[mishiro-score-viewer](https://github.com/toyobayashi/mishiro-score-viewer) 26 | 27 | ## 开发 & 构建 28 | 29 | ### Windows 需要 30 | 31 | * __Windows 7 以上__ 32 | * __Node.js 18+__ 33 | * __Python 3__ 34 | * __Visual Studio 2022 并安装`使用 C++ 的桌面开发`工作负载或 `VC++ v140+ 构建工具集`__ 35 | * __.NET 和 Powershell__ 36 | 37 | ### Linux 需要 38 | 39 | * __Node.js 18+__ 40 | * __Python 3__ 41 | * __gcc & g++__ 42 | * __make__ 43 | * __zip & unzip__ 44 | 45 | ### MacOS 需要 (这部分未测试) 46 | 47 | * __Node.js 18+__ 48 | * __Python 3__ 49 | * __Xcode__ (终端运行 ```xcode-select --install``` 安装Command Line Tools) 50 | 51 | ### 快速开始 52 | 53 | 1. 拉代码 / 更新代码 54 | 55 | ``` bash 56 | $ git clone https://github.com/toyobayashi/mishiro.git 57 | 58 | $ git pull 59 | ``` 60 | 61 | **NOTE:** 由于 C++ 原生模块编译必须匹配对应的 Electron / Node.js 版本,每当 `package.json` 内的 `electron` 版本变化时,请手动删除以下的文件夹然后再重新跑一次 `npm install`。 62 | 63 | * `/app/node_modules/mishiro-core` 64 | * `/app/node_modules/sqlite3` 65 | * `/app/node_modules/hca-decoder` 66 | * `/app/node_modules/spdlog` 67 | * `/app/node_modules/usm-decrypter` 68 | 69 | 也可以直接跑 `npm run rm` 来完成。 70 | 71 | 2. 装依赖 72 | 73 | mishiro 依赖了一些 C++ 原生模块,在 `npm install` 的时候这些 C++ 包的代码会被编译,所以请确保本地配置好了 C++ 的编译环境,否则 `npm install` 会失败。 74 | 75 | * Windows 76 | 77 | ``` bat 78 | > cd mishiro/app 79 | 80 | REM 设置国内镜像 81 | > npm config set registry https://registry.npmmirror.com/ 82 | > npm config set electron_mirror https://registry.npmmirror.com/-/binary/electron/ 83 | 84 | > npm install -g node-gyp 85 | 86 | REM 根据 package.json 中指定的 electron 版本下载对应的头文件 87 | > for /f "delims=" %P in ('node -p "require('./package.json').devDependencies.electron"') do node-gyp install --target=%P --dist-url=https://electronjs.org/headers 88 | 89 | REM 安装依赖 90 | > npm install 91 | REM 获取开发所需要的额外的资源 92 | > npm run get 93 | ``` 94 | 95 | * Linux / MacOS 96 | 97 | ``` bash 98 | $ cd mishiro/app 99 | 100 | $ npm config set registry http://registry.npm.taobao.org/ 101 | $ npm config set electron_mirror https://registry.npmmirror.com/-/binary/electron/ 102 | 103 | $ npm install -g node-gyp 104 | $ node-gyp install --target=$(node -p require\(\'./package.json\'\).devDependencies.electron) --dist-url=https://electronjs.org/headers 105 | 106 | $ npm install 107 | $ npm run get # 获取开发所需要的额外的资源 108 | ``` 109 | 110 | 如果 `npm install` 失败,请检查下面几种情况: 111 | 112 | 1. 是否有 C++ 编译环境(VC++ / g++) 113 | 2. electron 头文件版本及其存放的位置是否正确 114 | 3. 网络环境和 npm 镜像 115 | 116 | * 开发 117 | 118 | 推荐使用 VSCode 119 | 120 | ``` bash 121 | # ~/mishiro/app$ code . 122 | $ npm run dev 123 | 124 | # 或者 125 | $ npm run serve 126 | # 然后通过 VSCode 的调试模式启动 127 | ``` 128 | 129 | 如果启动时弹框报错,请检查原生模块是否编译成功以及 electron 头文件的版本是否正确。 130 | 131 | * 构建 132 | 133 | ``` bash 134 | # 打包生产环境代码 135 | $ npm run build 136 | ``` 137 | 138 | * 启动 139 | 140 | ``` bash 141 | # 生产环境启动 142 | $ npm start 143 | ``` 144 | 145 | * 打包 146 | 147 | ``` bash 148 | $ npm run pack:x64 # x64 149 | $ npm run pack:ia32 # Windows x86 150 | ``` 151 | 152 | ## 参考 153 | 特别感谢: 154 | * [デレステ解析ノート](https://subdiox.github.io/deresute/) 155 | * [subdiox/UnityLz4](https://github.com/subdiox/UnityLz4) 156 | * [subdiox/StarlightTool](https://github.com/subdiox/StarlightTool) 157 | * [Nyagamon/HCADecoder](https://github.com/Nyagamon/HCADecoder) 158 | * [marcan/deresuteme](https://github.com/marcan/deresuteme) 159 | * [summertriangle-dev/sparklebox](https://github.com/summertriangle-dev/sparklebox) 160 | * [superk589/DereGuide](https://github.com/superk589/DereGuide) 161 | * [OpenCGSS/DereTore](https://github.com/OpenCGSS/DereTore) 162 | 163 | ## 版权 164 | CGSS及其相关所有内容的版权归[BANDAI NAMCO Entertainment Inc.](https://bandainamcoent.co.jp/)所属。 165 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true 6 | }, 7 | parser: 'vue-eslint-parser', 8 | plugins: [ 9 | // '@typescript-eslint', 10 | 'html', 11 | 'vue' 12 | ], 13 | extends: [ 14 | 'standard-with-typescript' 15 | ], 16 | rules: { 17 | 'no-irregular-whitespace': 'off', 18 | '@typescript-eslint/method-signature-style': 'off', 19 | '@typescript-eslint/restrict-template-expressions': 'off', 20 | '@typescript-eslint/prefer-optional-chain': 'off', 21 | '@typescript-eslint/no-base-to-string': 'off', 22 | '@typescript-eslint/return-await': 'off', 23 | '@typescript-eslint/no-dynamic-delete': 'off', 24 | '@typescript-eslint/prefer-nullish-coalescing': 'off', 25 | '@typescript-eslint/no-unused-vars': 'error', 26 | '@typescript-eslint/strict-boolean-expressions': 'off', 27 | '@typescript-eslint/no-var-requires': 'off', 28 | '@typescript-eslint/promise-function-async': 'off', 29 | '@typescript-eslint/no-misused-promises': 'off', 30 | '@typescript-eslint/no-this-alias': 'off', 31 | '@typescript-eslint/no-non-null-assertion': 'off', 32 | '@typescript-eslint/no-namespace': 'off', 33 | 'standard/no-callback-literal': 'off' 34 | }, 35 | parserOptions: { 36 | parser: '@typescript-eslint/parser', 37 | ecmaVersion: 2019, 38 | sourceType: 'module', 39 | project: './tsconfig.json', 40 | tsconfigRootDir: __dirname, 41 | extraFileExtensions: ['.vue'], 42 | createDefaultProgram: true 43 | }, 44 | globals: { 45 | __non_webpack_require__: false, 46 | MISHIRO_DEV_SERVER_PORT: false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /resources/app 3 | /resources/asset/* 4 | /resources/config.json 5 | /tmp 6 | /local_resources 7 | -------------------------------------------------------------------------------- /app/.npmrc: -------------------------------------------------------------------------------- 1 | build_from_source=true 2 | runtime=electron 3 | target=27.3.2 4 | disturl=https://electronjs.org/headers 5 | # openssl_fips= 6 | # napi_build_version=9 7 | -------------------------------------------------------------------------------- /app/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "attach", 7 | "port": 9222, 8 | "name": "Attach to Main Process", 9 | "processId": "${command:PickProcess}" 10 | }, 11 | { 12 | "type": "chrome", 13 | "request": "attach", 14 | "name": "Attach to Renderer Process", 15 | "port": 9222, 16 | "webRoot": "${workspaceFolder}/local_resources", 17 | "sourceMaps": true, 18 | "sourceMapPathOverrides": { 19 | "webpack://mishiro/*": "${workspaceFolder}/*", 20 | "webpack:///*": "${workspaceFolder}/*", 21 | "webpack:///./*": "${workspaceFolder}/*" 22 | } 23 | }, 24 | { 25 | "name": "Launch Main Process", 26 | "type": "node", 27 | "request": "launch", 28 | "cwd": "${workspaceFolder}", 29 | "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron", 30 | "console": "integratedTerminal", 31 | "windows": { 32 | "runtimeExecutable": "${workspaceFolder}\\node_modules\\.bin\\electron.cmd" 33 | }, 34 | "runtimeArgs": [ 35 | "--remote-debugging-port=9222", 36 | "${workspaceFolder}/local_resources/app" 37 | ], 38 | "sourceMaps": true, 39 | "protocol": "inspector" 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[jsonc]": { 3 | "editor.defaultFormatter": "vscode.json-language-features" 4 | }, 5 | "[javascript]": { 6 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 7 | }, 8 | "[typescript]": { 9 | "editor.defaultFormatter": "dbaeumer.vscode-eslint", 10 | }, 11 | "[vue]": { 12 | "editor.defaultFormatter": "octref.vetur" 13 | }, 14 | "typescript.tsdk": "./node_modules/typescript/lib", 15 | "eslint.validate": [ 16 | "javascript", 17 | "typescript", 18 | "vue" 19 | ], 20 | "eslint.format.enable": true, 21 | "editor.formatOnSave": false, 22 | "javascript.format.semicolons": "remove", 23 | "typescript.format.semicolons": "remove", 24 | "vetur.validation.script": true, 25 | "vetur.format.defaultFormatter.js": "dbaeumer.vscode-eslint", 26 | "vetur.format.defaultFormatter.ts": "dbaeumer.vscode-eslint", 27 | "typescript.format.insertSpaceAfterConstructor": true, 28 | "javascript.format.insertSpaceAfterConstructor": true, 29 | "javascript.format.insertSpaceBeforeFunctionParenthesis": true, 30 | "typescript.format.insertSpaceBeforeFunctionParenthesis": true, 31 | "javascript.preferences.quoteStyle": "single", 32 | "typescript.preferences.quoteStyle": "single" 33 | } 34 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mishiro", 3 | "version": "2.7.1", 4 | "description": "mishiro", 5 | "main": "./main/mishiro.main.js", 6 | "scripts": { 7 | "dev": "ty dev", 8 | "serve": "ty serve --progress", 9 | "build": "ty build", 10 | "start": "cross-env NODE_ENV=production ty start", 11 | "pack:ia32": "ty pack --arch=ia32", 12 | "pack:x64": "ty pack --arch=x64", 13 | "pack:arm64": "ty pack --arch=arm64", 14 | "count": "ts-node -P ./script/tsconfig.json ./script/source-count.ts", 15 | "dlc": "ts-node -P ./script/tsconfig.json ./script/release-download-count.ts", 16 | "rm": "ts-node -P ./script/tsconfig.json ./script/rm.ts", 17 | "get": "ts-node -P ./script/tsconfig.json ./script/get-external-resources.ts" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/toyobayashi/mishiro.git" 22 | }, 23 | "keywords": [ 24 | "CGSS", 25 | "idol", 26 | "master", 27 | "346", 28 | "mishiro" 29 | ], 30 | "author": { 31 | "name": "toyobayashi", 32 | "url": "https://github.com/toyobayashi" 33 | }, 34 | "license": "MIT", 35 | "devDependencies": { 36 | "@tybys/ty": "~0.21.1", 37 | "@types/fs-extra": "^9.0.11", 38 | "@types/marked": "^1.1.0", 39 | "@types/node": "^14.14.41", 40 | "@types/sqlite3": "^3.1.6", 41 | "@types/terser-webpack-plugin": "^5.0.4", 42 | "@types/webpack": "^4.41.21", 43 | "@typescript-eslint/eslint-plugin": "^5.8.1", 44 | "@typescript-eslint/parser": "^5.8.1", 45 | "asar": "^3.0.3", 46 | "chromium-pickle-js": "^0.2.0", 47 | "cross-env": "^7.0.3", 48 | "cuint": "^0.2.2", 49 | "electron": "27.3.2", 50 | "eslint": "^8.6.0", 51 | "eslint-config-standard-with-typescript": "^21.0.1", 52 | "eslint-plugin-html": "^6.2.0", 53 | "eslint-plugin-import": "^2.25.3", 54 | "eslint-plugin-node": "^11.1.0", 55 | "eslint-plugin-promise": "^6.0.0", 56 | "eslint-plugin-vue": "^8.2.0", 57 | "got": "^11.8.2", 58 | "html-webpack-plugin": "^5.5.0", 59 | "marked": "1.2.7", 60 | "terser-webpack-plugin": "^5.3.0", 61 | "ts-node": "^9.1.1", 62 | "tslib": "~2.3.0", 63 | "typescript": "4.5.4", 64 | "vue": "2.6.14", 65 | "vue-class-component": "7.2.6", 66 | "vue-i18n": "8.24.4", 67 | "vue-property-decorator": "9.1.2", 68 | "vue-template-compiler": "2.6.14", 69 | "vuex": "3.6.2", 70 | "webpack": "^5.65.0", 71 | "webpack-dev-server": "^3.11.2" 72 | }, 73 | "dependencies": { 74 | "@tybys/downloader": "0.2.0", 75 | "@vscode/spdlog": "0.15.0", 76 | "acb": "2.1.0", 77 | "electron-github-asar-updater": "4.0.0", 78 | "fs-extra": "10.0.0", 79 | "hca-decoder": "1.6.0", 80 | "iconv-lite": "0.6.3", 81 | "mishiro-core": "6.3.7", 82 | "node-gyp": "^10", 83 | "sqlite3": "5.1.7" 84 | }, 85 | "overrides": { 86 | "node-gyp": "^10" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/public/.gitkeep -------------------------------------------------------------------------------- /app/resources/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/resources/.gitkeep -------------------------------------------------------------------------------- /app/script/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | @REM call npm.cmd config set toolset v144 4 | @REM call npm.cmd config set msvs_version 2022 5 | 6 | call npm.cmd install -g node-gyp@10 7 | 8 | @REM for /f "delims=" %%P in ('npm prefix -g') do call npm.cmd config set node_gyp "%%P\node_modules\node-gyp\bin\node-gyp.js" 9 | for /f "delims=" %%P in ('node -p "require('./app/package.json').devDependencies.electron"') do call node-gyp.cmd install --target=%%P --disturl=https://electronjs.org/headers 10 | 11 | cd .\app 12 | call npm.cmd install --legacy-peer-deps --verbose 13 | call npm.cmd run build 14 | cd .. 15 | -------------------------------------------------------------------------------- /app/script/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | npm install -g node-gyp@10 4 | # npm config set node_gyp "`npm prefix -g`/lib/node_modules/node-gyp/bin/node-gyp.js" 5 | node-gyp install --target=$(node -p require\(\'./app/package.json\'\).devDependencies.electron) --disturl=https://electronjs.org/headers 6 | 7 | cd ./app 8 | npm install --legacy-peer-deps --verbose 9 | npm run build 10 | cd .. 11 | -------------------------------------------------------------------------------- /app/script/get-external-resources.ts: -------------------------------------------------------------------------------- 1 | import { join, dirname } from 'path' 2 | import got from 'got' 3 | import * as fs from 'fs-extra' 4 | import { unzipSync } from '@tybys/cross-zip' 5 | 6 | (function () { 7 | const target = join(__dirname, '../tmp/mishiro-ex.zip') 8 | const tmpDir = dirname(target) 9 | 10 | if (fs.existsSync(target)) { 11 | unzipSync(target, join(__dirname, '../..')) 12 | return 13 | } 14 | 15 | if (fs.existsSync(tmpDir)) { 16 | fs.removeSync(tmpDir) 17 | } 18 | const stream = got.stream('https://github.com/toyobayashi/mishiro/releases/download/v1.0.0/mishiro-ex.zip', { 19 | headers: { 20 | 'User-Agent': 'mishiro' 21 | } 22 | }) 23 | 24 | if (!process.env.MISHIRO_NO_PROGRESS) { 25 | stream.on('downloadProgress', (progress) => { 26 | process.stdout.write(`\x1b[666D\x1b[0KDownload mishiro-ex.zip: ${(Math.floor(progress.percent * 10000) / 100).toFixed(2)}%`) 27 | }) 28 | } 29 | 30 | stream.on('error', (err) => { 31 | console.error(err) 32 | process.exit(1) 33 | }) 34 | 35 | fs.mkdirsSync(tmpDir) 36 | stream.pipe(fs.createWriteStream(target + '.tmp')) 37 | .on('error', (err) => { 38 | console.error(err) 39 | process.exit(1) 40 | }) 41 | .on('close', () => { 42 | console.log('\nbefore-deploy done.') 43 | fs.renameSync(target + '.tmp', target) 44 | unzipSync(target, join(__dirname, '../..')) 45 | fs.removeSync(tmpDir) 46 | }) 47 | })() 48 | -------------------------------------------------------------------------------- /app/script/release-download-count.ts: -------------------------------------------------------------------------------- 1 | import { get } from 'https' 2 | 3 | const repeat = (s: string, n: number): string => { 4 | if (n < 0) throw new Error('repeat(s, n < 0)') 5 | let str = '' 6 | for (let i = 0; i < n; i++) { 7 | str += s 8 | } 9 | return str 10 | } 11 | 12 | get({ 13 | host: 'api.github.com', 14 | path: '/repos/toyobayashi/mishiro/releases', 15 | headers: { 16 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' 17 | } 18 | }, (response) => { 19 | let body = '' 20 | response.on('data', (chunk: string) => { body += chunk }) 21 | response.on('end', () => { 22 | console.log('Release' + repeat(' ', 40 - 7) + 'Download Count\n') 23 | 24 | const res = JSON.parse(body) 25 | let total = 0 26 | let lineCount = 0 27 | try { 28 | for (const release of res) { 29 | for (const asset of release.assets) { 30 | total += asset.download_count as number 31 | const line = `${asset.name}${repeat(' ', 40 - asset.name.length)}${asset.download_count}` 32 | if (lineCount < 20) console.log(line) 33 | lineCount++ 34 | } 35 | } 36 | console.log(`\nTotal${repeat(' ', 40 - 5)}${total}`) 37 | } catch (err) { 38 | console.log(res) 39 | } 40 | }) 41 | response.on('error', err => console.log(err)) 42 | }) 43 | -------------------------------------------------------------------------------- /app/script/rm.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | const nativeModules = ['hca-decoder', 'sqlite3', 'mishiro-core', '@vscode/spdlog', 'usm-decrypter'] 5 | 6 | Promise.all(nativeModules.map(m => fs.remove(path.join(__dirname, '../node_modules', m)))).catch(e => console.log(e)) 7 | -------------------------------------------------------------------------------- /app/script/source-count.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | function countFile (filePath: string): number { 5 | const data = fs.readFileSync(filePath, 'utf8') 6 | const match: null | RegExpMatchArray = data.match(/\r?\n/g) 7 | if (match) return match.length + 1 8 | return 1 9 | } 10 | 11 | interface CountInfo { 12 | language?: string 13 | fileCount: number 14 | lineCount: number 15 | } 16 | 17 | function count (folderPath: string, resolve: string[], exclude?: RegExp): CountInfo[] { 18 | const result: CountInfo[] = [] 19 | for (const ext of resolve) { 20 | result.push({ language: ext, fileCount: 0, lineCount: 0 }) 21 | } 22 | function countFolder (folderPath: string, resolve: string[]): void { 23 | const list = fs.readdirSync(folderPath) 24 | for (const name of list) { 25 | const absPath = path.join(folderPath, name) 26 | if (!exclude || !exclude.test(absPath)) { 27 | if (fs.statSync(absPath).isFile()) { 28 | const extIndex = resolve.indexOf(path.parse(absPath).ext) 29 | if (extIndex !== -1) { 30 | result[extIndex].lineCount += countFile(absPath) 31 | result[extIndex].fileCount += 1 32 | } 33 | } else countFolder(absPath, resolve) 34 | } 35 | } 36 | } 37 | countFolder(folderPath, resolve) 38 | 39 | const total: CountInfo = { fileCount: 0, lineCount: 0 } 40 | for (const c of result) { 41 | total.fileCount += c.fileCount 42 | total.lineCount += c.lineCount 43 | } 44 | result.push(total) 45 | return result 46 | } 47 | 48 | const getPath = (r: string): string => path.join(__dirname, '..', r) 49 | 50 | console.log(count( 51 | getPath('.'), 52 | ['.ts', '.css', '.vue', '.json', '.html'], 53 | /node_modules|\.vscode|\.git|data|dist|cache|download|public\\.*\..+s|release|package-lock\.json/ 54 | )) 55 | -------------------------------------------------------------------------------- /app/script/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "resolveJsonModule": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/css/game.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | @font-face{ 6 | font-family:'CGSS-B'; 7 | src:url(../../asset/font.asar/FRB.otf) 8 | } 9 | .canvas { 10 | position: fixed; 11 | left: 50%; 12 | top: 50%; 13 | transform: translate(-50%, -50%); 14 | } 15 | 16 | .canvas-middle { 17 | width: 100%; 18 | } 19 | 20 | .canvas-center { 21 | height: 100%; 22 | } 23 | 24 | .img-center { 25 | position: fixed; 26 | left: 50%; 27 | top: 50%; 28 | transform: translate(-50%, -50%); 29 | height: 100%; 30 | z-index: -1000; 31 | } 32 | 33 | /* .hp-bar{ 34 | position: absolute; 35 | top: 5%; 36 | left: 10%; 37 | box-sizing: border-box; 38 | width: 231px; 39 | height: 28px; 40 | border: 1px solid #505050; 41 | background: #f0f0f0; 42 | border-radius: 5px; 43 | } 44 | .hp-bar>div{ 45 | position: relative; 46 | box-sizing: border-box; 47 | width: 215px; 48 | height: 20px; 49 | background: linear-gradient(180deg, #606060, #808080); 50 | border-radius: 4px; 51 | margin: 3px 7px; 52 | overflow: hidden; 53 | } 54 | .hp-bar .hp{ 55 | background: linear-gradient(180deg, #40d000, #b0f070 30%, #50d030); 56 | width: 80.5%; 57 | height: 20px; 58 | transition: width .2s linear; 59 | } 60 | .hp-bar .hp.warning{ 61 | background: linear-gradient(180deg, #f0c010, #f0e080 30%, #f0a000); 62 | } 63 | .hp-bar .hp.dangerous{ 64 | background: linear-gradient(180deg, #e04000, #f08070 30%, #c04000); 65 | } */ 66 | -------------------------------------------------------------------------------- /app/src/res/icon/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/1024x1024.png -------------------------------------------------------------------------------- /app/src/res/icon/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/128x128.png -------------------------------------------------------------------------------- /app/src/res/icon/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/16x16.png -------------------------------------------------------------------------------- /app/src/res/icon/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/24x24.png -------------------------------------------------------------------------------- /app/src/res/icon/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/256x256.png -------------------------------------------------------------------------------- /app/src/res/icon/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/32x32.png -------------------------------------------------------------------------------- /app/src/res/icon/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/48x48.png -------------------------------------------------------------------------------- /app/src/res/icon/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/512x512.png -------------------------------------------------------------------------------- /app/src/res/icon/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/64x64.png -------------------------------------------------------------------------------- /app/src/res/icon/app.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/app.icns -------------------------------------------------------------------------------- /app/src/res/icon/app.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/app/src/res/icon/app.ico -------------------------------------------------------------------------------- /app/src/ts/common/asar.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | (function () { 4 | const Module = require('module') 5 | const NODE_MODULES_PATH = join(__dirname, '../node_modules') 6 | const NODE_MODULES_ASAR_PATH = NODE_MODULES_PATH + '.asar' 7 | 8 | const originalResolveLookupPaths = Module._resolveLookupPaths 9 | 10 | Module._resolveLookupPaths = originalResolveLookupPaths.length === 2 11 | ? function (request: any, parent: any) { 12 | const result = originalResolveLookupPaths(request, parent) 13 | 14 | if (!result) return result 15 | 16 | for (let i = 0; i < result.length; i++) { 17 | if (result[i] === NODE_MODULES_PATH) { 18 | result.splice(i, 0, NODE_MODULES_ASAR_PATH) 19 | break 20 | } 21 | } 22 | 23 | return result 24 | } 25 | : function (request: any, parent: any, newReturn: any) { 26 | const result = originalResolveLookupPaths(request, parent, newReturn) 27 | 28 | const paths = newReturn ? result : result[1] 29 | for (let i = 0; i < paths.length; i++) { 30 | if (paths[i] === NODE_MODULES_PATH) { 31 | paths.splice(i, 0, NODE_MODULES_ASAR_PATH) 32 | break 33 | } 34 | } 35 | 36 | return result 37 | } 38 | })() 39 | -------------------------------------------------------------------------------- /app/src/ts/common/get-path.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | 3 | export interface GetPath { 4 | (...relative: string[]): string 5 | configPath: string 6 | logPath: string 7 | dataDir: (...relative: string[]) => string 8 | manifestPath: (resVer: number, db?: string) => string 9 | masterPath: (resVer: number, db?: string) => string 10 | downloadDir: (...relative: string[]) => string 11 | iconDir: (...relative: string[]) => string 12 | emblemDir: (...relative: string[]) => string 13 | cardDir: (...relative: string[]) => string 14 | scoreDir: (...relative: string[]) => string 15 | voiceDir: (...relative: string[]) => string 16 | bgmDir: (...relative: string[]) => string 17 | bgmAsarDir: (...relative: string[]) => string 18 | liveDir: (...relative: string[]) => string 19 | jacketDir: (...relative: string[]) => string 20 | batchDir: (...relative: string[]) => string 21 | } 22 | 23 | const appRoot: string = process.type === 'browser' ? join(__dirname, '..') : require('electron').ipcRenderer.sendSync('appRoot') 24 | 25 | if (process.type === 'browser') { 26 | require('electron').ipcMain.on('appRoot', (event) => { 27 | event.returnValue = appRoot 28 | }) 29 | } 30 | 31 | const getPath: GetPath = function getPath (...relative: string[]): string { 32 | return join(appRoot, ...relative) 33 | } 34 | 35 | getPath.configPath = getPath('../config.json') 36 | getPath.logPath = getPath('../log.txt') 37 | getPath.dataDir = (...relative) => getPath('../asset/data', ...relative) 38 | getPath.manifestPath = (resVer, db = '') => getPath.dataDir(`manifest_${resVer}${db}`) 39 | getPath.masterPath = (resVer, db = '') => getPath.dataDir(`master_${resVer}${db}`) 40 | getPath.downloadDir = (...relative) => getPath('../asset/download', ...relative) 41 | getPath.iconDir = (...relative) => getPath('../asset/icon', ...relative) 42 | getPath.emblemDir = (...relative) => getPath('../asset/emblem', ...relative) 43 | getPath.cardDir = (...relative) => getPath('../asset/card', ...relative) 44 | getPath.scoreDir = (...relative) => getPath('../asset/score', ...relative) 45 | getPath.voiceDir = (...relative) => getPath('../asset/voice', ...relative) 46 | getPath.bgmDir = (...relative) => getPath('../asset/bgm', ...relative) 47 | getPath.bgmAsarDir = (...relative) => getPath('../asset/bgm.asar', ...relative) 48 | getPath.liveDir = (...relative) => getPath('../asset/live', ...relative) 49 | getPath.jacketDir = (...relative) => getPath('../asset/jacket', ...relative) 50 | getPath.batchDir = (...relative) => getPath('../asset/batch', ...relative) 51 | 52 | export default getPath 53 | -------------------------------------------------------------------------------- /app/src/ts/common/util.ts: -------------------------------------------------------------------------------- 1 | export function formatSize (b: number): string { 2 | if (b < 1024) return `${b} B` 3 | if (b < 1024 * 1024) return `${Math.floor(b / 1024)} KB` 4 | if (b < 1024 * 1024 * 1024) return `${Math.floor(b / 1024 / 1024 * 100) / 100} MB` 5 | if (b < 1024 * 1024 * 1024 * 1024) return `${Math.floor(b / 1024 / 1024 / 1024 * 100) / 100} GB` 6 | return `${b}` 7 | } 8 | -------------------------------------------------------------------------------- /app/src/ts/i18n/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | i18n: { 3 | chinese: '中文', 4 | japanese: '日本語', 5 | english: 'English' 6 | }, 7 | home: { 8 | play: '播放', 9 | pause: '暫停', 10 | search: '捜索', 11 | download: '下載', 12 | opendir: '打開目録', 13 | errorTitle: '錯誤警告', 14 | hope: '敬請期待!', 15 | noEmptyString: '請輸入査詢関鍵字。', 16 | noEmptyDownload: '請選択文件。', 17 | noTask: '現在没有正在下載的任務。', 18 | noNetwork: '網絡未連接。', 19 | close: '関閉', 20 | stop: '中止', 21 | input: '捜索数据庫', 22 | resVer: '資源版本', 23 | downloadFailed: '下載失敗。請在網絡良好的環境下重試。', 24 | msg: '消息', 25 | stmnFull: '体力已回満。', 26 | all: '全部', 27 | canDownload: '未下載', 28 | autoDecLz4: '自動LZ4解圧', 29 | cName: '文件名', 30 | cHash: '哈希値', 31 | cSize: '文件大小', 32 | usmbtn: '選択USM', 33 | accountBannedMessage: '客戸端内置帳号已被封禁。請打開選項並設置另一個帳号来与遊戯服務器進行通信。' 34 | }, 35 | event: { 36 | atapon: '消耗道具型', 37 | caravan: '灰姑娘篷車', 38 | medley: '組曲串焼型', 39 | party: '五人組隊型', 40 | tour: '全国巡回型', 41 | calculate: '計算', 42 | startCount: '開始計時', 43 | stopCount: '停止計時', 44 | plv: '等級', 45 | stamina: '体力値', 46 | exp: '経験値', 47 | itemNumber: '道具数', 48 | currentPt: '当前pt', 49 | targetPt: '目標pt', 50 | commonTimes: '通常曲倍数', 51 | commonDifficulty: '通常曲難度', 52 | eventTimes: '活動曲倍数', 53 | eventDifficulty: '活動曲難度', 54 | levelUp: '可昇級次数', 55 | requirePt: '需要pt', 56 | requireItem: '需要道具', 57 | requireStamina: '需要回復体力', 58 | commonLiveTimes: '通常曲次数', 59 | eventLiveTimes: '活動曲次数', 60 | gameTime: '需要時間', 61 | extraStamina: '額外体力回復', 62 | currentMedal: '当前勲章数', 63 | targetMedal: '目標勲章数', 64 | starRank: '領隊星級', 65 | extraRewardOdds: '0/1/2/3/4槽概率', 66 | averageMedal: '平均毎回勲章数', 67 | requireMedal: '需要勲章数', 68 | hakoyureLevel: '箱揺Lv', 69 | smoke: 'スモーク', 70 | laser: 'レーザー', 71 | hanabi: '花火', 72 | currentAudience: '当前観客人数', 73 | targetAudience: '目標観客人数', 74 | areaStamina: '地区体力', 75 | liveOption: 'Live選項', 76 | requireAudience: '需要観客人数', 77 | timeLeft: '剰余時間 ', 78 | bonusItem: '奨励道具', 79 | bonusStamina: '奨励体力', 80 | cardRewardOdds: '上位/下位估計' 81 | }, 82 | update: { 83 | tip: '※請在網絡良好的環境下進行', 84 | check: '正在検測資源版本 ', 85 | manifest: '正在下載資源表単 ', 86 | master: '正在下載主数据庫 ' 87 | }, 88 | idol: { 89 | before: '特訓前', 90 | after: '特訓後', 91 | input: '請輸入偶像名或編号進行査詢', 92 | okurigana: '注音假名', 93 | name: '名字', 94 | age: '年齢', 95 | height: '身高', 96 | weight: '体重', 97 | birth: '生日', 98 | blood: '血型', 99 | handedness: '慣用手', 100 | threesize: '三囲', 101 | hometown: '籍貫', 102 | favorite: '愛好', 103 | constellation: '星座', 104 | voice: '声優', 105 | id: '卡片編号', 106 | card_name: '卡片名字', 107 | chara_id: '角色編号', 108 | rarity: '稀有度', 109 | hp: '生命値', 110 | vocal: '唱功', 111 | dance: '舞姿', 112 | visual: '外表', 113 | solo_live: '独唱曲', 114 | skill_name: '特技', 115 | skill_explain: '特技詳細', 116 | leader_skill_name: '領隊効果', 117 | leader_skill_explain: '領隊効果詳細', 118 | nashi: '无', 119 | limited: '限定', 120 | event: '活動', 121 | gacha: '転蛋', 122 | voiceBtn: '語音', 123 | noVoice: '這個角色没有語音。' 124 | }, 125 | live: { 126 | decoding: ' 解碼中...', 127 | input: '按曲名捜索', 128 | live: 'LIVE', 129 | score: '譜面', 130 | noScore: '這首曲子没有譜面。', 131 | noAudio: '未発現MP3文件。', 132 | start: '開始', 133 | gameRunning: 'LIVE正在進行。', 134 | liveResult: 'LIVE成績', 135 | copy: '複製', 136 | noLyrics: '無歌詞', 137 | export: '導出', 138 | exportDir: '選択目録' 139 | }, 140 | gacha: { 141 | ikkai: '単抽', 142 | jukkai: '十連', 143 | history: '歴史記録', 144 | information: '転蛋詳細', 145 | clear: '清除', 146 | ko: '个', 147 | name: '名称', 148 | id: '編号', 149 | discription: '説明', 150 | startDate: '開始時間', 151 | endDate: '結束時間', 152 | cost: '消耗星鑽', 153 | r: 'R', 154 | sr: 'SR', 155 | ssr: 'SSR', 156 | get: '已獲得', 157 | failed: '獲取数据失敗。' 158 | }, 159 | menu: { 160 | option: '選項', 161 | about: '関於', 162 | save: '保存', 163 | relaunch: '重新啓動', 164 | loopCount: '音頻循環次数', 165 | resVer: '資源版本', 166 | gacha: '転蛋ID', 167 | event: '活動ID', 168 | background: '背景', 169 | account: '帳号', 170 | license: '権利許可', 171 | lang: '語言', 172 | update: '更新', 173 | noUpdate: '当前版本已経是最新版本。', 174 | release: '獲得最新版本', 175 | version: '最新版本', 176 | commit: '最新提交', 177 | eventPlacehoder: '参照BGM文件名: ', 178 | backPlacehoder: '偶像卡片ID', 179 | var: '利用規約', 180 | varCon: '通過本軟件成功提取出来的資源版権帰BANDAI NAMCO Entertainment Inc.所有,請勿用於商業用途,如有因此引発的任何法律問題,一切与本軟件作者無関,後果自負。', 181 | appname: '応用名', 182 | appver: '版本', 183 | description: '説明', 184 | descCon: '用於提取CGSS (IDOLMASTER CINDERELLA GIRLS STARLIGHT STAGE) 音楽和図片資源的卓面応用。', 185 | exit: '結束程序', 186 | calculator: '活動估分', 187 | cacheClear: '清除緩存', 188 | cacheClearSuccess: '清除緩存成功。', 189 | noCache: '無需清除緩存。', 190 | commitHash: '提交', 191 | commitDate: '日期', 192 | getCardFrom: '従何処獲取卡面', 193 | official: '官方', 194 | kirara: 'kirara', 195 | arch: '架構', 196 | batchStart: '下載全部', 197 | batchStop: '中止', 198 | batchErrorName: '文件名', 199 | batchErrorMsg: '錯誤信息', 200 | batchErrorCode: '錯誤碼', 201 | proxy: '代理', 202 | lrcEncoding: '歌詞字符集', 203 | audioExport: '音頻格式' 204 | }, 205 | commu: { 206 | id: '請輸入ID', 207 | notfound: '請輸入正確的ID', 208 | newVersion: '有新的資源版本,請重启mishiro', 209 | originalSite: '原始网站' 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /app/src/ts/main/config.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import getPath from '../common/get-path' 3 | 4 | export interface MishiroConfig { 5 | latestResVer?: number 6 | loopCount?: number 7 | resVer?: number 8 | gacha?: number 9 | event?: number 10 | language?: 'zh' | 'ja' | 'en' 11 | background?: number 12 | account?: string 13 | proxy?: string 14 | card?: 'default' | 'kirara' 15 | lrcEncoding?: 'utf8' | 'Windows932' | 'Windows936' 16 | audioExport?: 'wav' | 'mp3' | 'aac' 17 | } 18 | 19 | export type MishiroConfigKey = keyof MishiroConfig 20 | 21 | export class Configurer { 22 | private readonly configFile: string 23 | private config: MishiroConfig 24 | constructor (configFile: string) { 25 | this.configFile = configFile 26 | if (!fs.existsSync(configFile)) { 27 | this.config = { 28 | latestResVer: 10088500, 29 | language: 'zh', 30 | card: 'default', 31 | lrcEncoding: 'utf8', 32 | audioExport: 'wav' 33 | } 34 | } else { 35 | this.config = fs.readJsonSync(configFile) || {} 36 | this.config.latestResVer = this.config.latestResVer || 10088500 37 | this.config.language = this.config.language || 'zh' 38 | this.config.card = this.config.card || 'default' 39 | this.config.lrcEncoding = this.config.lrcEncoding || 'utf8' 40 | this.config.audioExport = this.config.audioExport || 'wav' 41 | } 42 | fs.writeJsonSync(configFile, this.config, { spaces: 2 }) 43 | } 44 | 45 | getAll (): MishiroConfig { 46 | return this.config 47 | } 48 | 49 | get (key: K): MishiroConfig[K] { 50 | return this.config[key] 51 | } 52 | 53 | set (obj: MishiroConfig): void 54 | set (obj: K | MishiroConfig, value: MishiroConfig[K]): void 55 | set (obj: K | MishiroConfig, value?: MishiroConfig[K]): void { 56 | if (typeof obj === 'string') { 57 | this.config[obj] = value 58 | fs.writeJsonSync(this.configFile, this.config, { spaces: 2 }) 59 | } else { 60 | for (const k in obj) { 61 | const mishiroConfigKey = k as MishiroConfigKey 62 | if (obj[mishiroConfigKey]) { 63 | (this.config as any)[mishiroConfigKey] = obj[mishiroConfigKey] 64 | } else { 65 | if (this.config[mishiroConfigKey] !== undefined) { 66 | delete this.config[mishiroConfigKey] 67 | } 68 | } 69 | } 70 | fs.writeJsonSync(this.configFile, this.config, { spaces: 2 }) 71 | } 72 | } 73 | 74 | remove (key: MishiroConfigKey): void { 75 | if (this.config[key] !== undefined) { 76 | delete this.config[key] 77 | fs.writeJsonSync(this.configFile, this.config, { spaces: 2 }) 78 | } 79 | } 80 | } 81 | 82 | const configurer = new Configurer(getPath.configPath) 83 | 84 | export default configurer 85 | -------------------------------------------------------------------------------- /app/src/ts/main/core.ts: -------------------------------------------------------------------------------- 1 | import configurer from './config' 2 | import * as core from 'mishiro-core' 3 | 4 | const confver = configurer.get('latestResVer') 5 | const confacc = configurer.get('account') 6 | 7 | const client = new core.Client( 8 | confacc || '::', 9 | confver !== undefined ? (confver/* - 100 */).toString() : undefined 10 | ) 11 | 12 | client.setProxy(configurer.get('proxy') ?? '') 13 | 14 | if (!client.user) { 15 | client.user = '940464243' 16 | client.viewer = '174481488' 17 | client.udid = 'cf608be5-6d38-421a-8eb1-11a501132c0a' 18 | } 19 | 20 | export * from 'mishiro-core' 21 | 22 | export { client } 23 | -------------------------------------------------------------------------------- /app/src/ts/main/get-gacha-data.ts: -------------------------------------------------------------------------------- 1 | import type { MishiroConfig } from '../main/config' 2 | 3 | export default function (gachaAll: any[], config: MishiroConfig, now: number, timeOffset: number): { 4 | gachaNow: any 5 | gachaData: any 6 | } { 7 | gachaAll.sort((a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime()) 8 | 9 | const gachaNowArray = [] 10 | 11 | for (let i = 0; i < gachaAll.length; i++) { 12 | const gacha = gachaAll[i] 13 | if (gacha.id > 30000 && gacha.id < 40000) { 14 | const start = new Date(gacha.start_date).getTime() - timeOffset 15 | const end = new Date(gacha.end_date).getTime() - timeOffset 16 | if (now >= start && now <= end) gachaNowArray.push(gacha) 17 | } 18 | } 19 | if (!gachaNowArray.length) { 20 | for (let i = 0; i < gachaAll.length; i++) { 21 | if (gachaAll[i].id > 30000 && gachaAll[i].id < 40000) { 22 | gachaNowArray.push(gachaAll[i]) 23 | break 24 | } 25 | } 26 | } else gachaNowArray.sort((a, b) => a.id - b.id) 27 | 28 | // let gachaNowArray = master._exec("SELECT * FROM gacha_data WHERE start_date = (SELECT MAX(start_date) FROM gacha_data WHERE id LIKE '3%') AND id LIKE '3%'") 29 | const gachaNow = gachaNowArray[gachaNowArray.length - 1] 30 | let gachaData = null 31 | 32 | if (config && config.gacha) { 33 | for (let i = 0; i < gachaAll.length; i++) { 34 | if (config.gacha === Number(gachaAll[i].id)) { 35 | gachaData = gachaAll[i] 36 | break 37 | } 38 | } 39 | } else gachaData = gachaNow 40 | 41 | return { 42 | gachaNow, 43 | gachaData 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/src/ts/main/icon.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindowConstructorOptions, nativeImage, app } from 'electron' 2 | import { join } from 'path' 3 | import { existsSync } from 'fs-extra' 4 | 5 | export default function setIcon (options: BrowserWindowConstructorOptions): BrowserWindowConstructorOptions { 6 | if (process.platform === 'linux') { 7 | let linuxIcon: string 8 | try { 9 | linuxIcon = join(__dirname, '../../icon/1024x1024.png') 10 | } catch (_) { 11 | linuxIcon = '' 12 | } 13 | if (linuxIcon) { 14 | options.icon = nativeImage.createFromPath(join(__dirname, linuxIcon)) 15 | } 16 | } else { 17 | if (process.env.NODE_ENV !== 'production') { 18 | let icon: string = '' 19 | 20 | const iconPath = join(__dirname, `../../../src/res/icon/${process.platform === 'darwin' ? '1024x1024.png' : 'app.ico'}`) 21 | if (existsSync(iconPath)) icon = iconPath 22 | 23 | if (icon) { 24 | if (process.platform === 'darwin') { 25 | app.dock.setIcon(icon) 26 | } else { 27 | options.icon = nativeImage.createFromPath(icon) 28 | } 29 | } 30 | } 31 | } 32 | return options 33 | } 34 | -------------------------------------------------------------------------------- /app/src/ts/main/log.ts: -------------------------------------------------------------------------------- 1 | import * as spdlog from '@vscode/spdlog' 2 | import * as fs from 'fs-extra' 3 | import { dirname } from 'path' 4 | import getPath from '../common/get-path' 5 | 6 | let logger: spdlog.Logger 7 | 8 | function init (): void { 9 | if (!logger) { 10 | fs.mkdirsSync(dirname(getPath.logPath)) 11 | logger = new spdlog.Logger('rotating', 'mishiro', getPath.logPath, 1024 * 1024 * 5, 3) 12 | logger.setLevel(2) 13 | } 14 | } 15 | 16 | export function info (msg: string): void { 17 | init() 18 | logger.info(msg) 19 | } 20 | 21 | export function warn (msg: string): void { 22 | init() 23 | logger.warn(msg) 24 | } 25 | 26 | export function error (msg: string): void { 27 | init() 28 | logger.error(msg) 29 | } 30 | 31 | export function critical (msg: string): void { 32 | init() 33 | logger.critical(msg) 34 | } 35 | -------------------------------------------------------------------------------- /app/src/ts/main/on-check-score.ts: -------------------------------------------------------------------------------- 1 | // import { openSqlite } from './sqlite3' 2 | // import { Event } from 'electron' 3 | import DB from '../common/db' 4 | 5 | // export default async function (event: Event, objectId: string, scoreFile: string) { 6 | // let bdb = await openSqlite(scoreFile) 7 | // let rows = await bdb._all(`SELECT data FROM blobs WHERE name LIKE "%/__.csv" ESCAPE '/'`) 8 | // bdb.close() 9 | // if (rows.length === 5) event.sender.send(objectId, true) 10 | // else event.sender.send(objectId, false) 11 | // } 12 | 13 | export default async function getScoreDifficulties (scoreFile: string): Promise { 14 | const bdb = new DB(scoreFile) 15 | const rows = await bdb.find('blobs', ['name'], { name: { $like: ['%/__.csv', '/'] } }) 16 | await bdb.close() 17 | 18 | const type = [undefined, 'Debut', 'Regular', 'Pro', 'Master', 'Master+'] 19 | const res: any = {} 20 | for (let i = 0; i < rows.length; i++) { 21 | const t: string | undefined = type[Number(rows[i].name.slice(-5)[0])] 22 | if (t) { 23 | res[t] = type.indexOf(t).toString() 24 | } 25 | } 26 | return res 27 | } 28 | -------------------------------------------------------------------------------- /app/src/ts/main/on-game.ts: -------------------------------------------------------------------------------- 1 | // import { openSqlite } from './sqlite3' 2 | // import * as path from 'path' 3 | // import { Event } from 'electron' 4 | 5 | // function createScore (csv: string, bpm: number) { 6 | // let csvTable: any = csv.split('\n') 7 | // for (let i = 0; i < csvTable.length; i++) { 8 | // csvTable[i] = csvTable[i].split(',') 9 | // csvTable[i][1] = Math.round(csvTable[i][1] / (60 / bpm) * 1000) / 1000 10 | // if (i > 0) { 11 | // csvTable[i][2] = Number(csvTable[i][2]) 12 | // if (csvTable[i][2] !== 1 && csvTable[i][2] !== 2) { 13 | // csvTable.splice(i, 1) 14 | // i-- 15 | // } 16 | // } 17 | // } 18 | 19 | // let score = [] 20 | // for (let i = 1; i < csvTable.length; i++) { 21 | // let note = [csvTable[i][1], Number(csvTable[i][4])] 22 | // if (csvTable[i][2] === 2) { 23 | // let j = i + 1 24 | // for (j = i + 1; j < csvTable.length; j++) { 25 | // if (Number(csvTable[j][4]) === Number(csvTable[i][4])) { 26 | // let length = csvTable[j][1] - csvTable[i][1] 27 | // note.push(length) 28 | // csvTable.splice(j, 1) 29 | // score.push(note) 30 | // break 31 | // } 32 | // } 33 | // } else { 34 | // score.push(note) 35 | // } 36 | // } 37 | 38 | // return score 39 | // } 40 | 41 | // export default async function (event: Event, scoreFile: string, difficulty: number | string, bpm: number, src: string) { 42 | // let bdb = await openSqlite(scoreFile) 43 | // let rows = await bdb._all('SELECT name, data FROM blobs') 44 | // bdb.close() 45 | // let name = path.parse(scoreFile).name.split('_') 46 | // let musicscores = name[0] 47 | // let mxxx = name[1] 48 | 49 | // let matchedIdArr = name[1].match(/[0-9]+$/) 50 | // let id = matchedIdArr ? Number(matchedIdArr[0]) : void 0 51 | 52 | // let nameField = `${musicscores}/${mxxx}/${id}_${difficulty}.csv` 53 | 54 | // let data = rows.filter((row: any) => row.name === nameField)[0].data.toString() 55 | 56 | // let score = createScore(data, bpm) 57 | 58 | // let fullCombo = 0 59 | // for (let i = 0; i < score.length; i++) { 60 | // fullCombo += score[i][2] ? 2 : 1 61 | // } 62 | // let obj = { src, bpm, score, fullCombo } 63 | // event.sender.send('game', obj) 64 | // } 65 | -------------------------------------------------------------------------------- /app/src/ts/main/on-lyrics.ts: -------------------------------------------------------------------------------- 1 | // import { openSqlite } from './sqlite3' 2 | // import * as path from 'path' 3 | // import { Event } from 'electron' 4 | import { parse } from 'path' 5 | import DB from '../common/db' 6 | // export default async function (event: Event, scoreFile: string) { 7 | // let bdb = await openSqlite(scoreFile) 8 | // let rows = await bdb._all('SELECT name, data FROM blobs') 9 | // bdb.close() 10 | // let name = path.parse(scoreFile).name.split('_') 11 | // let musicscores = name[0] 12 | // let mxxx = name[1] 13 | 14 | // let nameField = `${musicscores}/${mxxx}/${mxxx}_lyrics.csv` 15 | // let data = rows.filter((row: any) => row.name === nameField)[0].data.toString() 16 | // const list = data.split('\n') 17 | // const lyrics = [] 18 | // for (let i = 1; i < list.length - 1; i++) { 19 | // const line = list[i].split(',') 20 | // lyrics.push({ 21 | // time: Number(line[0]), 22 | // lyrics: line[1], 23 | // size: line[2] 24 | // }) 25 | // } 26 | // event.sender.send('lyrics', lyrics) 27 | // } 28 | 29 | export interface Lyric { 30 | time: number 31 | lyrics: string 32 | size: number 33 | } 34 | 35 | export default async function getLyrics (scoreFile: string): Promise { 36 | const bdb = new DB(scoreFile) 37 | const rows = await bdb.find<{ name: string, data: Buffer | string }>('blobs', ['name', 'data']) 38 | await bdb.close() 39 | const name = parse(scoreFile).name.split('_') 40 | const musicscores = name[0] 41 | const mxxx = name[1] 42 | 43 | const nameField = `${musicscores}/${mxxx}/${mxxx}_lyrics.csv` 44 | const data = rows.filter((row: any) => row.name === nameField)[0].data.toString() 45 | const list = data.split('\n') 46 | const lyrics: Lyric[] = [] 47 | for (let i = 1; i < list.length - 1; i++) { 48 | const line = list[i].split(',') 49 | lyrics.push({ 50 | time: Number(line[0]), 51 | lyrics: line[1], 52 | size: Number(line[2]) 53 | }) 54 | } 55 | return lyrics 56 | } 57 | -------------------------------------------------------------------------------- /app/src/ts/main/on-score.ts: -------------------------------------------------------------------------------- 1 | import DB from '../common/db' 2 | // import { openSqlite } from './sqlite3' 3 | import { ipcMain } from 'electron' 4 | 5 | export interface ScoreNote { 6 | sec: number // music time 7 | type: 1 | 2 | 3 // 1: tap / flip 2: hold 3: hold + move 8 | finishPos: 1 | 2 | 3 | 4 | 5 9 | status: 0 | 1 | 2 // 0: tap 1: flip left 2: flip right 10 | sync: 0 | 1 // operate at the same time 11 | groupId: number // group id 12 | } 13 | 14 | function createScore (csv: string): { 15 | fullCombo: number 16 | score: ScoreNote[] 17 | } { 18 | const csvTable: string[] = csv.split('\n') 19 | const fullCombo = csvTable[1].split(',').map(value => Number(value))[5] 20 | const score: ScoreNote[] = [] 21 | 22 | for (let i = 2; i < csvTable.length; i++) { 23 | const noteArr: any[] = csvTable[i].split(',').map(value => Number(value)) 24 | if (noteArr[2] !== 1 && noteArr[2] !== 2 && noteArr[2] !== 3) { 25 | continue 26 | } 27 | score.push({ 28 | sec: noteArr[1], 29 | type: noteArr[2], 30 | finishPos: noteArr[4], 31 | status: noteArr[5], 32 | sync: noteArr[6], 33 | groupId: noteArr[7] 34 | }) 35 | } 36 | 37 | return { fullCombo, score } 38 | } 39 | 40 | let song: { src: string, bpm: number, score: ScoreNote[], fullCombo: number, difficulty: string } | null = null 41 | 42 | ipcMain.on('getSong', (event) => { 43 | const sync = song 44 | song = null 45 | event.returnValue = sync 46 | }) 47 | 48 | const difficultyMap: any = { 49 | 1: 'DEBUT', 50 | 2: 'REGULAR', 51 | 3: 'PRO', 52 | 4: 'MASTER', 53 | 5: 'MASTER+' 54 | } 55 | 56 | // export default async function (event: Event, scoreFile: string, difficulty: number | string, bpm: number, src: string) { 57 | // let bdb = await openSqlite(scoreFile) 58 | // let rows = await bdb._all(`SELECT data FROM blobs WHERE name LIKE "%/_${difficulty}.csv" ESCAPE '/'`) 59 | // bdb.close() 60 | // if (!rows.length) return 61 | 62 | // const data = rows[0].data.toString() 63 | 64 | // let { fullCombo, score } = createScore(data) 65 | 66 | // song = { src, bpm, score, fullCombo, difficulty: difficultyMap[difficulty] } 67 | // event.sender.send('score') 68 | // } 69 | 70 | export default async function getScore (scoreFile: string, difficulty: number | string, bpm: number, src: string): Promise { 71 | const bdb = new DB(scoreFile) 72 | const rows = await bdb.find('blobs', ['data'], { name: { $like: [`%/_${difficulty}.csv`, '/'] } }) 73 | await bdb.close() 74 | if (!rows.length) return false 75 | 76 | const data = rows[0].data.toString() 77 | 78 | const { fullCombo, score } = createScore(data) 79 | 80 | song = { src, bpm, score, fullCombo, difficulty: difficultyMap[difficulty] } 81 | return true 82 | } 83 | -------------------------------------------------------------------------------- /app/src/ts/main/open-score-window.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { format } from 'url' 3 | import getPath from '../common/get-path' 4 | import setIcon from './icon' 5 | import { error } from './log' 6 | 7 | let win: BrowserWindow | null = null 8 | 9 | export default function openScoreWindow (onClose?: () => void): void { 10 | if (win !== null) { 11 | return 12 | } 13 | 14 | win = new BrowserWindow(setIcon({ 15 | width: 1296, 16 | height: 759, 17 | // minWidth: 1296, 18 | // minHeight: 759, 19 | // maxWidth: 1296, 20 | // maxHeight: 759, 21 | show: false, 22 | backgroundColor: '#000000', 23 | webPreferences: { 24 | nodeIntegration: true, 25 | // enableRemoteModule: false, 26 | contextIsolation: false 27 | } 28 | })) 29 | 30 | win.once('ready-to-show', () => { 31 | win?.show() 32 | }) 33 | 34 | if (process.env.NODE_ENV === 'production') { 35 | win.loadURL(format({ 36 | pathname: getPath('./renderer/score.html'), 37 | protocol: 'file:', 38 | slashes: true 39 | })).catch(err => { 40 | console.error(err) 41 | error(`Score window load failed: ${err.stack}`) 42 | }) 43 | } else { 44 | win.loadURL(`http://localhost:${MISHIRO_DEV_SERVER_PORT}/app/renderer/score.html`).catch(err => console.error(err)) 45 | } 46 | 47 | win.on('close', () => { 48 | if (typeof onClose === 'function') { 49 | onClose() 50 | } 51 | win = null 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /app/src/ts/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": [ 4 | "./**/*", 5 | "../main.ts", 6 | "../typings/**/*", 7 | "../common/db.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /app/src/ts/main/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | // import { Configurer } from '../config' 2 | // import { Client } from 'mishiro-core' 3 | 4 | // declare global { 5 | // export const configurer: Configurer 6 | // export const client: Client 7 | // export const getPath: typeof import('../get-path').default 8 | // export const updater: import('electron-github-asar-updater') 9 | // } 10 | -------------------------------------------------------------------------------- /app/src/ts/main/updater.ts: -------------------------------------------------------------------------------- 1 | import { ipcMain } from 'electron' 2 | import * as Updater from 'electron-github-asar-updater' 3 | import configurer from './config' 4 | 5 | const updater = new Updater('toyobayashi/mishiro', 'resources') 6 | updater.setProxy(configurer.get('proxy') ?? '') 7 | 8 | export function updaterIpc (): void { 9 | ipcMain.on('updater#relaunch', (event) => { 10 | updater.relaunch() 11 | event.returnValue = undefined 12 | }) 13 | 14 | ipcMain.on('updater#getUpdateInfo', (event) => { 15 | event.returnValue = updater.getUpdateInfo() 16 | }) 17 | 18 | ipcMain.on('updater#abort', (event) => { 19 | updater.abort() 20 | event.returnValue = undefined 21 | }) 22 | 23 | ipcMain.handle('updater#check', async (_event, options) => { 24 | return updater.check(options) 25 | }) 26 | 27 | ipcMain.handle('updater#download', async (event) => { 28 | return updater.download((progress) => { 29 | event.sender.send('updater#onDownloadProgress', progress) 30 | }) 31 | }) 32 | } 33 | 34 | export function setUpdaterProxy (proxy: string): void { 35 | updater.setProxy(proxy) 36 | } 37 | -------------------------------------------------------------------------------- /app/src/ts/renderer-back.ts: -------------------------------------------------------------------------------- 1 | import './renderer/preload' 2 | import DB from './common/db' 3 | import { batchDownload, batchStop, getBatchErrorList, setDownloaderProxy } from './renderer/back/batch-download' 4 | // import mainWindowId from './renderer/back/main-window-id' 5 | import readMaster from './renderer/back/on-master-read' 6 | 7 | let manifest: DB | null = null 8 | let master: DB | null = null 9 | 10 | window.node.electron.ipcRenderer.on('port', e => { 11 | const mainWindowPort = e.ports[0] 12 | const portEvent = new window.node.events.EventEmitter() 13 | 14 | mainWindowPort.onmessage = function (event) { 15 | portEvent.emit('message', event) 16 | } 17 | 18 | function defineRemoteFunction (name: string, fn: (...args: any[]) => any): void { 19 | portEvent.on('message', (event) => { 20 | if (event.data.type === name) { 21 | Promise.resolve(fn(...event.data.payload)).then(ret => { 22 | mainWindowPort.postMessage({ 23 | id: event.data.id, 24 | err: null, 25 | data: ret 26 | }) 27 | }).catch(err => { 28 | mainWindowPort.postMessage({ 29 | id: event.data.id, 30 | err: err.message, 31 | data: undefined 32 | }) 33 | }) 34 | } 35 | }) 36 | } 37 | 38 | defineRemoteFunction('openManifestDatabase', async (path: string) => { 39 | if (manifest) { 40 | return 41 | } 42 | manifest = await DB.open(path) 43 | }) 44 | 45 | defineRemoteFunction('openMasterDatabase', async (path: string) => { 46 | if (master) { 47 | return 48 | } 49 | master = await DB.open(path) 50 | }) 51 | 52 | defineRemoteFunction('getMasterHash', async () => { 53 | const masterHash = (await manifest!.find('manifests', ['name', 'hash'], { name: 'master.mdb' }))[0].hash as string 54 | return masterHash 55 | }) 56 | 57 | defineRemoteFunction('readMasterData', async (masterFile: string) => { 58 | master = await DB.open(masterFile) 59 | const masterData = await readMaster(master, manifest!) 60 | await master.close() 61 | master = null 62 | return masterData 63 | }) 64 | 65 | defineRemoteFunction('getCardHash', async (id: string | number) => { 66 | const res = await manifest!.find('manifests', ['hash'], { name: `card_bg_${id}.unity3d` }) 67 | return res[0].hash 68 | }) 69 | 70 | defineRemoteFunction('getIconHash', async (id: string | number) => { 71 | const res = await manifest!.findOne('manifests', ['hash'], { name: `card_${id}_m.unity3d` }) 72 | return res.hash 73 | }) 74 | 75 | defineRemoteFunction('getEmblemHash', async (id: string | number) => { 76 | const res = await manifest!.findOne('manifests', ['hash'], { name: `emblem_${id}_l.unity3d` }) 77 | return res.hash 78 | }) 79 | 80 | defineRemoteFunction('searchResources', async (queryString: string) => { 81 | const res = await manifest!.find<{ name: string, hash: string }>('manifests', ['name', 'hash', 'size'], { name: { $like: `%${queryString.trim()}%` } }) 82 | return res 83 | }) 84 | 85 | let batchDownloading = false 86 | 87 | defineRemoteFunction('startBatchDownload', async () => { 88 | batchDownloading = true 89 | await batchDownload(mainWindowPort, manifest!) 90 | return batchDownloading 91 | }) 92 | 93 | defineRemoteFunction('stopBatchDownload', async () => { 94 | await batchStop() 95 | batchDownloading = false 96 | return batchDownloading 97 | }) 98 | 99 | defineRemoteFunction('getBatchErrorList', () => { 100 | const list = getBatchErrorList() 101 | return list 102 | }) 103 | 104 | defineRemoteFunction('setDownloaderProxy', (proxy: string) => { 105 | setDownloaderProxy(proxy) 106 | }) 107 | }) 108 | 109 | window.addEventListener('beforeunload', () => { 110 | manifest?.close().catch(err => { 111 | console.error(err) 112 | }) 113 | manifest = null 114 | }) 115 | -------------------------------------------------------------------------------- /app/src/ts/renderer-game.ts: -------------------------------------------------------------------------------- 1 | // import '../css/game.css' 2 | // import Vue from 'vue' 3 | // import MishiroGame from '../vue/MishiroGame.vue' 4 | 5 | // // tslint:disable-next-line:no-unused-expression 6 | // new Vue({ 7 | // el: '#app', 8 | // render: h => h(MishiroGame) 9 | // }) 10 | -------------------------------------------------------------------------------- /app/src/ts/renderer-score.ts: -------------------------------------------------------------------------------- 1 | import './renderer/preload' 2 | import '../css/mishiro.css' 3 | import '../css/game.css' 4 | import ScoreViewer from './renderer/scoreviewer/score-viewer' 5 | 6 | ScoreViewer.main() 7 | 8 | if (process.env.NODE_ENV !== 'production') { 9 | if ((module as any).hot) (module as any).hot.accept() 10 | } 11 | -------------------------------------------------------------------------------- /app/src/ts/renderer.ts: -------------------------------------------------------------------------------- 1 | import './renderer/preload' 2 | 3 | import '../css/mishiro.css' 4 | import './renderer/developer-api' 5 | import Vue from 'vue' 6 | import VueI18n from 'vue-i18n' 7 | import Mishiro from '../vue/Mishiro.vue' 8 | import zh from './i18n/zh-CN' 9 | import ja from './i18n/ja-JP' 10 | import en from './i18n/en-US' 11 | import vueGlobal from './renderer/vue-global' 12 | import store from './renderer/store' 13 | import configurer from './renderer/config' 14 | 15 | if (process.env.NODE_ENV !== 'production') Object.defineProperty(window, 'ELECTRON_DISABLE_SECURITY_WARNINGS', { value: true }) 16 | 17 | // if (process.env.NODE_ENV !== 'production') require('./renderer/socket.ts').connect() 18 | 19 | Vue.use(VueI18n) 20 | Vue.use(vueGlobal) 21 | 22 | const vm = new Vue({ 23 | i18n: new VueI18n({ 24 | locale: configurer.get('language') || 'zh', 25 | messages: { 26 | zh, 27 | ja, 28 | en 29 | } 30 | }), 31 | store, 32 | render: (h) => h(Mishiro) 33 | }) 34 | vm.$mount('#app') 35 | 36 | if (process.env.NODE_ENV !== 'production') { 37 | if ((module as any).hot) (module as any).hot.accept() 38 | } 39 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/get-event-data.ts: -------------------------------------------------------------------------------- 1 | import configurer from '../config' 2 | 3 | export default function getEventData (eventAll: any[], now: number, timeOffset: number): { 4 | eventData: any 5 | eventHappening: boolean 6 | } { 7 | eventAll.sort((a, b) => new Date(b.event_start).getTime() - new Date(a.event_start).getTime()) 8 | 9 | const event = configurer.get('event') 10 | if (event != null) { 11 | for (let i = 0; i < eventAll.length; i++) { 12 | if (event === Number(eventAll[i].id)) return { eventData: eventAll[i], eventHappening: true } 13 | } 14 | } 15 | 16 | let eventData = null 17 | let eventHappening = false 18 | 19 | for (let i = 0; i < eventAll.length; i++) { 20 | const e = eventAll[i] 21 | const start = new Date(e.event_start).getTime() - timeOffset 22 | const end = new Date(e.result_end).getTime() - timeOffset 23 | if (now >= start && now <= end) { 24 | eventHappening = true 25 | eventData = e 26 | break 27 | } 28 | } 29 | if (!eventHappening) eventData = now > new Date(eventAll[0].result_end).getTime() ? eventAll[0] : eventAll[1] 30 | // const eventNow = master._exec("SELECT * FROM event_data WHERE event_start = (SELECT MAX(event_start) FROM (SELECT * FROM event_data WHERE event_start < DATETIME(CURRENT_TIMESTAMP, 'localtime')))")[0] 31 | 32 | return { eventData, eventHappening } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/get-limited-card.ts: -------------------------------------------------------------------------------- 1 | export default function (eventAll: any[], gachaAll: any[], eventLimited: any[], gachaLimited: any[]): { 2 | gachaLimitedCard: any 3 | eventLimitedCard: any 4 | } { 5 | const gachaLimitedCard: any = {} 6 | gachaLimited.forEach((card) => { 7 | if (!gachaLimitedCard[card.reward_id]) { 8 | gachaLimitedCard[card.reward_id] = [] 9 | } 10 | const gacha = gachaAll.filter(gacha => Number(gacha.id) === Number(card.gacha_id))[0] 11 | gachaLimitedCard[card.reward_id].push({ name: gacha.name, id: gacha.id, startDate: gacha.start_date.split(' ')[0], endDate: gacha.end_date.split(' ')[0] }) 12 | }) 13 | 14 | const eventLimitedCard: any = {} 15 | eventLimited.forEach((card) => { 16 | if (!eventLimitedCard[card.reward_id]) { 17 | eventLimitedCard[card.reward_id] = [] 18 | } 19 | const event = eventAll.filter(event => Number(event.id) === Number(card.event_id))[0] 20 | eventLimitedCard[card.reward_id].push({ name: event.name, id: event.id, startDate: event.event_start.split(' ')[0], endDate: event.event_end.split(' ')[0] }) 21 | }) 22 | 23 | return { gachaLimitedCard, eventLimitedCard } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/hash.ts: -------------------------------------------------------------------------------- 1 | 2 | import { Writable, WritableOptions } from 'stream' 3 | import { createHash } from 'crypto' 4 | 5 | const { createReadStream } = window.node.fs 6 | 7 | class HashResult extends Writable { 8 | private _value: string 9 | private _defer: { 10 | resolve: (value: string | PromiseLike) => void 11 | reject: (reason?: any) => void 12 | } 13 | 14 | public readonly promise: Promise 15 | public constructor (o?: WritableOptions) { 16 | super(o) 17 | this._value = '' 18 | Object.defineProperty(this, 'promise', { 19 | enumerable: true, 20 | configurable: true, 21 | writable: false, 22 | value: new Promise((resolve, reject) => { 23 | this._defer = { resolve, reject } 24 | this.once('error', reject) 25 | }) 26 | }) 27 | } 28 | 29 | _write (chunk: any, _encoding: string, cb: (err?: Error) => void): void { 30 | this._value = chunk.toString('hex') 31 | cb() 32 | } 33 | 34 | _final (cb: (err?: Error) => void): void { 35 | this._defer.resolve(this._value) 36 | cb() 37 | } 38 | } 39 | 40 | export function md5File (path: string): Promise { 41 | try { 42 | return createReadStream(path).pipe(createHash('md5')).pipe(new HashResult()).promise 43 | } catch (err) { 44 | return Promise.reject(err) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/main-window-id.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | const mainWindowId = ipcRenderer.sendSync('mainWindowId') 4 | export default mainWindowId 5 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/on-master-read.ts: -------------------------------------------------------------------------------- 1 | import getEventData from './get-event-data' 2 | // import getGachaData from './get-gacha-data' 3 | import getLimitedCard from './get-limited-card' 4 | import resolveCharaData from './resolve-chara-data' 5 | import resolveCardData from './resolve-card-data' 6 | import resolveAudioManifest, { BGM, Live } from './resolve-audio-manifest' 7 | // import resolveGachaAvailable from './resolve-gacha-available' 8 | import resolveUserLevel from './resolve-user-level' 9 | // import { openSqlite } from './sqlite3' 10 | import type DB from '../../common/db' 11 | 12 | // let masterData: MasterData | null = null 13 | 14 | export default async function readMaster (master: DB, manifestDB: DB): Promise<{ 15 | eventAll: any 16 | eventData: any 17 | eventAvailable: any 18 | eventHappening: any 19 | cardData: any 20 | bgmManifest: any 21 | liveManifest: any 22 | voiceManifest: any 23 | userLevel: any 24 | timeOffset: any 25 | }> { 26 | const timeOffset = (9 - (-(new Date().getTimezoneOffset() / 60))) * 60 * 60 * 1000 27 | const now = new Date().getTime() 28 | 29 | const gachaAll = await master.find('gacha_data') 30 | const eventAll = await master.find('event_data') 31 | 32 | const { eventData, eventHappening } = getEventData(eventAll, now, timeOffset) 33 | console.log(`eventID: ${eventData.id}`) 34 | const eventAvailable = await master.find('event_available', undefined, { event_id: eventData.id }) 35 | 36 | let cardData = await master.find('card_data') 37 | let charaData = await master.find('chara_data') 38 | const textData = [await master.find('text_data', undefined, { category: '2' }), await master.find('text_data', undefined, { category: '4' })] 39 | const skillData = await master.find('skill_data') 40 | const leaderSkillData = await master.find('leader_skill_data') 41 | const musicData = await master.find('music_data', ['id', 'name', 'bpm']) 42 | 43 | // let { gachaNow, gachaData } = getGachaData(gachaAll, config, now, timeOffset) 44 | // console.log(`gachaID: ${gachaData.id}`) 45 | // let gachaAvailable = await master._all(`SELECT * FROM gacha_available WHERE gacha_id LIKE '${gachaData.id}'`) 46 | 47 | // let liveManifest = manifestData.liveManifest 48 | // let bgmManifest = manifestData.bgmManifest 49 | // let voiceManifest = manifestData.voiceManifest 50 | // let scoreManifest = manifestData.scoreManifest 51 | // manifestData = {} 52 | 53 | let liveManifest = await manifestDB.find('manifests', ['name', 'hash'], { name: { $like: 'l/%' } }) 54 | let bgmManifest = await manifestDB.find('manifests', ['name', 'hash'], { name: { $like: 'b/%' } }) 55 | const voiceManifest = await manifestDB.find('manifests', ['name', 'hash'], { name: { $like: 'v/%' } }) 56 | const scoreManifest = await manifestDB.find('manifests', ['name', 'hash'], { name: { $like: 'musicscores_m___.bdb' } }) 57 | 58 | // let gachaLimited = await master._all('SELECT gacha_id, reward_id FROM gacha_available WHERE limited_flag = 1 ORDER BY reward_id') 59 | const gachaLimited = await master.find('gacha_available', ['gacha_id', 'reward_id'], { limited_flag: 1 }, { reward_id: 1 }) 60 | // let eventLimited = await master._all('SELECT event_id, reward_id FROM event_available ORDER BY reward_id') 61 | const eventLimited = await master.find('event_available', ['event_id', 'reward_id'], undefined, { reward_id: 1 }) 62 | 63 | let userLevel = await master.find('user_level', ['level', 'stamina', 'total_exp']) 64 | const liveData = await master.find('live_data', ['id', 'music_data_id', 'event_type', 'difficulty_5']) 65 | const jacketManifest = await manifestDB.find('manifests', ['name', 'hash'], { name: { $like: 'jacket%unity3d' } }) 66 | // master.close((err: Error) => { 67 | // if (err) throw err 68 | // master = void 0 69 | // }) 70 | 71 | const { gachaLimitedCard, eventLimitedCard } = getLimitedCard(eventAll, gachaAll, eventLimited, gachaLimited) 72 | charaData = resolveCharaData(charaData, textData) 73 | cardData = resolveCardData(cardData, charaData, skillData, leaderSkillData, eventLimitedCard, gachaLimitedCard) 74 | 75 | const audioManifest = resolveAudioManifest(bgmManifest, liveManifest, musicData, charaData, liveData, scoreManifest, jacketManifest) 76 | bgmManifest = audioManifest.bgmManifest 77 | liveManifest = audioManifest.liveManifest 78 | 79 | // let resolvedGacha = await resolveGachaAvailable(gachaAvailable, cardData, gachaData) 80 | // gachaAvailable = resolvedGacha.gachaAvailable 81 | // gachaData.count = resolvedGacha.count 82 | 83 | userLevel = resolveUserLevel(userLevel) 84 | 85 | // let gachaIcon: { name: string; hash: string; [x: string]: any }[] = gachaAvailable.map((o: any) => { 86 | // return manifests.filter(m => m.name === `card_${o.reward_id}_m.unity3d`)[0] 87 | // }) 88 | return { 89 | eventAll, 90 | eventData, 91 | eventAvailable, 92 | eventHappening, 93 | cardData, 94 | bgmManifest, 95 | liveManifest, 96 | voiceManifest, 97 | // gachaData, 98 | // gachaAvailable, 99 | // gachaNow, 100 | // gachaIcon, 101 | userLevel, 102 | timeOffset 103 | } 104 | } 105 | 106 | export interface MasterData { 107 | eventAll: any[] 108 | eventData: any 109 | eventAvailable: any[] 110 | eventHappening: any 111 | cardData: any[] 112 | bgmManifest: BGM[] 113 | liveManifest: Live[] 114 | voiceManifest: any[] 115 | // gachaData: any[] 116 | // gachaAvailable: any[] 117 | // gachaNow: any 118 | // gachaIcon: { name: string; hash: string; [x: string]: any }[] 119 | userLevel: any[] 120 | timeOffset: number 121 | } 122 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/resolve-card-data.ts: -------------------------------------------------------------------------------- 1 | export default function (cardData: any[], charaData: any[], skillData: any[], leaderSkillData: any[], eventLimitedCard: any[], gachaLimitedCard: any[]): any[] { 2 | const gachaLimitedCardId = Object.keys(gachaLimitedCard).map(id => Number(id)) 3 | const eventLimitedCardId = Object.keys(eventLimitedCard).map(id => Number(id)) 4 | for (let i = 0; i < cardData.length; i++) { 5 | const card = cardData[i] 6 | cardData[i].charaData = charaData.filter(row => Number(row.chara_id) === Number(card.chara_id))[0] 7 | cardData[i].skill = skillData.filter(row => Number(row.id) === Number(card.skill_id))[0] 8 | cardData[i].leaderSkill = leaderSkillData.filter(row => Number(row.id) === Number(card.leader_skill_id))[0] 9 | if (eventLimitedCardId.includes(cardData[i].id)) { 10 | cardData[i].limited = eventLimitedCard[cardData[i].id] 11 | } 12 | if (gachaLimitedCardId.includes(cardData[i].id)) { 13 | cardData[i].limited = gachaLimitedCard[cardData[i].id] 14 | } 15 | } 16 | return cardData 17 | } 18 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/resolve-chara-data.ts: -------------------------------------------------------------------------------- 1 | export default function (charaData: any[], textData: any[]): any[] { 2 | for (let i = 0; i < charaData.length; i++) { 3 | const chara = charaData[i] 4 | const hometown = textData[0].filter((row: any) => Number(row.index) === Number(chara.home_town))[0] 5 | const seiza = textData[1].filter((row: any) => (1000 + Number(row.index)) === Number(chara.constellation))[0] 6 | if (hometown) { 7 | charaData[i].hometown = hometown.text 8 | } 9 | if (seiza) { 10 | charaData[i].seiza = seiza.text 11 | } 12 | } 13 | return charaData 14 | } 15 | -------------------------------------------------------------------------------- /app/src/ts/renderer/back/resolve-user-level.ts: -------------------------------------------------------------------------------- 1 | export default function (userLevel: any[]): any[] { 2 | userLevel.sort((a, b) => a.level - b.level) 3 | 4 | for (let i = 0; i < userLevel.length; i++) { 5 | if (i !== userLevel.length - 1) userLevel[i].exp = userLevel[i + 1].total_exp - userLevel[i].total_exp 6 | else userLevel[i].exp = Infinity 7 | } 8 | 9 | return userLevel 10 | } 11 | -------------------------------------------------------------------------------- /app/src/ts/renderer/check.ts: -------------------------------------------------------------------------------- 1 | import configurer from './config' 2 | import { checkResourceVersion } from './ipc' 3 | 4 | async function check (): Promise { 5 | const resVer = configurer.get('resVer') 6 | if (resVer) { 7 | return resVer 8 | } 9 | const res = await checkResourceVersion() 10 | if (res !== 0) { 11 | return res 12 | } 13 | 14 | throw new Error('Version checking failed') 15 | } 16 | 17 | export default check 18 | -------------------------------------------------------------------------------- /app/src/ts/renderer/config.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | export class Configurer { 4 | public getAll (): import('../main/config').MishiroConfig { 5 | return ipcRenderer.sendSync('configurer#getAll') 6 | } 7 | 8 | public get (key: K): import('../main/config').MishiroConfig[K] { 9 | return ipcRenderer.sendSync('configurer#get', key) 10 | } 11 | 12 | public set (obj: import('../main/config').MishiroConfig): void 13 | public set (obj: K, value: import('../main/config').MishiroConfig[K]): void 14 | public set (key: K | import('../main/config').MishiroConfig, value?: import('../main/config').MishiroConfig[K]): void { 15 | return ipcRenderer.sendSync('configurer#set', key, value) 16 | } 17 | 18 | public remove (key: import('../main/config').MishiroConfigKey): void { 19 | return ipcRenderer.sendSync('configurer#remove', key) 20 | } 21 | } 22 | 23 | const configurer = new Configurer() 24 | 25 | export default configurer 26 | -------------------------------------------------------------------------------- /app/src/ts/renderer/developer-api.ts: -------------------------------------------------------------------------------- 1 | import { getProfile } from './ipc' 2 | 3 | const mishiroCore = window.node.mishiroCore 4 | 5 | declare global { 6 | interface Window { 7 | mishiro: { 8 | encode: typeof mishiroCore.Client.cryptoGrapher.encode 9 | decode: typeof mishiroCore.Client.cryptoGrapher.decode 10 | _decryptBody: typeof mishiroCore.Client.decryptBody 11 | decryptBody: (body: string, udid: string) => any 12 | getProfile: (viewer: string | number) => Promise 13 | } 14 | } 15 | } 16 | 17 | window.mishiro = { 18 | encode: mishiroCore.Client.cryptoGrapher.encode, 19 | decode: mishiroCore.Client.cryptoGrapher.decode, 20 | _decryptBody: mishiroCore.Client.decryptBody, 21 | decryptBody (body, udid) { 22 | if (!(/^[0-9a-f-]{36}$/.test(udid))) { 23 | udid = mishiroCore.Client.cryptoGrapher.decode(udid) 24 | } 25 | return mishiroCore.Client.decryptBody(body, Buffer.from(udid.replace(/-/g, ''), 'hex')) 26 | }, 27 | getProfile (viewer) { 28 | return getProfile(String(viewer)) 29 | } 30 | } 31 | 32 | export {} 33 | -------------------------------------------------------------------------------- /app/src/ts/renderer/ipc-back.ts: -------------------------------------------------------------------------------- 1 | import store, { Action } from './store' 2 | 3 | const { ipcRenderer } = window.node.electron 4 | 5 | // const backWindowId = ipcRenderer.sendSync('backWindowId') 6 | 7 | let id = 0 8 | 9 | function createChannelName (): string { 10 | id = ((id + 1) % 0xffffff) 11 | return `__main_callback_${id}__` 12 | } 13 | 14 | let backWindowPort: MessagePort 15 | 16 | interface Deferred { 17 | resolve: (value: any) => void 18 | reject: (reason: any) => void 19 | } 20 | 21 | const promiseMap = new Map() 22 | 23 | ipcRenderer.on('port', e => { 24 | backWindowPort = e.ports[0] 25 | 26 | backWindowPort.onmessage = function (ev) { 27 | if (ev.data.type === 'setBatchStatus') { 28 | store.commit(Action.SET_BATCH_STATUS, ev.data.payload[0]) 29 | } 30 | 31 | const channel = ev.data.id 32 | if (typeof channel === 'string') { 33 | if (promiseMap.has(channel)) { 34 | const deferred = promiseMap.get(channel)! 35 | if (ev.data.err) { 36 | deferred.reject(new Error(ev.data.err)) 37 | } else { 38 | deferred.resolve(ev.data.data) 39 | } 40 | } 41 | } 42 | } 43 | }) 44 | 45 | function invokeBackWindow (name: string, args: any[] = []): Promise { 46 | return new Promise((resolve, reject) => { 47 | if (!backWindowPort) { 48 | reject(new Error('back window is not ready')) 49 | return 50 | } 51 | const callbackChannel = createChannelName() 52 | promiseMap.set(callbackChannel, { 53 | resolve: (value) => { 54 | promiseMap.delete(callbackChannel) 55 | resolve(value) 56 | }, 57 | reject: (reason) => { 58 | promiseMap.delete(callbackChannel) 59 | reject(reason) 60 | } 61 | }) 62 | backWindowPort.postMessage({ 63 | id: callbackChannel, 64 | type: name, 65 | payload: args 66 | }) 67 | }) 68 | } 69 | 70 | export function openManifestDatabase (path: string): Promise { 71 | return invokeBackWindow('openManifestDatabase', [path]) 72 | } 73 | 74 | export function getMasterHash (): Promise { 75 | return invokeBackWindow('getMasterHash') 76 | } 77 | 78 | export function readMasterData (masterFile: string): Promise { 79 | return invokeBackWindow('readMasterData', [masterFile]) 80 | } 81 | 82 | export function getCardHash (id: string | number): Promise { 83 | return invokeBackWindow('getCardHash', [id]) 84 | } 85 | 86 | export function getIconHash (id: string | number): Promise { 87 | return invokeBackWindow('getIconHash', [id]) 88 | } 89 | 90 | export function getEmblemHash (id: string | number): Promise { 91 | return invokeBackWindow('getEmblemHash', [id]) 92 | } 93 | 94 | export function searchResources (query: string): Promise { 95 | return invokeBackWindow('searchResources', [query]) 96 | } 97 | 98 | export function startBatchDownload (): Promise { 99 | return invokeBackWindow('startBatchDownload') 100 | } 101 | 102 | export function stopBatchDownload (): Promise { 103 | return invokeBackWindow('stopBatchDownload') 104 | } 105 | 106 | export function getBatchErrorList (): Promise { 107 | return invokeBackWindow('getBatchErrorList') 108 | } 109 | 110 | export function setDownloaderProxy (proxy: string): Promise { 111 | return invokeBackWindow('setDownloaderProxy', [proxy]) 112 | } 113 | -------------------------------------------------------------------------------- /app/src/ts/renderer/ipc.ts: -------------------------------------------------------------------------------- 1 | import type { OpenDialogOptions, OpenDialogReturnValue, RelaunchOptions, SaveDialogOptions, SaveDialogReturnValue } from 'electron' 2 | import type { ServerResponse } from 'mishiro-core' 3 | 4 | const { ipcRenderer } = window.node.electron 5 | 6 | export function relaunch (options: RelaunchOptions): void { 7 | return ipcRenderer.sendSync('relaunch', options) 8 | } 9 | 10 | export function exit (exitCode?: number): void { 11 | return ipcRenderer.sendSync('exit', exitCode) 12 | } 13 | 14 | export function showSaveDialog (options: SaveDialogOptions): Promise { 15 | return ipcRenderer.invoke('showSaveDialog', options) 16 | } 17 | 18 | export function showOpenDialog (options: OpenDialogOptions): Promise { 19 | return ipcRenderer.invoke('showOpenDialog', options) 20 | } 21 | 22 | export function getAppName (): string { 23 | return ipcRenderer.sendSync('appName') 24 | } 25 | 26 | export function getAppVersion (): string { 27 | return ipcRenderer.sendSync('appVersion') 28 | } 29 | 30 | export function getPackageJson (): Record { 31 | return ipcRenderer.sendSync('package.json') 32 | } 33 | 34 | export function checkResourceVersion (): Promise { 35 | return ipcRenderer.invoke('checkResourceVersion') 36 | } 37 | 38 | export function updateClientProxy (proxy: string): void { 39 | return ipcRenderer.sendSync('updateClientProxy', proxy) 40 | } 41 | 42 | export function getProfile (viewer: string): Promise { 43 | return ipcRenderer.invoke('getProfile', viewer) 44 | } 45 | 46 | export function getLyrics (scoreFile: string): Promise> { 47 | return ipcRenderer.invoke('getLyrics', scoreFile) 48 | } 49 | 50 | export function getScoreDifficulties (scoreFile: string): Promise { 51 | return ipcRenderer.invoke('getScoreDifficulties', scoreFile) 52 | } 53 | 54 | export function getScore (scoreFile: string, difficulty: number | string, bpm: number, src: string): Promise { 55 | return ipcRenderer.invoke('getScore', scoreFile, difficulty, bpm, src) 56 | } 57 | -------------------------------------------------------------------------------- /app/src/ts/renderer/license.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | # mishiro 3 | 4 | ## The MIT License 5 | 6 | Copyright (c) 2017-2019 Toyobayashi 7 | 8 | Permission is hereby granted, free of charge, to any 9 | person obtaining a copy of this software and associated 10 | documentation files (the "Software"), to deal in the 11 | Software without restriction, including without limitation 12 | the rights to use, copy, modify, merge, publish, distribute, 13 | and/or sublicense copies of the Software, and to permit 14 | persons to whom the Software is furnished to do so, subject 15 | to the following conditions: 16 | 17 | 1. The above copyright notice and this permission notice shall 18 | be included in all copies or substantial portions of the Software. 19 | 20 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 21 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 22 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 23 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 24 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 25 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 26 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 27 | OTHER DEALINGS IN THE SOFTWARE. 28 | ` 29 | -------------------------------------------------------------------------------- /app/src/ts/renderer/log.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer } from 'electron' 2 | 3 | export function info (msg: string): void { 4 | ipcRenderer.sendSync('log', 'info', msg) 5 | } 6 | 7 | export function warn (msg: string): void { 8 | ipcRenderer.sendSync('log', 'warn', msg) 9 | } 10 | 11 | export function error (msg: string): void { 12 | ipcRenderer.sendSync('log', 'error', msg) 13 | } 14 | 15 | export function critical (msg: string): void { 16 | ipcRenderer.sendSync('log', 'critical', msg) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/ts/renderer/mishiro-entry.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | import getPath from '../common/get-path' 3 | const fs = window.node.fs 4 | 5 | @Component 6 | export default class extends Vue { 7 | bg: boolean | null = null 8 | isTouched: boolean = false 9 | coverSrc: string = '../../asset/img.asar/title_bg_10010.png' 10 | 11 | enter (): void { 12 | if (!this.isTouched) { 13 | this.isTouched = true 14 | this.playSe(new Audio('../../asset/se.asar/se_title_start.mp3')) 15 | this.$emit('touch') 16 | setTimeout(() => { 17 | this.$emit('enter') 18 | }, 1000) 19 | } 20 | } 21 | 22 | beforeMount (): void { 23 | this.$nextTick(() => { 24 | const msrEvent = localStorage.getItem('msrEvent') 25 | if (msrEvent) { 26 | try { 27 | const o = JSON.parse(msrEvent) 28 | if (fs.existsSync(getPath.cardDir(`bg_${o.card}.png`))) { 29 | this.coverSrc = `../../asset/card/bg_${o.card}.png` 30 | } 31 | } catch (_) { 32 | localStorage.clear() 33 | } 34 | } 35 | }) 36 | } 37 | 38 | showOption (btn: HTMLElement): void { 39 | btn.blur() 40 | this.playSe(this.enterSe) 41 | this.event.$emit('option', false) 42 | } 43 | 44 | mounted (): void { 45 | this.$nextTick(() => { 46 | this.bg = (window.innerWidth / window.innerHeight >= 1280 / 824) 47 | window.addEventListener('resize', () => { 48 | this.bg = (window.innerWidth / window.innerHeight >= 1280 / 824) 49 | }, false) 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/src/ts/renderer/mishiro-game.ts: -------------------------------------------------------------------------------- 1 | // import TheCombo from '../../vue/component/TheCombo.vue' 2 | // import TheLiveGauge from '../../vue/component/TheLiveGauge.vue' 3 | // import { liveResult, Game } from './game' 4 | // import { Vue, Component } from 'vue-property-decorator' 5 | // import { Event } from 'electron' 6 | 7 | // @Component({ 8 | // components: { 9 | // TheCombo, 10 | // TheLiveGauge 11 | // } 12 | // }) 13 | // export default class extends Vue { 14 | // liveResult = liveResult 15 | // mounted () { 16 | // this.$nextTick(() => { 17 | // window.node.electron.ipcRenderer.once('start', (_event: Event, song: any, fromWindowId: number) => { 18 | // Game.start(song, fromWindowId) 19 | // }) 20 | // Game.init() 21 | // }) 22 | // } 23 | // } 24 | -------------------------------------------------------------------------------- /app/src/ts/renderer/mishiro-menu.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | import { exit, relaunch } from './ipc' 3 | import license from './license' 4 | import updater from './updater' 5 | 6 | import getPath from '../common/get-path' 7 | import { error } from './log' 8 | const fs = window.node.fs 9 | 10 | const { dataDir } = getPath 11 | 12 | @Component 13 | export default class extends Vue { 14 | showOption (btn: HTMLElement): void { 15 | btn.blur() 16 | this.playSe(this.enterSe) 17 | this.event.$emit('option', true) 18 | } 19 | 20 | showAbout (): void { 21 | this.playSe(this.enterSe) 22 | this.event.$emit('showAbout') 23 | } 24 | 25 | showLicense (): void { 26 | this.playSe(this.enterSe) 27 | // tslint:disable-next-line: no-floating-promises 28 | import(/* webpackChunkName: "marked" */ 'marked').then((marked) => { 29 | this.event.$emit('license') 30 | this.event.$emit('alert', this.$t('menu.license'), (marked as any).default(license), 900) 31 | }).catch(err => { 32 | console.error(err) 33 | error(`MENU showLicense: ${err.stack}`) 34 | }) 35 | } 36 | 37 | showVar (): void { 38 | this.playSe(this.enterSe) 39 | this.event.$emit('alert', this.$t('menu.var'), this.$t('menu.varCon')) 40 | } 41 | 42 | async update (): Promise { 43 | this.playSe(this.enterSe) 44 | if (!navigator.onLine) { 45 | this.event.$emit('alert', this.$t('home.errorTitle'), this.$t('home.noNetwork')) 46 | return 47 | } 48 | this.$emit('checking') 49 | 50 | await updater.check() 51 | this.$emit('checked') 52 | 53 | const info = updater.getUpdateInfo() 54 | if (info) { 55 | this.event.$emit('versionCheck', info) 56 | } else { 57 | this.event.$emit('alert', this.$t('menu.update'), this.$t('menu.noUpdate')) 58 | } 59 | // const headers = { 60 | // 'User-Agent': 'mishiro' 61 | // } 62 | // const releases = { 63 | // url: 'https://api.github.com/repos/toyobayashi/mishiro/releases', 64 | // json: true, 65 | // headers 66 | // } 67 | // const tags = { 68 | // url: 'https://api.github.com/repos/toyobayashi/mishiro/tags', 69 | // json: true, 70 | // headers 71 | // } 72 | // request(releases, (err, _res, body) => { 73 | // if (!err) { 74 | // const latest = body[0] 75 | // const version = latest.tag_name.substr(1) 76 | // if (getVersion() >= version) { 77 | // this.$emit('checked') 78 | // this.event.$emit('alert', this.$t('menu.update'), this.$t('menu.noUpdate')) 79 | // } else { 80 | // const description = marked(latest.body) 81 | // const zip = latest.assets.filter((a: any) => ((a.content_type === 'application/x-zip-compressed' || a.content_type === 'application/zip') && (a.name.indexOf(`${process.platform}-${process.arch}`) !== -1)))[0] 82 | // const exe = latest.assets.filter((a: any) => ((a.content_type === 'application/x-msdownload') && (a.name.indexOf(`${process.platform}-${process.arch}`) !== -1)))[0] 83 | // const patch = latest.assets.filter((a: any) => (a.name.indexOf('patch.zip') !== -1))[0] 84 | // const zipUrl = zip ? zip.browser_download_url : null 85 | // const exeUrl = exe ? exe.browser_download_url : null 86 | // const patchUrl = patch ? patch.browser_download_url : null 87 | 88 | // request(tags, (err, _res, body) => { 89 | // this.$emit('checked') 90 | // if (!err) { 91 | // const latestTag = body.filter((tag: any) => tag.name === latest.tag_name)[0] 92 | // const commit = latestTag.commit.sha 93 | // const versionData = { version, commit, description, zipUrl, exeUrl, patchUrl } 94 | // console.log(versionData) 95 | // this.event.$emit('versionCheck', versionData) 96 | // } else { 97 | // this.event.$emit('alert', this.$t('home.errorTitle'), err.message) 98 | // } 99 | // }) 100 | // } 101 | // } else { 102 | // this.event.$emit('alert', this.$t('home.errorTitle'), err.message) 103 | // } 104 | // }) 105 | } 106 | 107 | relaunch (): void { 108 | this.playSe(this.enterSe) 109 | relaunch({ args: [getPath()] }) 110 | exit(0) 111 | } 112 | 113 | calculator (): void { 114 | this.playSe(this.enterSe) 115 | this.event.$emit('openCal') 116 | } 117 | 118 | exit (): void { 119 | exit(0) 120 | this.playSe(this.cancelSe) 121 | } 122 | 123 | cacheClear (): void { 124 | this.playSe(this.enterSe) 125 | const files = fs.readdirSync(dataDir()) 126 | const deleteItem = [] 127 | for (let i = 0; i < files.length; i++) { 128 | if (!new RegExp(`${this.$store.state.resVer === -1 ? 'Unknown' : this.$store.state.resVer}`).test(files[i])) deleteItem.push(dataDir(files[i])) 129 | } 130 | if (deleteItem.length) { 131 | Promise.all(deleteItem.map(item => fs.remove(item))).then(() => { 132 | this.event.$emit('alert', this.$t('menu.cacheClear'), this.$t('menu.cacheClearSuccess')) 133 | }).catch(err => this.event.$emit('alert', this.$t('home.errorTitle'), err && err.message)) 134 | } else this.event.$emit('alert', this.$t('menu.cacheClear'), this.$t('menu.noCache')) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-about.ts: -------------------------------------------------------------------------------- 1 | import modalMixin from './modal-mixin' 2 | import Component, { mixins } from 'vue-class-component' 3 | import { getAppName, getAppVersion, getPackageJson } from './ipc' 4 | import getPath from '../common/get-path' 5 | import { error } from './log' 6 | const pkg = getPackageJson() 7 | 8 | @Component 9 | export default class extends mixins(modalMixin) { 10 | appName = getAppName() 11 | appVersion = getAppVersion() 12 | versions = window.node.process.versions 13 | arch = window.node.process.arch 14 | osinfo!: string 15 | commit = process.env.NODE_ENV === 'production' ? pkg._commit : window.node.childProcess.execSync('git rev-parse HEAD', { cwd: getPath() }).toString().replace(/[\r\n]/g, '') 16 | commitDate = process.env.NODE_ENV === 'production' ? pkg._commitDate : new Date((window.node.childProcess.execSync('git log -1', { cwd: getPath() }).toString().match(/Date:\s*(.*?)\n/) as RegExpMatchArray)[1]).toISOString() 17 | 18 | showRepo (): void { 19 | window.node.electron.shell.openExternal('https://github.com/toyobayashi/mishiro').catch(err => { 20 | console.error(err) 21 | error(`SCORE showRepo: ${err.stack}`) 22 | }) 23 | this.playSe(this.enterSe) 24 | } 25 | 26 | created (): void { 27 | this.osinfo = `${window.node.os.type()} ${this.arch} ${window.node.os.release()}` 28 | } 29 | 30 | mounted (): void { 31 | this.$nextTick(() => { 32 | this.event.$on('showAbout', () => { 33 | this.show = true 34 | this.visible = true 35 | }) 36 | }) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-batch-error.ts: -------------------------------------------------------------------------------- 1 | import modalMixin from './modal-mixin' 2 | import Component, { mixins } from 'vue-class-component' 3 | 4 | @Component 5 | export default class extends mixins(modalMixin) { 6 | modalWidth = '800px' 7 | errorList: IBatchError[] = [] 8 | 9 | afterLeave (): void { 10 | this.show = false 11 | this.errorList = [] 12 | this.modalWidth = '800px' 13 | } 14 | 15 | mounted (): void { 16 | this.$nextTick(() => { 17 | this.event.$on('alertBatchError', (errorList: IBatchError[]) => { 18 | this.errorList = errorList ?? [] 19 | this.show = true 20 | this.visible = true 21 | }) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-gacha-card.ts: -------------------------------------------------------------------------------- 1 | import TabSmall from '../../vue/component/TabSmall.vue' 2 | import MishiroIdol from './mishiro-idol' 3 | import modalMixin from './modal-mixin' 4 | import Component, { mixins } from 'vue-class-component' 5 | @Component({ 6 | components: { 7 | TabSmall 8 | } 9 | }) 10 | export default class extends mixins(modalMixin, MishiroIdol) { 11 | card: any = {} 12 | cardPlus: any = {} 13 | 14 | toggle (practice: string): void { 15 | switch (practice) { 16 | case 'idol.before': 17 | this.information = this.card 18 | break 19 | case 'idol.after': 20 | this.information = this.cardPlus 21 | break 22 | default: 23 | break 24 | } 25 | } 26 | 27 | mounted (): void { 28 | this.$nextTick(() => { 29 | this.event.$on('showCard', (card: any) => { 30 | const cardPlus = this.cardData.filter((c: any) => Number(c.id) === Number(card.evolution_id))[0] 31 | this.currentPractice = 'idol.before' 32 | this.information = card 33 | this.card = card 34 | this.cardPlus = cardPlus 35 | this.show = true 36 | this.visible = true 37 | }) 38 | }) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-live-difficulty.ts: -------------------------------------------------------------------------------- 1 | 2 | // import InputRadio from '../../vue/component/InputRadio.vue' 3 | // import { Event } from 'electron' 4 | // import modalMixin from './modal-mixin' 5 | // import Component, { mixins } from 'vue-class-component' 6 | 7 | // const { ipcRenderer, remote } = window.node.electron 8 | // const getPath = window.preload.getPath 9 | // const url = window.node.url 10 | // const BrowserWindow = remote.BrowserWindow 11 | // const { scoreDir, liveDir } = getPath 12 | 13 | // @Component({ 14 | // components: { 15 | // InputRadio 16 | // } 17 | // }) 18 | // export default class extends mixins(modalMixin) { 19 | 20 | // difficulty: string = '4' 21 | // live: any = {} 22 | 23 | // start () { 24 | // this.playSe(this.enterSe) 25 | // ipcRenderer.send( 26 | // 'game', 27 | // scoreDir(this.live.score), // scoreFile 28 | // this.difficulty, // difficulty 29 | // this.live.bpm, // bpm 30 | // liveDir(this.live.fileName) // audioFile 31 | // ) 32 | // } 33 | // mounted () { 34 | // this.$nextTick(() => { 35 | // ipcRenderer.on('game', (_event: Event, obj: { src: string; bpm: number; score: any[][]; fullCombo: number;}) => { 36 | // const focusedWindow = BrowserWindow.getFocusedWindow() 37 | // if (!focusedWindow) return 38 | // this.event.$emit('gameStart') 39 | // this.event.$emit('pauseBgm') 40 | // const windowID = focusedWindow.id 41 | 42 | // let win = new BrowserWindow({ 43 | // width: 1296, 44 | // height: 759, 45 | // minWidth: 1296, 46 | // minHeight: 759, 47 | // maxWidth: 1296, 48 | // maxHeight: 759, 49 | // backgroundColor: '#000000', 50 | // parent: focusedWindow, 51 | // webPreferences: { 52 | // nodeIntegration: false, 53 | // contextIsolation: false, 54 | // preload: window.preload.getPath('public', 'preload.js') 55 | // } 56 | // }) 57 | 58 | // if (process.env.NODE_ENV === 'production') { 59 | // win.loadURL(url.format({ 60 | // pathname: getPath('./public/game.html'), 61 | // protocol: 'file:', 62 | // slashes: true 63 | // })) 64 | // } else { 65 | // const config = require('../../../script/config.ts').default 66 | // win.loadURL(`http://${config.devServerHost}:${config.devServerPort}${config.publicPath}game.html`) 67 | // } 68 | 69 | // win.webContents.on('did-finish-load', function () { 70 | // win.webContents.send('start', obj, windowID) 71 | // }) 72 | 73 | // this.visible = false 74 | // }) 75 | // this.event.$on('game', (live: any) => { 76 | // this.difficulty = '4' 77 | // this.live = live 78 | // this.show = true 79 | // this.visible = true 80 | // }) 81 | // this.event.$on('enterKey', (block: string) => { 82 | // if (block === 'live' && this.visible) { 83 | // this.start() 84 | // } 85 | // }) 86 | // }) 87 | // } 88 | // } 89 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-mixin.ts: -------------------------------------------------------------------------------- 1 | import StaticTitleDot from '../../vue/component/StaticTitleDot.vue' 2 | import { Vue, Component } from 'vue-property-decorator' 3 | @Component({ 4 | components: { 5 | StaticTitleDot 6 | } 7 | }) 8 | export default class extends Vue { 9 | show: boolean = false 10 | visible: boolean = false 11 | bodyMaxHeight: string = `${window.innerHeight - 267}px` 12 | modalWidth: string = '600px' 13 | 14 | close (): void { 15 | this.playSe(this.cancelSe) 16 | this.visible = false 17 | } 18 | 19 | afterLeave (): void { 20 | this.show = false 21 | } 22 | 23 | mounted (): void { 24 | this.$nextTick(() => { 25 | window.addEventListener( 26 | 'resize', 27 | () => { 28 | this.bodyMaxHeight = `${window.innerHeight - 267}px` 29 | }, 30 | false 31 | ) 32 | this.event.$on('escKey', () => { 33 | if (this.visible) { 34 | this.close() 35 | } 36 | }) 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-score.ts: -------------------------------------------------------------------------------- 1 | 2 | import InputRadio from '../../vue/component/InputRadio.vue' 3 | 4 | import modalMixin from './modal-mixin' 5 | import Component, { mixins } from 'vue-class-component' 6 | import getPath from '../common/get-path' 7 | import { getScore } from './ipc' 8 | import type { Live } from './back/resolve-audio-manifest' 9 | import { error } from './log' 10 | // import configurer from './config' 11 | 12 | const { ipcRenderer } = window.node.electron 13 | 14 | const { scoreDir, liveDir } = getPath 15 | 16 | @Component({ 17 | components: { 18 | InputRadio 19 | } 20 | }) 21 | export default class extends mixins(modalMixin) { 22 | difficulty: string = '4' 23 | live: Live = {} as any 24 | difficulties: { [key: string]: string } = {} 25 | 26 | async start (): Promise { 27 | this.playSe(this.enterSe) 28 | const res = await getScore( 29 | scoreDir(this.live.score!), // scoreFile) 30 | this.difficulty, // difficulty 31 | this.live.bpm!, // bpm 32 | liveDir(this.live.fileName + '.hca') // hca file 33 | ) 34 | if (!res) return 35 | 36 | this.event.$emit('gameStart') 37 | this.event.$emit('pauseBgm') 38 | 39 | await ipcRenderer.invoke('openScoreWindow') 40 | 41 | this.visible = false 42 | // ipcRenderer.send( 43 | // 'score', 44 | // scoreDir(this.live.score), // scoreFile 45 | // this.difficulty, // difficulty 46 | // this.live.bpm, // bpm 47 | // liveDir(this.live.fileName) // audioFile 48 | // ) 49 | } 50 | 51 | mounted (): void { 52 | this.$nextTick(() => { 53 | this.event.$on('score', (live: Live, difficulties: { [key: string]: string }) => { 54 | const diffs = Object.keys(difficulties) 55 | if (diffs.length === 0) { 56 | this.event.$emit('alert', this.$t('home.errorTitle'), this.$t('live.noScore')) 57 | return 58 | } 59 | this.difficulties = difficulties 60 | this.difficulty = diffs.length.toString() 61 | this.live = live 62 | this.show = true 63 | this.visible = true 64 | }) 65 | this.event.$on('enterKey', (block: string) => { 66 | if (block === 'live' && this.visible) { 67 | this.start().catch(err => { 68 | console.error(err) 69 | error(`SCORE start: ${err.stack}`) 70 | }) 71 | } 72 | }) 73 | }) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app/src/ts/renderer/modal-version.ts: -------------------------------------------------------------------------------- 1 | import modalMixin from './modal-mixin' 2 | import ProgressBar from '../../vue/component/ProgressBar.vue' 3 | 4 | import Component, { mixins } from 'vue-class-component' 5 | import updater from './updater' 6 | import { error } from './log' 7 | 8 | const { shell } = window.node.electron 9 | 10 | @Component({ 11 | components: { 12 | ProgressBar 13 | } 14 | }) 15 | export default class extends mixins(modalMixin) { 16 | versionData: any = {} 17 | updateProgress: number = 0 18 | btnDisabled: boolean = false 19 | 20 | cancel (): void { 21 | updater.abort() 22 | this.close() 23 | } 24 | 25 | async showRepo (): Promise { 26 | this.playSe(this.enterSe) 27 | if (this.versionData.appZipUrl && process.env.NODE_ENV === 'production') { 28 | this.btnDisabled = true 29 | try { 30 | updater.onDownload((status) => { 31 | this.updateProgress = status.loading 32 | }) 33 | const result = await updater.download() 34 | updater.onDownload(null) 35 | if (result) { 36 | updater.relaunch() 37 | } else { 38 | this.btnDisabled = false 39 | this.updateProgress = 0 40 | } 41 | } catch (err: any) { 42 | updater.onDownload(null) 43 | this.btnDisabled = false 44 | this.event.$emit('alert', this.$t('home.errorTitle'), err.message) 45 | } 46 | } else if (this.versionData.exeUrl) { 47 | shell.openExternal(this.versionData.exeUrl).catch(err => { 48 | console.error(err) 49 | error(`VERSION exeUrl: ${err.stack}`) 50 | }) 51 | } else if (this.versionData.zipUrl) { 52 | shell.openExternal(this.versionData.zipUrl).catch(err => { 53 | console.error(err) 54 | error(`VERSION zipUrl: ${err.stack}`) 55 | }) 56 | } else { 57 | shell.openExternal('https://github.com/toyobayashi/mishiro/releases').catch(err => { 58 | console.error(err) 59 | error(`VERSION releases: ${err.stack}`) 60 | }) 61 | } 62 | } 63 | 64 | mounted (): void { 65 | this.$nextTick(() => { 66 | this.event.$on('versionCheck', (versionData: any) => { 67 | // tslint:disable-next-line: no-floating-promises 68 | import(/* webpackChunkName: "marked" */ 'marked').then(marked => { 69 | this.show = true 70 | this.visible = true 71 | versionData.description = (marked as any).default(versionData.release.body) 72 | this.versionData = versionData 73 | }).catch(err => { 74 | console.error(err) 75 | error(`MENU versionCheck: ${err.stack}`) 76 | }) 77 | }) 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/src/ts/renderer/preload.ts: -------------------------------------------------------------------------------- 1 | import * as electron from 'electron' 2 | 3 | // process.once('loaded', function () { 4 | window.node = { 5 | process: process, 6 | electron, 7 | tybys: { 8 | downloader: __non_webpack_require__('@tybys/downloader') 9 | }, 10 | mishiroCore: __non_webpack_require__('mishiro-core'), 11 | fs: __non_webpack_require__('fs-extra'), 12 | events: __non_webpack_require__('events'), 13 | acb: __non_webpack_require__('acb'), 14 | hcaDecoder: __non_webpack_require__('hca-decoder'), 15 | os: __non_webpack_require__('os'), 16 | path: __non_webpack_require__('path'), 17 | url: __non_webpack_require__('url'), 18 | childProcess: __non_webpack_require__('child_process'), 19 | iconvLite: __non_webpack_require__('iconv-lite') 20 | } 21 | // }) 22 | -------------------------------------------------------------------------------- /app/src/ts/renderer/scoreviewer/global.ts: -------------------------------------------------------------------------------- 1 | import { error } from '../log' 2 | 3 | export interface GlobalConstructorArgument { 4 | noteWidth?: number 5 | noteHeight?: number 6 | noteWidthFlip?: number 7 | scale?: number 8 | saveSpeed?: number 9 | notePng: string 10 | se?: string 11 | seOk?: string 12 | } 13 | 14 | export interface Song { 15 | src: string 16 | bpm: number 17 | score: ScoreType[] 18 | fullCombo: number 19 | difficulty: string 20 | } 21 | 22 | export interface Option { 23 | speed: number 24 | } 25 | 26 | class Global { 27 | public noteWidth: number = 102 28 | public noteHeight: number = 102 29 | public noteWidthFlip: number = 125 30 | public scale: number = 3 31 | public saveSpeed: number = 12 32 | private readonly _notePng: string = '' 33 | 34 | public tapCanvas: HTMLCanvasElement = document.createElement('canvas') 35 | public longLoopCanvas: HTMLCanvasElement = document.createElement('canvas') 36 | public longMoveCanvas: HTMLCanvasElement = document.createElement('canvas') 37 | public longMoveWhiteCanvas: HTMLCanvasElement = document.createElement('canvas') 38 | public flipLeftCanvas: HTMLCanvasElement = document.createElement('canvas') 39 | public flipRightCanvas: HTMLCanvasElement = document.createElement('canvas') 40 | private _se: HTMLAudioElement | null = null 41 | private _seOk: HTMLAudioElement | null = null 42 | 43 | private static _instance: Global | null = null 44 | public static newImage (src: string): HTMLImageElement { 45 | const img = new Image() 46 | img.src = src 47 | return img 48 | } 49 | 50 | public static createAudio (src: string): HTMLAudioElement { 51 | const audio = new Audio(src) 52 | audio.preload = 'auto' 53 | return audio 54 | } 55 | 56 | constructor (options: GlobalConstructorArgument) { 57 | if (Global._instance) return Global._instance 58 | this.noteWidth = options.noteWidth || this.noteWidth 59 | this.noteHeight = options.noteHeight || this.noteHeight 60 | this.noteWidthFlip = options.noteWidthFlip || this.noteWidthFlip 61 | this.scale = options.scale || this.scale 62 | this.saveSpeed = options.saveSpeed || this.saveSpeed 63 | this._notePng = options.notePng 64 | 65 | this.tapCanvas.width = this.longLoopCanvas.width = this.longMoveCanvas.width = this.longMoveWhiteCanvas.width = this.tapCanvas.height = this.longLoopCanvas.height = this.longMoveCanvas.height = this.longMoveWhiteCanvas.height = this.flipLeftCanvas.height = this.flipRightCanvas.height = this.noteWidth 66 | this.flipLeftCanvas.width = this.flipRightCanvas.width = this.noteWidthFlip 67 | const iconNotesImg = Global.newImage(this._notePng) 68 | iconNotesImg.addEventListener('load', () => { 69 | (this.tapCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, 0, 0, this.noteWidth, this.noteHeight, 0, 0, this.noteWidth, this.noteHeight); 70 | (this.longLoopCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, this.noteWidth, 0, this.noteWidth, this.noteHeight, 0, 0, this.noteWidth, this.noteHeight); 71 | (this.longMoveCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, this.noteWidth * 2, 0, this.noteWidth, this.noteHeight, 0, 0, this.noteWidth, this.noteHeight); 72 | (this.longMoveWhiteCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, this.noteWidth * 3, 0, this.noteWidth, this.noteHeight, 0, 0, this.noteWidth, this.noteHeight); 73 | (this.flipLeftCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, this.noteWidth * 4, 0, this.noteWidthFlip, this.noteHeight, 0, 0, this.noteWidthFlip, this.noteHeight); 74 | (this.flipRightCanvas.getContext('2d') as CanvasRenderingContext2D).drawImage(iconNotesImg, this.noteWidth * 4 + this.noteWidthFlip, 0, this.noteWidthFlip, this.noteHeight, 0, 0, this.noteWidthFlip, this.noteHeight) 75 | }) 76 | 77 | if (options.se) this._se = Global.createAudio(options.se) 78 | if (options.seOk) this._seOk = Global.createAudio(options.seOk) 79 | 80 | Global._instance = this 81 | return this 82 | } 83 | 84 | get noteWidthDelta (): number { 85 | return this.noteWidthFlip - this.noteWidth 86 | } 87 | 88 | get noteWidthHalf (): number { 89 | return this.noteWidth / 2 90 | } 91 | 92 | get noteHeightHalf (): number { 93 | return this.noteHeight / 2 94 | } 95 | 96 | public getInstance (): Global { 97 | if (!Global._instance) throw new Error('Global instance null.') 98 | return Global._instance 99 | } 100 | 101 | public playSe (): void { 102 | if (this._se) { 103 | this._se.currentTime = 0 104 | this._se.play().catch(err => { 105 | console.error(err) 106 | error(`SCOREVIEWER playSe: ${err.stack}`) 107 | }) 108 | } 109 | } 110 | 111 | public playSeOk (): void { 112 | if (this._seOk) { 113 | this._seOk.currentTime = 0 114 | this._seOk.play().catch(err => { 115 | console.error(err) 116 | error(`SCOREVIEWER playSeOk: ${err.stack}`) 117 | }) 118 | } 119 | } 120 | } 121 | 122 | export const globalInstance = new Global({ 123 | notePng: '../../asset/img.asar/icon_notes.png', 124 | se: '../../asset/se.asar/se_common_cancel.mp3', 125 | seOk: '../../asset/se.asar/se_common_enter.mp3' 126 | }) 127 | 128 | export default Global 129 | -------------------------------------------------------------------------------- /app/src/ts/renderer/scoreviewer/long-move-note.ts: -------------------------------------------------------------------------------- 1 | import { globalInstance } from './global' 2 | import type { ScoreNote } from '../../main/on-score' 3 | import Note from './note' 4 | import ScoreViewer from './score-viewer' 5 | 6 | class LongMoveNote extends Note { 7 | constructor (note: ScoreNote, connectionNote?: ScoreNote, syncNote?: ScoreNote) { 8 | super(note) 9 | 10 | if (connectionNote) { 11 | this._connection = connectionNote 12 | } 13 | if (syncNote) { 14 | this._synchronizedNote = syncNote 15 | } 16 | } 17 | 18 | public drawConnection (sv: ScoreViewer): void { 19 | if (this._connection) { 20 | const connectionX = ScoreViewer.X[this._connection.finishPos - 1] 21 | const connectionY = ScoreViewer.calY(sv.options.speed, this._connection.sec, sv.audio.currentTime) 22 | sv.frontCtx.beginPath() 23 | sv.frontCtx.arc(this._x + globalInstance.noteWidthHalf, this._y + globalInstance.noteHeightHalf, globalInstance.noteWidthHalf, 0, Math.PI, true) 24 | const targetY = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? ScoreViewer.TOP_TO_TARGET_POSITION + globalInstance.noteHeightHalf : connectionY + globalInstance.noteHeightHalf 25 | const targetX = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? connectionX + ((this._x - connectionX) * (-(ScoreViewer.TOP_TO_TARGET_POSITION - connectionY)) / ((-(ScoreViewer.TOP_TO_TARGET_POSITION - connectionY)) + (ScoreViewer.TOP_TO_TARGET_POSITION - this._y))) : connectionX 26 | sv.frontCtx.lineTo(targetX, targetY) 27 | sv.frontCtx.arc(targetX + globalInstance.noteWidthHalf, targetY, globalInstance.noteWidthHalf, Math.PI, 2 * Math.PI, true) 28 | sv.frontCtx.lineTo(this._x + globalInstance.noteWidth, this._y + globalInstance.noteHeightHalf) 29 | sv.frontCtx.fill() 30 | if (connectionY > ScoreViewer.TOP_TO_TARGET_POSITION) sv.frontCtx.drawImage(globalInstance.longMoveWhiteCanvas, targetX, targetY - globalInstance.noteHeightHalf) 31 | } 32 | } 33 | 34 | public drawNote (sv: ScoreViewer): void { 35 | sv.frontCtx.drawImage(globalInstance.longMoveCanvas, this._x, this._y) 36 | } 37 | 38 | public saveDrawConnection (sv: ScoreViewer): void { 39 | if (this._connection) { 40 | const connectionX = ScoreViewer.X[this._connection.finishPos - 1] 41 | const connectionY = ScoreViewer.saveCalY(sv, this._connection.sec) 42 | sv.saveCtx.beginPath() 43 | sv.saveCtx.arc(this._x + globalInstance.noteWidthHalf, this._y + globalInstance.noteHeightHalf, globalInstance.noteWidthHalf, 0, Math.PI, true) 44 | const targetY = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? ScoreViewer.TOP_TO_TARGET_POSITION + globalInstance.noteHeightHalf : connectionY + globalInstance.noteHeightHalf 45 | const targetX = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? connectionX + ((this._x - connectionX) * (-(ScoreViewer.TOP_TO_TARGET_POSITION - connectionY)) / ((-(ScoreViewer.TOP_TO_TARGET_POSITION - connectionY)) + (ScoreViewer.TOP_TO_TARGET_POSITION - this._y))) : connectionX 46 | sv.saveCtx.lineTo(targetX, targetY) 47 | sv.saveCtx.arc(targetX + globalInstance.noteWidthHalf, targetY, globalInstance.noteWidthHalf, Math.PI, 2 * Math.PI, true) 48 | sv.saveCtx.lineTo(this._x + globalInstance.noteWidth, this._y + globalInstance.noteHeightHalf) 49 | sv.saveCtx.fill() 50 | if (connectionY > ScoreViewer.TOP_TO_TARGET_POSITION) sv.saveCtx.drawImage(globalInstance.longMoveWhiteCanvas, 0, 0, globalInstance.longMoveWhiteCanvas.width, globalInstance.longMoveWhiteCanvas.height, targetX, targetY - globalInstance.noteHeightHalf, globalInstance.longMoveWhiteCanvas.width / globalInstance.scale, globalInstance.longMoveWhiteCanvas.height / globalInstance.scale) 51 | } 52 | } 53 | 54 | public saveDrawNote (sv: ScoreViewer): void { 55 | sv.saveCtx.drawImage(globalInstance.longMoveCanvas, 0, 0, globalInstance.longMoveCanvas.width, globalInstance.longMoveCanvas.height, this._x, this._y, globalInstance.longMoveCanvas.width / globalInstance.scale, globalInstance.longMoveCanvas.height / globalInstance.scale) 56 | } 57 | } 58 | 59 | export default LongMoveNote 60 | -------------------------------------------------------------------------------- /app/src/ts/renderer/scoreviewer/long-note.ts: -------------------------------------------------------------------------------- 1 | import { globalInstance } from './global' 2 | import type { ScoreNote } from '../../main/on-score' 3 | import Note from './note' 4 | import ScoreViewer from './score-viewer' 5 | 6 | class LongNote extends Note { 7 | constructor (note: ScoreNote, connectionNote?: ScoreNote, syncNote?: ScoreNote) { 8 | super(note) 9 | 10 | if (connectionNote) { 11 | this._connection = connectionNote 12 | } 13 | if (syncNote) { 14 | this._synchronizedNote = syncNote 15 | } 16 | } 17 | 18 | public drawConnection (sv: ScoreViewer): void { 19 | if (this._connection) { 20 | const connectionY = ScoreViewer.calY(sv.options.speed, this._connection.sec, sv.audio.currentTime) 21 | sv.frontCtx.beginPath() 22 | sv.frontCtx.arc(this._x + globalInstance.noteWidthHalf, this._y + globalInstance.noteHeightHalf, globalInstance.noteWidthHalf, 0, Math.PI, true) 23 | const targetY = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? ScoreViewer.TOP_TO_TARGET_POSITION + globalInstance.noteHeightHalf : connectionY + globalInstance.noteHeightHalf 24 | sv.frontCtx.lineTo(this._x, targetY) 25 | sv.frontCtx.arc(this._x + globalInstance.noteWidthHalf, targetY, globalInstance.noteWidthHalf, Math.PI, 2 * Math.PI, true) 26 | sv.frontCtx.lineTo(this._x + globalInstance.noteWidth, this._y + globalInstance.noteHeightHalf) 27 | sv.frontCtx.fill() 28 | } 29 | } 30 | 31 | public drawNote (sv: ScoreViewer): void { 32 | sv.frontCtx.drawImage(globalInstance.longLoopCanvas, this._x, this._y) 33 | } 34 | 35 | public saveDrawConnection (sv: ScoreViewer): void { 36 | if (this._connection) { 37 | const connectionY = ScoreViewer.saveCalY(sv, this._connection.sec) 38 | sv.saveCtx.beginPath() 39 | sv.saveCtx.arc(this._x + globalInstance.noteWidthHalf, this._y + globalInstance.noteHeightHalf, globalInstance.noteWidthHalf, 0, Math.PI, true) 40 | const targetY = connectionY > ScoreViewer.TOP_TO_TARGET_POSITION ? ScoreViewer.TOP_TO_TARGET_POSITION + globalInstance.noteHeightHalf : connectionY + globalInstance.noteHeightHalf 41 | sv.saveCtx.lineTo(this._x, targetY) 42 | sv.saveCtx.arc(this._x + globalInstance.noteWidthHalf, targetY, globalInstance.noteWidthHalf, Math.PI, 2 * Math.PI, true) 43 | sv.saveCtx.lineTo(this._x + globalInstance.noteWidth, this._y + globalInstance.noteHeightHalf) 44 | sv.saveCtx.fill() 45 | } 46 | } 47 | 48 | public saveDrawNote (sv: ScoreViewer): void { 49 | sv.saveCtx.drawImage(globalInstance.longLoopCanvas, 0, 0, globalInstance.longLoopCanvas.width, globalInstance.longLoopCanvas.height, this._x, this._y, globalInstance.longLoopCanvas.width / globalInstance.scale, globalInstance.longLoopCanvas.height / globalInstance.scale) 50 | } 51 | } 52 | 53 | export default LongNote 54 | -------------------------------------------------------------------------------- /app/src/ts/renderer/scoreviewer/note.ts: -------------------------------------------------------------------------------- 1 | import { globalInstance } from './global' 2 | import type { ScoreNote } from '../../main/on-score' 3 | import ScoreViewer from './score-viewer' 4 | 5 | export interface ScoreNoteWithNoteInstance extends ScoreNote { 6 | _instance?: Note 7 | } 8 | 9 | abstract class Note { 10 | protected _sec: number 11 | protected _x: number 12 | protected _y: number 13 | protected _connection: ScoreNoteWithNoteInstance | null 14 | protected _synchronizedNote: ScoreNoteWithNoteInstance | null 15 | private readonly _connectionHeight = 12 16 | 17 | constructor (note: ScoreNote) { 18 | this._sec = note.sec 19 | this._x = ScoreViewer.X[note.finishPos - 1] 20 | this._y = -globalInstance.noteHeight 21 | this._connection = null 22 | this._synchronizedNote = null 23 | } 24 | 25 | public setY (y: number): void { 26 | this._y = y 27 | } 28 | 29 | public getX (): number { 30 | return this._x 31 | } 32 | 33 | public setX (x: number): void { 34 | this._x = x 35 | } 36 | 37 | public saveDrawSync (sv: ScoreViewer): void { 38 | if (!this._synchronizedNote) return 39 | 40 | const syncX = ScoreViewer.X[this._synchronizedNote.finishPos - 1] + globalInstance.noteWidthHalf 41 | const syncY = ScoreViewer.saveCalY(sv, this._sec) + globalInstance.noteHeightHalf - this._connectionHeight / 2 / globalInstance.scale 42 | const selfX = this._x + globalInstance.noteWidthHalf + (ScoreViewer.X.includes(this._x) ? 0 : globalInstance.noteWidthDelta) 43 | sv.saveCtx.fillRect((selfX < syncX ? selfX : syncX) + globalInstance.noteWidthHalf, syncY, (selfX < syncX ? syncX - selfX : selfX - syncX) - globalInstance.noteWidth, this._connectionHeight / globalInstance.scale) 44 | } 45 | 46 | public drawSync (sv: ScoreViewer): void { 47 | if (!this._synchronizedNote) return 48 | 49 | const syncX = ScoreViewer.X[this._synchronizedNote.finishPos - 1] + globalInstance.noteWidthHalf 50 | const syncY = ScoreViewer.calY(sv.options.speed, this._sec, sv.audio.currentTime) + globalInstance.noteHeightHalf - this._connectionHeight / 2 51 | const selfX = this._x + globalInstance.noteWidthHalf + (ScoreViewer.X.includes(this._x) ? 0 : globalInstance.noteWidthDelta) 52 | sv.frontCtx.fillRect((selfX < syncX ? selfX : syncX) + globalInstance.noteWidthHalf, syncY, (selfX < syncX ? syncX - selfX : selfX - syncX) - globalInstance.noteWidth, this._connectionHeight) 53 | } 54 | 55 | public isNeedDraw (): boolean { 56 | if (this._y < -globalInstance.noteHeight) { 57 | if (!this._connection) return false 58 | return (this._connection._instance as Note)._y >= -globalInstance.noteHeight 59 | } 60 | return this._y < ScoreViewer.TOP_TO_TARGET_POSITION 61 | } 62 | 63 | public abstract saveDrawConnection (sv: ScoreViewer): void 64 | public abstract saveDrawNote (sv: ScoreViewer): void 65 | public abstract drawConnection (sv: ScoreViewer): void 66 | public abstract drawNote (sv: ScoreViewer): void 67 | } 68 | 69 | export default Note 70 | -------------------------------------------------------------------------------- /app/src/ts/renderer/scoreviewer/tap-note.ts: -------------------------------------------------------------------------------- 1 | import { globalInstance } from './global' 2 | import type { ScoreNote } from '../../main/on-score' 3 | import Note from './note' 4 | import type ScoreViewer from './score-viewer' 5 | 6 | class TapNote extends Note { 7 | constructor (note: ScoreNote, syncNote?: ScoreNote) { 8 | super(note) 9 | if (syncNote) this._synchronizedNote = syncNote 10 | } 11 | 12 | public drawConnection (_sv: ScoreViewer): void { 13 | // empty 14 | } 15 | 16 | public drawNote (sv: ScoreViewer): void { 17 | sv.frontCtx.drawImage(globalInstance.tapCanvas, this._x, this._y) 18 | } 19 | 20 | public saveDrawConnection (_sv: ScoreViewer): void { 21 | // empty 22 | } 23 | 24 | public saveDrawNote (sv: ScoreViewer): void { 25 | sv.saveCtx.drawImage(globalInstance.tapCanvas, 0, 0, globalInstance.tapCanvas.width, globalInstance.tapCanvas.height, this._x, this._y, globalInstance.tapCanvas.width / globalInstance.scale, globalInstance.tapCanvas.height / globalInstance.scale/* ScoreViewer.calY(sv.options.speed, this._sec, 0) */) 26 | } 27 | } 28 | 29 | export default TapNote 30 | -------------------------------------------------------------------------------- /app/src/ts/renderer/socket.ts: -------------------------------------------------------------------------------- 1 | import { Socket } from 'net' 2 | 3 | export function connect (): void { 4 | const client = new Socket() 5 | client.on('data', msg => { 6 | if (msg.toString() === 'reload') { 7 | client.destroy() 8 | location.reload() 9 | } 10 | }) 11 | 12 | client.on('error', (err) => console.log(err)) 13 | client.connect(3461, 'localhost', () => { 14 | console.log('Socket connect localhost:3461') 15 | client.write('mishiro') 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /app/src/ts/renderer/store.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import type { MasterData } from './back/on-master-read' 5 | import type { BGM, Live } from './back/resolve-audio-manifest' 6 | 7 | // const { ipcRenderer } = window.node.electron 8 | 9 | Vue.use(Vuex) 10 | 11 | export enum Action { 12 | SET_RES_VER = 'SET_RES_VER', 13 | SET_LATEST_RES_VER = 'SET_LATEST_RES_VER', 14 | SET_MASTER = 'SET_MASTER', 15 | SET_BATCH_DOWNLOADING = 'SET_BATCH_DOWNLOADING', 16 | SET_BATCH_STATUS = 'SET_BATCH_STATUS', 17 | SET_AUDIO_LIST = 'SET_AUDIO_LIST', 18 | } 19 | 20 | const store = new Vuex.Store<{ 21 | resVer: number 22 | latestResVer: number 23 | master: Partial 24 | audioListData: BGM[] | Live[] 25 | // batchDownloading: boolean 26 | batchStatus: { 27 | name: string 28 | status: string 29 | status2: string 30 | curprog: number 31 | totalprog: number 32 | } 33 | }>({ 34 | state: { 35 | resVer: -1, 36 | latestResVer: -1, 37 | master: {}, 38 | audioListData: [], 39 | 40 | // batchDownloading: false, 41 | batchStatus: { 42 | name: '', 43 | status: '', 44 | status2: '', 45 | curprog: 0, 46 | totalprog: 0 47 | } 48 | }, 49 | mutations: { 50 | [Action.SET_RES_VER] (state, payload) { 51 | state.resVer = payload 52 | }, 53 | [Action.SET_LATEST_RES_VER] (state, payload) { 54 | state.latestResVer = payload 55 | }, 56 | [Action.SET_MASTER] (state, payload) { 57 | state.master = payload 58 | }, 59 | // [Action.SET_BATCH_DOWNLOADING] (state, payload) { 60 | // state.batchDownloading = payload 61 | // }, 62 | [Action.SET_BATCH_STATUS] (state, status) { 63 | state.batchStatus.name = status.name ?? '' 64 | state.batchStatus.status = status.status ?? '' 65 | state.batchStatus.status2 = status.status2 ?? '' 66 | state.batchStatus.curprog = status.curprog ?? 0 67 | state.batchStatus.totalprog = status.totalprog ?? 0 68 | }, 69 | [Action.SET_AUDIO_LIST] (state, list) { 70 | state.audioListData = list 71 | } 72 | } 73 | }) 74 | 75 | // ipcRenderer.on('setBatchDownloading', (_event, payload) => { 76 | // store.commit(Action.SET_BATCH_DOWNLOADING, payload) 77 | // }) 78 | 79 | // ipcRenderer.on('setBatchStatus', (_event, status) => { 80 | // store.commit(Action.SET_BATCH_STATUS, status) 81 | // }) 82 | 83 | export function setResVer (resVer: number): void { 84 | store.commit(Action.SET_RES_VER, resVer) 85 | } 86 | 87 | export function setLatestResVer (resVer: number): void { 88 | store.commit(Action.SET_LATEST_RES_VER, resVer) 89 | } 90 | 91 | export function setMaster (master: MasterData): void { 92 | store.commit(Action.SET_MASTER, master) 93 | setAudioList(master.bgmManifest) 94 | } 95 | 96 | export function setAudioList (list: BGM[] | Live[]): void { 97 | store.commit(Action.SET_AUDIO_LIST, list) 98 | } 99 | 100 | export default store 101 | -------------------------------------------------------------------------------- /app/src/ts/renderer/tab-small.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component, Prop, Model } from 'vue-property-decorator' 2 | @Component 3 | export default class extends Vue { 4 | @Model('tabClicked') value: string 5 | @Prop({ required: true, default: () => ({}), type: Object }) tab: any 6 | @Prop({ default: false }) noTranslation: boolean 7 | @Prop({ default: 18 }) fontSize: number 8 | /* model: { 9 | prop: 'value', 10 | event: 'tabClicked' 11 | }, */ 12 | // currentActive: string = this.value 13 | 14 | /* playSe: Function 15 | enterSe: HTMLAudioElement */ 16 | 17 | liClick (item: string): void { 18 | if (item !== this.value) { 19 | this.playSe(this.enterSe) 20 | this.$emit('tabClicked', item) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/src/ts/renderer/the-background.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | @Component 3 | export default class extends Vue { 4 | bg: boolean | null = null 5 | backgroundId: boolean | number = false 6 | 7 | mounted (): void { 8 | this.$nextTick(() => { 9 | this.bg = (window.innerWidth / window.innerHeight >= 1280 / 824) 10 | window.addEventListener('resize', () => { 11 | this.bg = (window.innerWidth / window.innerHeight >= 1280 / 824) 12 | }, false) 13 | this.event.$on('eventBgReady', (cardId: number) => { 14 | this.backgroundId = cardId 15 | }) 16 | this.event.$on('idolSelect', (cardId: number) => { 17 | this.backgroundId = cardId 18 | }) 19 | this.event.$on('noBg', () => { 20 | this.backgroundId = false 21 | }) 22 | }) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/src/ts/renderer/the-footer.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component, Prop } from 'vue-property-decorator' 2 | @Component 3 | export default class extends Vue { 4 | @Prop() value: string 5 | 6 | blocks: string[] = ['home', 'idol', 'commu', 'live', 'menu'] 7 | 8 | input (block: string): void { 9 | if (block !== this.value) { 10 | // if (block === 'gacha') { 11 | // this.event.$emit('alert', this.$t('home.errorTitle'), 'GACHA part has been disabled.') 12 | // return 13 | // } 14 | this.event.$emit('changeBgm', block) 15 | this.playSe(this.enterSe) 16 | this.$emit('input', block) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/ts/renderer/the-table.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component, Prop, Watch } from 'vue-property-decorator' 2 | @Component 3 | export default class extends Vue { 4 | @Prop({ 5 | type: Array, 6 | default: () => ([]) 7 | }) 8 | data: ResourceData[] 9 | 10 | @Prop({ type: Function, required: true }) 11 | isDisabled: (obj: ResourceData) => boolean 12 | 13 | @Prop({ 14 | type: Function, 15 | default: (_key: string, value: any) => { 16 | return value 17 | } 18 | }) 19 | formatter: (key: string, value: any) => string 20 | 21 | @Prop({ 22 | type: Function, 23 | default: (key: string) => key 24 | }) 25 | headerFormatter: (key: string) => string 26 | 27 | selected: ResourceData[] = [] 28 | selectAll: boolean = false 29 | 30 | change (selected: ResourceData[]): void { 31 | this.$emit('change', selected) 32 | } 33 | 34 | @Watch('selectAll') 35 | selectAllWatchHandler (val: boolean): void { 36 | if (val) { 37 | this.selected = [] 38 | for (let i = 0; i < this.data.length; i++) { 39 | if (!this.isDisabled(this.data[i])) { 40 | this.selected.push(this.data[i]) 41 | } 42 | } 43 | } else { 44 | this.selected = [] 45 | } 46 | this.$emit('change', this.selected) 47 | } 48 | 49 | mounted (): void { 50 | this.$nextTick(() => { 51 | this.event.$on('completeTask', (hash: string, disable: boolean = true) => { 52 | for (let i = 0; i < this.selected.length; i++) { 53 | if (this.selected[i].hash === hash) { 54 | if (disable) { 55 | const o = document.getElementById(hash) 56 | if (o) o.setAttribute('disabled', 'disabled') 57 | } 58 | this.selected.splice(i, 1) 59 | break 60 | } 61 | } 62 | }) 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/src/ts/renderer/the-toggle-button.ts: -------------------------------------------------------------------------------- 1 | import { Vue, Component } from 'vue-property-decorator' 2 | @Component 3 | export default class extends Vue { 4 | toggle (): void { 5 | this.playSe(this.cancelSe) 6 | this.$emit('toggle') 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/src/ts/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "include": [ 4 | "./**/*", 5 | "../renderer.ts", 6 | "../typings/**/*", 7 | "../../vue/**/*" 8 | ], 9 | "exclude": [ 10 | "../../vue/modal/ModalGachaInformation.vue" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /app/src/ts/renderer/typings/main.d.ts: -------------------------------------------------------------------------------- 1 | import getPath from '../../main/get-path' 2 | export { Configurer } from '../../main/config' 3 | export { Client } from 'mishiro-core' 4 | export { getPath } 5 | -------------------------------------------------------------------------------- /app/src/ts/renderer/typings/sfc.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { Vue } from 'vue-property-decorator' 3 | export default Vue 4 | } 5 | -------------------------------------------------------------------------------- /app/src/ts/renderer/typings/vue.d.ts: -------------------------------------------------------------------------------- 1 | import VueI18n from 'vue-i18n' 2 | import type { MishiroAudio } from '../audio' 3 | 4 | declare module 'vue/types/vue' { 5 | interface Vue { 6 | _i18n: { _vm: VueI18n } 7 | event: Vue 8 | 9 | // bgm: HTMLAudioElement 10 | bgm: MishiroAudio 11 | enterSe: HTMLAudioElement 12 | cancelSe: HTMLAudioElement 13 | 14 | core: typeof import('mishiro-core') 15 | 16 | createCardIconTask: (cardIdArr: number[]) => string[][] 17 | handleClientError: (err: Error, ignore?: boolean) => void 18 | playSe: (se: HTMLAudioElement) => void 19 | // getBgmUrl: (hash: string) => string 20 | // getLiveUrl: (hash: string) => string 21 | // getVoiceUrl: (hash: string) => string 22 | // getAcbUrl: (bORl: string, hash: string) => string 23 | // getUnityUrl: (hash: string) => string 24 | // getDbUrl: (hash: string) => string 25 | // getCardUrl: (id: string | number) => string 26 | getIconUrl: (id: string | number) => string 27 | mainWindowId: number 28 | acb2mp3: (acbPath: string, rename?: string, onProgress?: (current: number, total: number, prog: import('mishiro-core').ProgressInfo) => void) => Promise 29 | acb2wav: (acbPath: string, rename?: string, onProgress?: (current: number, total: number, filename: string) => void) => Promise 30 | acb2aac: (acbPath: string, rename?: string, onProgress?: (current: number, total: number, prog: import('mishiro-core').ProgressInfo) => void) => Promise 31 | // Downloader: typeof Downloader 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/ts/renderer/unpack-texture-2d.ts: -------------------------------------------------------------------------------- 1 | const fs = window.node.fs 2 | // import getPath from './get-path' 3 | // import { format } from 'url' 4 | 5 | // let id = ipcRenderer.sendSync('mainWindowId') 6 | 7 | // let win: BrowserWindow | null = new remote.BrowserWindow({ width: 346, height: 346, show: true/* process.env.NODE_ENV !== 'production' */, parent: remote.BrowserWindow.fromId(id) }) 8 | 9 | // if (process.env.NODE_ENV === 'production') { 10 | // win.loadURL(format({ 11 | // pathname: getPath('./public/back.html'), 12 | // protocol: 'file:', 13 | // slashes: true 14 | // })) 15 | // } else { 16 | // const { devServerHost, devServerPort, publicPath } = require('../../../script/config.json') 17 | // win.loadURL(`http://${devServerHost}:${devServerPort}${publicPath}back.html`) 18 | // } 19 | 20 | // window.addEventListener('beforeunload', () => { 21 | // if (win) win.close() 22 | // win = null 23 | // }) 24 | 25 | export function unpackTexture2D (assetbundle: string): Promise { 26 | return new Promise((resolve, reject) => { 27 | // let randomId = (Math.round(Math.random() * 346346) + new Date().getTime()).toString() 28 | // ipcRenderer.once(randomId, (_event: Event, err: { message: string; stack?: string } | null, pngs: string[]) => { 29 | // if (err) { 30 | // const newErr = new Error() 31 | // newErr.message = err.message 32 | // newErr.stack = err.stack 33 | // return reject(newErr) 34 | // } 35 | // resolve(pngs) 36 | // }) 37 | // console.log(assetbundle); 38 | // (win as BrowserWindow).webContents.send('texture2d', assetbundle, randomId, id) 39 | window.node.mishiroCore.util.unpackTexture2D(assetbundle).then((result: string[]) => { 40 | fs.removeSync(assetbundle) 41 | resolve(result) 42 | }).catch((e: Error) => { 43 | // eslint-disable-next-line prefer-promise-reject-errors 44 | reject({ 45 | message: e.message, 46 | stack: e.stack 47 | }) 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /app/src/ts/renderer/updater.ts: -------------------------------------------------------------------------------- 1 | import { ipcRenderer, Event, IpcRendererEvent } from 'electron' 2 | import type * as ElectronGithubAsarUpdater from 'electron-github-asar-updater' 3 | 4 | class Updater { 5 | private _onDownloadProgress: ((event: IpcRendererEvent, data: ElectronGithubAsarUpdater.ProgressData) => void) | null = null 6 | 7 | public relaunch (): void { 8 | return ipcRenderer.sendSync('updater#relaunch') 9 | } 10 | 11 | public getUpdateInfo (): ElectronGithubAsarUpdater.Info { 12 | return ipcRenderer.sendSync('updater#getUpdateInfo') 13 | } 14 | 15 | public abort (): void { 16 | return ipcRenderer.sendSync('updater#abort') 17 | } 18 | 19 | public async check (options?: ElectronGithubAsarUpdater.CheckOptions): Promise { 20 | return ipcRenderer.invoke('updater#check', options) 21 | } 22 | 23 | public async download (): Promise { 24 | return ipcRenderer.invoke('updater#download') 25 | } 26 | 27 | public onDownload (onDownloadProgress: ElectronGithubAsarUpdater.OnProgressCallback | null): void { 28 | const channel = 'updater#onDownloadProgress' 29 | if (this._onDownloadProgress) { 30 | ipcRenderer.off(channel, this._onDownloadProgress) 31 | this._onDownloadProgress = null 32 | } 33 | if (onDownloadProgress) { 34 | this._onDownloadProgress = function (_event: Event, data: ElectronGithubAsarUpdater.ProgressData) { 35 | onDownloadProgress(data) 36 | } 37 | ipcRenderer.on(channel, this._onDownloadProgress) 38 | } 39 | } 40 | } 41 | 42 | const updater = new Updater() 43 | 44 | export default updater 45 | -------------------------------------------------------------------------------- /app/src/ts/renderer/vue-global.ts: -------------------------------------------------------------------------------- 1 | import type { PluginFunction } from 'vue' 2 | import getPath from '../common/get-path' 3 | import { MishiroAudio } from './audio' 4 | import { error } from './log' 5 | const { ipcRenderer } = window.node.electron 6 | const fs = window.node.fs 7 | const path = window.node.path 8 | 9 | const { iconDir } = getPath 10 | 11 | // const gameHostBase = 'http://storage.game.starlight-stage.jp/dl/resources' 12 | const imgHostBase = 'https://hidamarirhodonite.kirara.ca' 13 | // const getBgmUrl = (hash: string) => `${gameHostBase}/High/Sound/Common/b/${hash}` 14 | // const getLiveUrl = (hash: string) => `${gameHostBase}/High/Sound/Common/l/${hash}` 15 | // const getVoiceUrl = (hash: string) => `${gameHostBase}/High/Sound/Common/v/${hash}` 16 | // const getAcbUrl = (bORl: string, hash: string) => `${gameHostBase}/High/Sound/Common/${bORl}/${hash}` 17 | // const getUnityUrl = (hash: string) => `${gameHostBase}/High/AssetBundles/Android/${hash}` 18 | // const getDbUrl = (hash: string) => `${gameHostBase}/Generic/${hash}` 19 | // const getCardUrl = (id: string | number) => `${imgHostBase}/spread/${id}.png` 20 | const getIconUrl = (id: string | number): string => `${imgHostBase}/icon_card/${id}.png` 21 | 22 | const install: PluginFunction = function (Vue) { 23 | // 全局属性 24 | Vue.prototype.event = new Vue({}) // 全局事件总站 25 | // Vue.prototype.bgm = new Audio() // 背景音乐 26 | Vue.prototype.bgm = new MishiroAudio() // 背景音乐 27 | Vue.prototype.enterSe = new Audio('../../asset/se.asar/se_common_enter.mp3') // 确认音效 28 | Vue.prototype.cancelSe = new Audio('../../asset/se.asar/se_common_cancel.mp3') // 取消音效 29 | Vue.prototype.core = window.node.mishiroCore 30 | 31 | // 全局方法 32 | Vue.prototype.handleClientError = function (err: Error, ignore?: boolean) { 33 | if (err.message.includes('Error: 203')) { 34 | this.event.$emit('alert', this.$t('home.errorTitle'), this.$t('home.accountBannedMessage')) 35 | } else { 36 | if (!ignore) { 37 | this.event.$emit('alert', this.$t('home.errorTitle'), err.message) 38 | } 39 | } 40 | } 41 | Vue.prototype.playSe = function (se: HTMLAudioElement) { // 播放音效 42 | se.currentTime = 0 43 | setTimeout(() => { 44 | se.play().catch(err => { 45 | console.error(err) 46 | error(`playSe: ${err.stack}`) 47 | }) 48 | }, 0) 49 | } 50 | // Vue.prototype.createCardBackgroundTask = function (cardIdArr: number[]) { 51 | // let task = [] 52 | // for (let i = 0; i < cardIdArr.length; i++) { 53 | // task.push([getCardUrl(cardIdArr[i]), cardDir(`bg_${cardIdArr[i]}.png`)]) 54 | // } 55 | // return task 56 | // } 57 | Vue.prototype.createCardIconTask = function (cardIdArr: number[]) { 58 | const task = [] 59 | for (let i = 0; i < cardIdArr.length; i++) { 60 | task.push([getIconUrl(cardIdArr[i]), iconDir(`card_${cardIdArr[i]}_m.png`)]) 61 | } 62 | return task 63 | } 64 | // Vue.prototype.getBgmUrl = getBgmUrl 65 | // Vue.prototype.getLiveUrl = getLiveUrl 66 | // Vue.prototype.getVoiceUrl = getVoiceUrl 67 | // Vue.prototype.getAcbUrl = getAcbUrl 68 | // Vue.prototype.getUnityUrl = getUnityUrl 69 | // Vue.prototype.getDbUrl = getDbUrl 70 | // Vue.prototype.getCardUrl = getCardUrl 71 | Vue.prototype.getIconUrl = getIconUrl 72 | Vue.prototype.mainWindowId = ipcRenderer.sendSync('mainWindowId') 73 | Vue.prototype.acb2mp3 = async function (acbPath: string, rename?: string, onProgress?: (current: number, total: number, prog: import('mishiro-core').ProgressInfo) => void) { 74 | const mp3list = await window.node.mishiroCore.audio.acb2mp3(acbPath, undefined, onProgress) 75 | const mp3 = mp3list[0] 76 | const dest = path.join(path.dirname(acbPath), rename || path.basename(mp3)) 77 | const awbPath = path.join(path.dirname(acbPath), path.parse(acbPath).name + '.awb') 78 | await fs.move(mp3, dest) 79 | await Promise.all([ 80 | fs.remove(path.dirname(mp3)), 81 | fs.remove(acbPath), 82 | fs.existsSync(awbPath) ? fs.remove(awbPath) : Promise.resolve() 83 | ]) 84 | return dest 85 | } 86 | Vue.prototype.acb2wav = async function (acbPath: string, rename?: string, onProgress?: (current: number, total: number, filename: string) => void) { 87 | const wavList = await window.node.mishiroCore.audio.acb2wav(acbPath, onProgress) 88 | const wav = wavList[0] 89 | const dest = path.join(path.dirname(acbPath), rename || path.basename(wav)) 90 | const awbPath = path.join(path.dirname(acbPath), path.parse(acbPath).name + '.awb') 91 | await fs.move(wav, dest) 92 | await Promise.all([ 93 | fs.remove(path.dirname(wav)), 94 | fs.remove(acbPath), 95 | fs.existsSync(awbPath) ? fs.remove(awbPath) : Promise.resolve() 96 | ]) 97 | return dest 98 | } 99 | Vue.prototype.acb2aac = async function (acbPath: string, rename?: string, onProgress?: (current: number, total: number, prog: import('mishiro-core').ProgressInfo) => void) { 100 | const aaclist = await window.node.mishiroCore.audio.acb2aac(acbPath, undefined, onProgress) 101 | const aac = aaclist[0] 102 | const dest = path.join(path.dirname(acbPath), rename || path.basename(aac)) 103 | const awbPath = path.join(path.dirname(acbPath), path.parse(acbPath).name + '.awb') 104 | await fs.move(aac, dest) 105 | await Promise.all([ 106 | fs.remove(path.dirname(aac)), 107 | fs.remove(acbPath), 108 | fs.existsSync(awbPath) ? fs.remove(awbPath) : Promise.resolve() 109 | ]) 110 | return dest 111 | } 112 | } 113 | 114 | export default { 115 | install 116 | } 117 | -------------------------------------------------------------------------------- /app/src/ts/template/back.template.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ` 11 | -------------------------------------------------------------------------------- /app/src/ts/template/game.template.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | ` 12 | -------------------------------------------------------------------------------- /app/src/ts/template/index.template.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | mishiro 5 | 6 | 7 | 8 |
9 | 10 | 11 | ` 12 | -------------------------------------------------------------------------------- /app/src/ts/template/score.template.ts: -------------------------------------------------------------------------------- 1 | export default ` 2 | 3 | 4 | 5 | 6 | 41 | 42 | 43 | 44 |
45 |
46 | 0 47 | combo 48 |
49 | ${process.env.NODE_ENV === 'production' ? '' : ''} 50 | 51 | 52 | ` 53 | -------------------------------------------------------------------------------- /app/src/ts/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | declare interface IBatchError { 2 | path: string 3 | code: number 4 | message: string 5 | } 6 | 7 | declare const MISHIRO_DEV_SERVER_PORT: number 8 | 9 | declare module 'mishiro-core/util/proxy' { 10 | export function getProxyAgent (proxy?: string): { 11 | http?: import('http').Agent 12 | https?: import('https').Agent 13 | } | false 14 | } 15 | -------------------------------------------------------------------------------- /app/src/ts/typings/manifest.d.ts: -------------------------------------------------------------------------------- 1 | declare interface ManifestResouce { 2 | name: string 3 | hash: string 4 | attr: number 5 | category: string 6 | size: number 7 | decrypt_key: null | Buffer 8 | } 9 | 10 | declare type ResourceData = Pick 11 | -------------------------------------------------------------------------------- /app/src/ts/typings/non-webpack-require.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/naming-convention 2 | declare function __non_webpack_require__ (moduleName: string): any 3 | -------------------------------------------------------------------------------- /app/src/ts/typings/preload.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | node: { 3 | process: typeof process 4 | electron: typeof import('electron') 5 | tybys: { 6 | downloader: typeof import('@tybys/downloader') 7 | } 8 | mishiroCore: typeof import('mishiro-core') 9 | acb: typeof import('acb') 10 | hcaDecoder: typeof import('hca-decoder') 11 | events: typeof import('events') 12 | fs: typeof import('fs-extra') 13 | os: typeof import('os') 14 | path: typeof import('path') 15 | url: typeof import('url') 16 | childProcess: typeof import('child_process') 17 | iconvLite: typeof import('iconv-lite') 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/src/vue/Mishiro.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /app/src/vue/MishiroGame.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /app/src/vue/component/InputRadio.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 24 | 25 | 60 | -------------------------------------------------------------------------------- /app/src/vue/component/InputText.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 44 | 45 | 67 | -------------------------------------------------------------------------------- /app/src/vue/component/ProgressBar.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 73 | -------------------------------------------------------------------------------- /app/src/vue/component/StaticTitleDot.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | -------------------------------------------------------------------------------- /app/src/vue/component/TabSmall.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 14 | 15 | 59 | -------------------------------------------------------------------------------- /app/src/vue/component/TaskLoading.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | 27 | 37 | -------------------------------------------------------------------------------- /app/src/vue/component/TheBackground.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 23 | 24 | 91 | -------------------------------------------------------------------------------- /app/src/vue/component/TheCombo.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /app/src/vue/component/TheFooter.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 11 | 12 | 74 | -------------------------------------------------------------------------------- /app/src/vue/component/TheLiveGauge.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 50 | -------------------------------------------------------------------------------- /app/src/vue/component/ThePlayer.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 25 | 26 | 111 | -------------------------------------------------------------------------------- /app/src/vue/component/TheTable.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 20 | 21 | 99 | -------------------------------------------------------------------------------- /app/src/vue/component/TheToggleButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 7 | 8 | 23 | -------------------------------------------------------------------------------- /app/src/vue/component/TheVersion.vue: -------------------------------------------------------------------------------- 1 | 4 | 13 | 20 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalAbout.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 38 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalAlert.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 75 | 76 | 129 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalBatchError.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 31 | 32 | 58 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalGachaCard.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 40 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalGachaHistory.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | 48 | 65 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalGachaInformation.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 96 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalLiveDifficulty.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 34 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalLiveResult.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 61 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalScore.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 36 | -------------------------------------------------------------------------------- /app/src/vue/modal/ModalVersion.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroCommu.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 20 | 21 | 55 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroEntry.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroGacha.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 37 | 38 | 105 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroHome.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 40 | 41 | 132 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroIdol.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 41 | 42 | 119 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroMenu.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 19 | 20 | 47 | -------------------------------------------------------------------------------- /app/src/vue/view/MishiroUpdate.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 38 | 39 | 41 | -------------------------------------------------------------------------------- /app/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "noImplicitReturns": true, 5 | "moduleResolution": "node", 6 | "noUnusedLocals": true, 7 | "noUnusedParameters": true, 8 | "noImplicitAny": true, 9 | "importsNotUsedAsValues": "error", 10 | "noEmitHelpers": true, 11 | "importHelpers": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | "strictPropertyInitialization": false, 6 | "module": "esnext", 7 | "target": "es2019", 8 | "sourceMap": true, 9 | "baseUrl": ".", 10 | "paths": { 11 | "@/*": [ 12 | "src/*" 13 | ] 14 | } 15 | }, 16 | "include": [ 17 | "./src/ts/**/*", 18 | "./src/vue/**/*" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /dist/build.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | set version=%1 4 | set arch=%2 5 | 6 | if "%version%"=="" ( 7 | echo Require target version. 8 | goto end 9 | ) 10 | 11 | if "%arch%"=="x64" ( 12 | makensis /DPRODUCT_VERSION=%version% /DLIBRARY_X64 mishiro.nsi 13 | ) else ( 14 | makensis /DPRODUCT_VERSION=%version% mishiro.nsi 15 | ) 16 | 17 | :end 18 | -------------------------------------------------------------------------------- /dist/mishiro.nsi: -------------------------------------------------------------------------------- 1 | !include "MUI.nsh" 2 | !include "x64.nsh" 3 | !include "Library.nsh" 4 | 5 | !define PRODUCT_NAME "mishiro" 6 | !define PRODUCT_PUBLISHER "Toyobayashi" 7 | !define PRODUCT_WEB_SITE "https://github.com/toyobayashi/mishiro" 8 | !define PRODUCT_UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" 9 | !define PRODUCT_UNINST_ROOT_KEY "HKLM" 10 | !define PRODUCT_STARTMENU_REGVAL "NSIS:StartMenuDir" 11 | 12 | !ifdef LIBRARY_X64 13 | !define PROGRAM_FILES_MAP $PROGRAMFILES64 14 | !define PRODUCT_ARCH "x64" 15 | !else 16 | !define PROGRAM_FILES_MAP $PROGRAMFILES 17 | !define PRODUCT_ARCH "ia32" 18 | !endif 19 | 20 | !macro TIP_WHEN_AMD64_INSTALLER_RUNAT_X86 21 | !ifdef LIBRARY_X64 22 | ${If} ${RunningX64} 23 | ${Else} 24 | MessageBox MB_ICONINFORMATION|MB_OK "Please run this installer on x64 machines." 25 | Abort 26 | ${EndIf} 27 | !endif 28 | !macroend 29 | 30 | SetCompressor lzma 31 | 32 | !define MUI_ABORTWARNING 33 | !define MUI_ICON "..\app\src\res\icon\mishiro.ico" 34 | !define MUI_UNICON "..\app\src\res\icon\mishiro.ico" 35 | 36 | !insertmacro MUI_PAGE_WELCOME 37 | 38 | !insertmacro MUI_PAGE_DIRECTORY 39 | 40 | var ICONS_GROUP 41 | !define MUI_STARTMENUPAGE_NODISABLE 42 | !define MUI_STARTMENUPAGE_DEFAULTFOLDER "${PRODUCT_NAME}" 43 | !define MUI_STARTMENUPAGE_REGISTRY_ROOT "${PRODUCT_UNINST_ROOT_KEY}" 44 | !define MUI_STARTMENUPAGE_REGISTRY_KEY "${PRODUCT_UNINST_KEY}" 45 | !define MUI_STARTMENUPAGE_REGISTRY_VALUENAME "${PRODUCT_STARTMENU_REGVAL}" 46 | !insertmacro MUI_PAGE_STARTMENU Application $ICONS_GROUP 47 | 48 | !insertmacro MUI_PAGE_INSTFILES 49 | 50 | !insertmacro MUI_PAGE_FINISH 51 | 52 | !insertmacro MUI_UNPAGE_INSTFILES 53 | 54 | !insertmacro MUI_LANGUAGE "English" 55 | 56 | !insertmacro MUI_RESERVEFILE_INSTALLOPTIONS 57 | 58 | Name "${PRODUCT_NAME}-v${PRODUCT_VERSION}" 59 | OutFile "${PRODUCT_NAME}-v${PRODUCT_VERSION}-win32-${PRODUCT_ARCH}-setup.exe" 60 | InstallDir "${PROGRAM_FILES_MAP}\${PRODUCT_NAME}" 61 | ShowInstDetails show 62 | ShowUnInstDetails show 63 | BrandingText "https://github.com/toyobayashi/mishiro" 64 | 65 | Section "MainSection" SEC01 66 | SetOutPath "$INSTDIR\*.*" 67 | SetOverwrite ifnewer 68 | File /r "${PRODUCT_NAME}-v${PRODUCT_VERSION}-win32-${PRODUCT_ARCH}\*.*" 69 | 70 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 71 | CreateDirectory "$SMPROGRAMS\$ICONS_GROUP" 72 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" 73 | CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\${PRODUCT_NAME}.exe" 74 | !insertmacro MUI_STARTMENU_WRITE_END 75 | SectionEnd 76 | 77 | Section -AdditionalIcons 78 | SetOutPath $INSTDIR 79 | !insertmacro MUI_STARTMENU_WRITE_BEGIN Application 80 | CreateShortCut "$SMPROGRAMS\$ICONS_GROUP\Uninstall.lnk" "$INSTDIR\uninst.exe" 81 | !insertmacro MUI_STARTMENU_WRITE_END 82 | SectionEnd 83 | 84 | Section -Post 85 | !ifdef LIBRARY_X64 86 | SetRegView 64 87 | !endif 88 | WriteUninstaller "$INSTDIR\uninst.exe" 89 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)" 90 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "UninstallString" "$INSTDIR\uninst.exe" 91 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "DisplayVersion" "${PRODUCT_VERSION}" 92 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}" 93 | WriteRegStr ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" "Publisher" "${PRODUCT_PUBLISHER}" 94 | !ifdef LIBRARY_X64 95 | SetRegView lastused 96 | !endif 97 | SectionEnd 98 | 99 | Section Uninstall 100 | !insertmacro MUI_STARTMENU_GETFOLDER "Application" $ICONS_GROUP 101 | Delete "$DESKTOP\${PRODUCT_NAME}.lnk" 102 | 103 | RMDir /r "$SMPROGRAMS\$ICONS_GROUP" 104 | 105 | RMDir /r "$INSTDIR" 106 | !ifdef LIBRARY_X64 107 | SetRegView 64 108 | !endif 109 | DeleteRegKey ${PRODUCT_UNINST_ROOT_KEY} "${PRODUCT_UNINST_KEY}" 110 | !ifdef LIBRARY_X64 111 | SetRegView lastused 112 | !endif 113 | SetAutoClose true 114 | SectionEnd 115 | 116 | Function .onInit 117 | !insertmacro TIP_WHEN_AMD64_INSTALLER_RUNAT_X86 118 | FunctionEnd 119 | 120 | Function un.onInit 121 | !insertmacro TIP_WHEN_AMD64_INSTALLER_RUNAT_X86 122 | MessageBox MB_ICONQUESTION|MB_YESNO|MB_DEFBUTTON2 "Do you want to uninstall $(^Name)? " IDYES +2 123 | Abort 124 | FunctionEnd 125 | 126 | Function un.onUninstSuccess 127 | HideWindow 128 | MessageBox MB_ICONINFORMATION|MB_OK "$(^Name) is uninstalled successfully." 129 | FunctionEnd 130 | -------------------------------------------------------------------------------- /img/alipay.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/img/alipay.jpg -------------------------------------------------------------------------------- /img/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toyobayashi/mishiro/a226ace8f5d9bee62b560c5fcb87a007eaa6ad68/img/screenshot.png --------------------------------------------------------------------------------