feat: verbose toggle, info tab, many misc fixes

This commit is contained in:
2025-04-14 19:20:08 +00:00
parent 37df371006
commit f588892b05
30 changed files with 410 additions and 186 deletions

52
CHANGELOG.md Normal file
View File

@ -0,0 +1,52 @@
## 0.7.0
- Hopefully fixed issues with the download button
- Added a verbose logging option
- Added an info tab
- Instead of auto-installing segatools & mempatcher at launch, the package store now shows a "install recommended" button
## 0.6.1
- Added support for O.N.G.E.K.I. English Translation
- Disabled the icon buttons as they broke at some point
## 0.6.0
- Chunithm: added support for DLLs (saekawa, mempatcher)
- Chunithm: added a patch interface
- Chunithm: added display settings
- Chunithm: removed split IR
- Both games: added hardware aime reader support
- Added an update progress bar
## 0.5.0
- Added a keyboard configuration UI (for both games)
- Added a prompt after selecting `mu3.exe`/`chusanApp.exe` in the file picker, allowing you to copy much of the data from `segatools.ini`, if it already exists.
- Added an option to not switch the primary monitor.
- This is not recommended to use but it may help when the primary monitor switcher doesn't work correctly.
## 0.4.0
- New error dialog.
- Added tooltips for IO, Aime, Hook.
- Added a welcome message.
- Added a separate auto-update toggle.
- Fixed a display bug with the offline mode toggle.
## 0.3.0
- Added UI scaling, offline mode, and an 'update all' button
- First public release
## 0.2.0
- Added a context menu for the start button with additional launch options
- Added an audio mode button for ongeki
- Fixed the launcher freezing while the game is running
- Probably added auto-updates
## 0.1.0
⚠️ this release is incomplete and potentially cursed
it's more of a preview of a preview

View File

@ -1,11 +1,9 @@
### Short-term ### Short-term
- CHUNITHM support
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63 - https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
### Long-term ### Long-term
- Auto-updates
- Progress bars and other GUI sugar - Progress bars and other GUI sugar
- IO DLLs and artemis as special packages - artemis as a special package
- Other arcade games (if there is demand) - Other arcade games (if there is demand)

View File

@ -4,10 +4,11 @@
"": { "": {
"name": "startliner", "name": "startliner",
"dependencies": { "dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3", "@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3", "@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.1.2", "@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.4.1", "@tauri-apps/api": "^2.4.1",
"@tauri-apps/plugin-cli": "^2.2.0", "@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1", "@tauri-apps/plugin-deep-link": "~2.2.1",
@ -17,29 +18,31 @@
"@tauri-apps/plugin-shell": "~2.2.1", "@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0", "@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"pinia": "^3.0.1", "@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.3", "primevue": "^4.3.3",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"tailwindcss": "^4.1.2", "tailwindcss": "^4.1.3",
"tailwindcss-primeui": "^0.4.0", "tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13", "vue": "^3.5.13",
"vuetify": "^3.8.0", "vuetify": "^3.8.1",
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.4.1", "@tauri-apps/cli": "^2.4.1",
"@tsconfig/node22": "^22.0.1", "@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.0", "@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3", "@vitejs/plugin-vue": "^5.2.3",
"@vue/eslint-config-typescript": "^14.5.0", "@vue/eslint-config-typescript": "^14.5.0",
"@vue/tsconfig": "^0.5.1", "@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2", "npm-run-all2": "^7.0.2",
"sass": "1.77.8", "sass": "1.77.8",
"sass-embedded": "^1.86.3", "sass-embedded": "^1.86.3",
"typescript": "^5.8.2", "typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1", "unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5", "unplugin-vue-components": "^0.27.5",
"vite": "^6.2.5", "vite": "^6.2.6",
"vite-plugin-vuetify": "^2.1.1", "vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8", "vue-tsc": "^2.2.8",
}, },
@ -132,6 +135,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="],
"@f3ve/vue-markdown-it": ["@f3ve/vue-markdown-it@0.2.3", "", { "dependencies": { "markdown-it": "^14.1.0" }, "peerDependencies": { "vue": "^3.3.4" } }, "sha512-v0VNd7wb55kwsUUy3n6DLI9+0FYSG0PrCTD3bWuSRo6WS3OHD5wghh/aHzebVdsVkSBXfVpiEUlMA3DrxLs7Lw=="],
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
@ -292,6 +297,12 @@
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/linkify-it": ["@types/linkify-it@5.0.0", "", {}, "sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q=="],
"@types/markdown-it": ["@types/markdown-it@14.1.2", "", { "dependencies": { "@types/linkify-it": "^5", "@types/mdurl": "^2" } }, "sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog=="],
"@types/mdurl": ["@types/mdurl@2.0.0", "", {}, "sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg=="],
"@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="], "@types/node": ["@types/node@22.14.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="], "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.29.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.29.0", "@typescript-eslint/type-utils": "8.29.0", "@typescript-eslint/utils": "8.29.0", "@typescript-eslint/visitor-keys": "8.29.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-PAIpk/U7NIS6H7TEtN45SPGLQaHNgB7wSjsQV/8+KYokAb2T/gloOA/Bee2yd4/yKVhPKe5LlaUGhAZk5zmSaQ=="],
@ -540,6 +551,8 @@
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="], "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.29.2", "", { "os": "win32", "cpu": "x64" }, "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA=="],
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
"local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="], "local-pkg": ["local-pkg@0.5.1", "", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="],
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
@ -550,6 +563,10 @@
"magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
"mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="],
"memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="], "memorystream": ["memorystream@0.3.1", "", {}, "sha512-S3UwM3yj5mtUSEfP41UZmt/0SCoVYUcU1rkXv+BQ5Ig8ndL4sPoJNBUJERafdPb5jjHJGuMgytgKvKIf58XNBw=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
@ -620,6 +637,8 @@
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="], "read-package-json-fast": ["read-package-json-fast@4.0.0", "", { "dependencies": { "json-parse-even-better-errors": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" } }, "sha512-qpt8EwugBWDw2cgE2W+/3oxC+KTez2uSVR8JU9Q36TXPAGCaozfQUs59v4j4GFpWTaw0i6hAZSvOmu1J0uOEUg=="],
@ -726,6 +745,8 @@
"typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="], "typescript-eslint": ["typescript-eslint@8.29.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.29.0", "@typescript-eslint/parser": "8.29.0", "@typescript-eslint/utils": "8.29.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.9.0" } }, "sha512-ep9rVd9B4kQsZ7ZnWCVxUE/xDLUUUsRzE0poAeNu+4CkFErLfuvPt/qtm2EpnSyfvsR0S6QzDFSrPCFBwf64fg=="],
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
"ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="], "ufo": ["ufo@1.5.4", "", {}, "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],

View File

@ -10,6 +10,7 @@
"tauri": "tauri" "tauri": "tauri"
}, },
"dependencies": { "dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@mdi/font": "7.4.47", "@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3", "@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3", "@primevue/themes": "^4.3.3",
@ -23,6 +24,8 @@
"@tauri-apps/plugin-shell": "~2.2.1", "@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0", "@tauri-apps/plugin-updater": "^2.7.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.2", "@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
"pinia": "^3.0.2", "pinia": "^3.0.2",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.3", "primevue": "^4.3.3",

1
rust/Cargo.lock generated
View File

@ -4730,6 +4730,7 @@ dependencies = [
"humantime", "humantime",
"junction", "junction",
"log", "log",
"open",
"regex", "regex",
"reqwest", "reqwest",
"rust-ini", "rust-ini",

View File

@ -45,6 +45,7 @@ sha256 = "1.6.0"
serialport = "4.7.1" serialport = "4.7.1"
fern = { version ="0.7.1", features = ["colored"] } fern = { version ="0.7.1", features = ["colored"] }
humantime = "2.2.0" humantime = "2.2.0"
open = "5.3.2"
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
tauri-plugin-cli = "2" tauri-plugin-cli = "2"

View File

@ -23,6 +23,7 @@
"fs:allow-data-read-recursive", "fs:allow-data-read-recursive",
"fs:allow-data-write-recursive", "fs:allow-data-write-recursive",
"fs:allow-config-read-recursive", "fs:allow-config-read-recursive",
"fs:allow-config-write-recursive" "fs:allow-config-write-recursive",
"shell:allow-open"
] ]
} }

View File

@ -1,4 +1,5 @@
use std::hash::{DefaultHasher, Hash, Hasher}; use std::hash::{DefaultHasher, Hash, Hasher};
use std::time::SystemTime;
use crate::model::config::GlobalConfig; use crate::model::config::GlobalConfig;
use crate::model::patch::PatchFileVec; use crate::model::patch::PatchFileVec;
use crate::pkg::{Feature, Status}; use crate::pkg::{Feature, Status};
@ -7,6 +8,7 @@ use crate::{model::misc::Game, pkg::PkgKey};
use crate::pkg_store::PackageStore; use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use fern::colors::{Color, ColoredLevelConfig};
use tauri::AppHandle; use tauri::AppHandle;
pub struct GlobalState { pub struct GlobalState {
@ -34,6 +36,8 @@ impl AppData {
.and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?)) .and_then(|s| Ok(serde_json::from_str::<GlobalConfig>(&s)?))
.unwrap_or_default(); .unwrap_or_default();
Self::init_logger(&cfg);
let profile = match cfg.recent_profile { let profile = match cfg.recent_profile {
Some((game, ref name)) => Profile::load(game, name.clone()).ok(), Some((game, ref name)) => Profile::load(game, name.clone()).ok(),
None => None None => None
@ -127,4 +131,36 @@ impl AppData {
p.fix(&self.pkgs); p.fix(&self.pkgs);
} }
} }
fn init_logger(cfg: &GlobalConfig) {
let mut fern_builder;
let colors = ColoredLevelConfig::new()
.debug(Color::Green)
.info(Color::Blue)
.warn(Color::Yellow)
.error(Color::Red);
fern_builder = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
colors.color(record.level()),
record.target(),
message
))
})
.chain(std::io::stdout())
.chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger"));
if cfg.verbose == true {
fern_builder = fern_builder.level(log::LevelFilter::Debug);
} else {
fern_builder = fern_builder.level(log::LevelFilter::Info);
}
if let Err(e) = fern_builder.apply() {
panic!("unable to initialize the logger? {:?}", e);
}
}
} }

View File

@ -323,9 +323,10 @@ pub async fn duplicate_profile(profile: ProfileMeta) -> Result<(), String> {
pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> { pub async fn delete_profile(state: State<'_, Mutex<AppData>>, profile: ProfileMeta) -> Result<(), String> {
log::debug!("invoke: delete_profile({:?})", profile); log::debug!("invoke: delete_profile({:?})", profile);
std::fs::remove_dir_all(profile.config_dir()) util::remove_dir_all(profile.config_dir())
.await
.map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?; .map_err(|e| format!("Unable to delete {:?}: {}", profile.config_dir(), e))?;
if let Err(e) = std::fs::remove_dir_all(profile.data_dir()) { if let Err(e) = util::remove_dir_all(profile.data_dir()).await {
log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e); log::warn!("Unable to delete: {:?} {}", profile.data_dir(), e);
} }
@ -414,7 +415,8 @@ pub async fn get_global_config(state: State<'_, Mutex<AppData>>, field: GlobalCo
let appd = state.lock().await; let appd = state.lock().await;
match field { match field {
GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode), GlobalConfigField::OfflineMode => Ok(appd.cfg.offline_mode),
GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates) GlobalConfigField::EnableAutoupdates => Ok(appd.cfg.enable_autoupdates),
GlobalConfigField::Verbose => Ok(appd.cfg.verbose)
} }
} }
@ -425,7 +427,8 @@ pub async fn set_global_config(state: State<'_, Mutex<AppData>>, field: GlobalCo
let mut appd = state.lock().await; let mut appd = state.lock().await;
match field { match field {
GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value, GlobalConfigField::OfflineMode => appd.cfg.offline_mode = value,
GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value GlobalConfigField::EnableAutoupdates => appd.cfg.enable_autoupdates = value,
GlobalConfigField::Verbose => appd.cfg.verbose = value,
}; };
appd.write().map_err(|e| e.to_string()) appd.write().map_err(|e| e.to_string())
} }
@ -472,6 +475,18 @@ pub async fn file_exists(path: String) -> Result<bool, ()> {
Ok(std::fs::exists(path).unwrap_or(false)) Ok(std::fs::exists(path).unwrap_or(false))
} }
// Easier than trying to get the barely-documented tauri permissions system to work
#[tauri::command]
pub async fn open_file(path: String) -> Result<(), String> {
open::that(path).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub fn get_changelog() -> Result<String, ()> {
Ok(include_str!("../../CHANGELOG.md").to_owned())
}
#[tauri::command] #[tauri::command]
pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> { pub async fn list_com_ports() -> Result<BTreeMap<String, i32>, String> {
let ports = serialport::available_ports().unwrap_or(Vec::new()); let ports = serialport::available_ports().unwrap_or(Vec::new());

View File

@ -1,4 +1,6 @@
use std::{collections::HashSet, path::PathBuf}; use std::{collections::HashSet, path::PathBuf};
use futures::Stream;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
use tokio::fs::File; use tokio::fs::File;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
@ -6,14 +8,20 @@ use anyhow::{anyhow, Result};
use crate::pkg::{Package, PkgKey, Remote}; use crate::pkg::{Package, PkgKey, Remote};
pub struct DownloadHandler { pub struct DownloadHandler {
set: HashSet<String>, paths: HashSet<PathBuf>,
app: AppHandle app: AppHandle
} }
#[derive(Serialize, Deserialize, Clone)]
pub struct DownloadTick {
pkg_key: PkgKey,
ratio: f32
}
impl DownloadHandler { impl DownloadHandler {
pub fn new(app: AppHandle) -> DownloadHandler { pub fn new(app: AppHandle) -> DownloadHandler {
DownloadHandler { DownloadHandler {
set: HashSet::new(), paths: HashSet::new(),
app app
} }
} }
@ -22,11 +30,11 @@ impl DownloadHandler {
let rmt = pkg.rmt.as_ref() let rmt = pkg.rmt.as_ref()
.ok_or_else(|| anyhow!("Attempted to download a package without remote data"))? .ok_or_else(|| anyhow!("Attempted to download a package without remote data"))?
.clone(); .clone();
if self.set.contains(zip_path.to_string_lossy().as_ref()) { if self.paths.contains(zip_path) {
// Todo when there is a clear cache button, it should clear the set Ok(())
Err(anyhow!("Already downloading"))
} else { } else {
self.set.insert(zip_path.to_string_lossy().to_string()); // TODO clear cache button should clear this
self.paths.insert(zip_path.clone());
tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt)); tauri::async_runtime::spawn(Self::download_zip_proc(self.app.clone(), zip_path.clone(), pkg.key(), rmt));
Ok(()) Ok(())
} }
@ -42,10 +50,15 @@ impl DownloadHandler {
let mut cache_file_w = File::create(&zip_path_part).await?; let mut cache_file_w = File::create(&zip_path_part).await?;
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream(); let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
let first_hint = byte_stream.size_hint().0 as f32;
log::info!("downloading: {}", rmt.download_url); log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await { while let Some(item) = byte_stream.next().await {
let i = item?; let i = item?;
app.emit("download-tick", DownloadTick {
pkg_key: pkg_key.clone(),
ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint
})?;
cache_file_w.write_all(&mut i.as_ref()).await?; cache_file_w.write_all(&mut i.as_ref()).await?;
} }
cache_file_w.sync_all().await?; cache_file_w.sync_all().await?;

View File

@ -9,11 +9,10 @@ mod modules;
mod profiles; mod profiles;
mod patcher; mod patcher;
use std::{sync::OnceLock, time::SystemTime}; use std::sync::OnceLock;
use anyhow::anyhow; use anyhow::anyhow;
use closure::closure; use closure::closure;
use appdata::{AppData, ToggleAction}; use appdata::{AppData, ToggleAction};
use fern::colors::{Color, ColoredLevelConfig};
use model::misc::Game; use model::misc::Game;
use pkg::PkgKey; use pkg::PkgKey;
use pkg_store::Payload; use pkg_store::Payload;
@ -48,42 +47,7 @@ pub async fn run(_args: Vec<String>) {
util::init_dirs(&apph); util::init_dirs(&apph);
let mut fern_builder; let mut app_data = AppData::new(app.handle().clone());
{
let colors = ColoredLevelConfig::new()
.debug(Color::Green)
.info(Color::Blue)
.warn(Color::Yellow)
.error(Color::Red);
fern_builder = fern::Dispatch::new()
.format(move |out, message, record| {
out.finish(format_args!(
"[{} {} {}] {}",
humantime::format_rfc3339_seconds(SystemTime::now()),
colors.color(record.level()),
record.target(),
message
))
})
.chain(std::io::stdout())
.chain(fern::log_file(util::data_dir().join("log.txt")).expect("unable to initialize the logger"));
}
#[cfg(debug_assertions)]
{
fern_builder = fern_builder.level(log::LevelFilter::Debug);
}
#[cfg(not(debug_assertions))]
{
if std::env::var("DEBUG_LOG").is_ok() {
fern_builder = fern_builder.level(log::LevelFilter::Debug);
} else {
fern_builder = fern_builder.level(log::LevelFilter::Info);
}
}
fern_builder.apply()?;
log::info!( log::info!(
"running from {}", "running from {}",
@ -93,7 +57,6 @@ pub async fn run(_args: Vec<String>) {
.unwrap_or_default() .unwrap_or_default()
); );
let mut app_data = AppData::new(app.handle().clone());
let start_immediately; let start_immediately;
if let Ok(matches) = app.cli().matches() { if let Ok(matches) = app.cli().matches() {
@ -244,6 +207,8 @@ pub async fn run(_args: Vec<String>) {
cmd::list_platform_capabilities, cmd::list_platform_capabilities,
cmd::list_directories, cmd::list_directories,
cmd::file_exists, cmd::file_exists,
cmd::open_file,
cmd::get_changelog,
cmd::list_com_ports, cmd::list_com_ports,
@ -326,7 +291,7 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
update.download_and_install( update.download_and_install(
|chunk_length, content_length| { |chunk_length, content_length| {
downloaded += chunk_length; downloaded += chunk_length;
_ = app.emit("update-progress", (chunk_length as f64) / (content_length.unwrap_or(u64::MAX) as f64)); _ = app.emit("update-progress", (downloaded as f64) / (content_length.unwrap_or(u64::MAX) as f64));
}, },
|| { || {
log::info!("download finished"); log::info!("download finished");

View File

@ -2,10 +2,12 @@ use serde::{Deserialize, Serialize};
use super::misc::Game; use super::misc::Game;
#[derive(Serialize, Deserialize, Clone)] #[derive(Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct GlobalConfig { pub struct GlobalConfig {
pub recent_profile: Option<(Game, String)>, pub recent_profile: Option<(Game, String)>,
pub offline_mode: bool, pub offline_mode: bool,
pub enable_autoupdates: bool, pub enable_autoupdates: bool,
pub verbose: bool,
} }
impl Default for GlobalConfig { impl Default for GlobalConfig {
@ -13,13 +15,16 @@ impl Default for GlobalConfig {
Self { Self {
recent_profile: Default::default(), recent_profile: Default::default(),
offline_mode: false, offline_mode: false,
enable_autoupdates: true enable_autoupdates: true,
verbose: false,
} }
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum GlobalConfigField { pub enum GlobalConfigField {
OfflineMode, OfflineMode,
EnableAutoupdates EnableAutoupdates,
Verbose
} }

View File

@ -5,10 +5,9 @@ use crate::pkg::PkgKey;
use super::profile::ProfileModule; use super::profile::ProfileModule;
#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)] #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Copy)]
#[serde(rename_all = "snake_case")]
pub enum Game { pub enum Game {
#[serde(rename = "ongeki")]
Ongeki, Ongeki,
#[serde(rename = "chunithm")]
Chunithm, Chunithm,
} }
@ -89,10 +88,9 @@ pub enum StartCheckError {
} }
#[derive(Default, Serialize, Deserialize, Clone)] #[derive(Default, Serialize, Deserialize, Clone)]
#[serde(default)]
pub struct ConfigHook { pub struct ConfigHook {
#[serde(skip_serializing_if = "Option::is_none")]
pub allnet_auth: Option<ConfigHookAuth>, pub allnet_auth: Option<ConfigHookAuth>,
#[serde(skip_serializing_if = "Option::is_none")]
pub aime: Option<ConfigHookAime>, pub aime: Option<ConfigHookAime>,
} }

View File

@ -14,6 +14,7 @@ pub enum Aime {
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct AMNet { pub struct AMNet {
pub name: String, pub name: String,
pub addr: String, pub addr: String,
@ -26,18 +27,17 @@ impl Default for AMNet {
} }
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug, Default )]
#[serde(default)]
pub struct Segatools { pub struct Segatools {
pub target: PathBuf, pub target: PathBuf,
pub hook: Option<PkgKey>, pub hook: Option<PkgKey>,
pub io: Option<PkgKey>, pub io: Option<PkgKey>,
#[serde(default)]
pub aime: Aime, pub aime: Aime,
pub amfs: PathBuf, pub amfs: PathBuf,
pub option: PathBuf, pub option: PathBuf,
pub appdata: PathBuf, pub appdata: PathBuf,
pub intel: bool, pub intel: bool,
#[serde(default)]
pub amnet: AMNet, pub amnet: AMNet,
pub aime_port: Option<i32>, pub aime_port: Option<i32>,
} }
@ -69,7 +69,8 @@ pub enum DisplayMode {
Fullscreen Fullscreen
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Display { pub struct Display {
pub target: String, pub target: String,
pub rez: (i32, i32), pub rez: (i32, i32),
@ -77,11 +78,7 @@ pub struct Display {
pub rotation: Option<i32>, pub rotation: Option<i32>,
pub frequency: i32, pub frequency: i32,
pub borderless_fullscreen: bool, pub borderless_fullscreen: bool,
#[serde(default)]
pub dont_switch_primary: bool, pub dont_switch_primary: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub monitor_index_override: Option<i32>, pub monitor_index_override: Option<i32>,
} }
@ -113,6 +110,7 @@ pub enum NetworkType {
} }
#[derive(Deserialize, Serialize, Clone, Default, Debug)] #[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(default)]
pub struct Network { pub struct Network {
pub network_type: NetworkType, pub network_type: NetworkType,
@ -127,11 +125,13 @@ pub struct Network {
} }
#[derive(Deserialize, Serialize, Clone, Default, Debug)] #[derive(Deserialize, Serialize, Clone, Default, Debug)]
#[serde(default)]
pub struct BepInEx { pub struct BepInEx {
pub console: bool, pub console: bool,
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone)]
#[serde(default)]
pub struct Wine { pub struct Wine {
pub runtime: PathBuf, pub runtime: PathBuf,
pub prefix: PathBuf, pub prefix: PathBuf,
@ -155,20 +155,17 @@ pub enum Mu3Audio {
Excl2Ch, Excl2Ch,
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[serde(default)]
pub struct Mu3Ini { pub struct Mu3Ini {
#[serde(skip_serializing_if = "Option::is_none")]
pub audio: Option<Mu3Audio>, pub audio: Option<Mu3Audio>,
#[serde(skip_serializing_if = "Option::is_none")]
pub blacklist: Option<(i32, i32)>, pub blacklist: Option<(i32, i32)>,
} }
fn default_true() -> bool { true }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct OngekiKeyboard { pub struct OngekiKeyboard {
#[serde(default = "default_true")] pub enabled: bool, pub enabled: bool,
pub use_mouse: bool, pub use_mouse: bool,
pub coin: i32, pub coin: i32,
pub svc: i32, pub svc: i32,
@ -208,8 +205,9 @@ impl Default for OngekiKeyboard {
} }
#[derive(Deserialize, Serialize, Clone, Debug)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct ChunithmKeyboard { pub struct ChunithmKeyboard {
#[serde(default = "default_true")] pub enabled: bool, pub enabled: bool,
pub coin: i32, pub coin: i32,
pub svc: i32, pub svc: i32,
pub test: i32, pub test: i32,

View File

@ -14,10 +14,10 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
if redo_bepinex { if redo_bepinex {
if pfx_dir.join("BepInEx").exists() { if pfx_dir.join("BepInEx").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("BepInEx")).await?; util::remove_dir_all(pfx_dir.join("BepInEx")).await?;
} }
if pfx_dir.join("lang").exists() { if pfx_dir.join("lang").exists() {
tokio::fs::remove_dir_all(pfx_dir.join("lang")).await?; util::remove_dir_all(pfx_dir.join("lang")).await?;
} }
} }

View File

@ -1,5 +1,4 @@
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::Path;
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter}; use tauri::{AppHandle, Emitter};
@ -207,8 +206,9 @@ impl PackageStore {
"{}-{}-{}.zip", "{}-{}-{}.zip",
pkg.namespace, pkg.name, rmt.version pkg.namespace, pkg.name, rmt.version
)); ));
let part_path = zip_path.join(".part");
if !zip_path.exists() { if !zip_path.exists() && !part_path.exists() {
self.dlh.download_zip(&zip_path, &pkg)?; self.dlh.download_zip(&zip_path, &pkg)?;
log::debug!("deferring {}", key); log::debug!("deferring {}", key);
return Ok(InstallResult::Deferred); return Ok(InstallResult::Deferred);
@ -243,7 +243,7 @@ impl PackageStore {
if path.exists() && path.join("manifest.json").exists() { if path.exists() && path.join("manifest.json").exists() {
pkg.loc = None; pkg.loc = None;
let rv = Self::clean_up_package(&path).await; let rv = util::remove_dir_all(&path).await;
if rv.is_ok() { if rv.is_ok() {
self.app.emit("install-end-prelude", Payload { self.app.emit("install-end-prelude", Payload {
@ -269,48 +269,6 @@ impl PackageStore {
self.store.insert(key, new); self.store.insert(key, new);
} }
async fn clean_up_dir(path: impl AsRef<Path>, name: &str) -> Result<()> {
let path = path.as_ref().join(name);
if path.exists() {
tokio::fs::remove_dir_all(path)
.await
.map_err(|e| anyhow!("could not delete {}: {}", name, e))?;
}
Ok(())
}
async fn clean_up_file(path: impl AsRef<Path>, name: &str, force: bool) -> Result<()> {
let path = path.as_ref().join(name);
if force || path.exists() {
tokio::fs::remove_file(path).await
.map_err(|e| anyhow!("Could not delete /{}: {}", name, e))?;
}
Ok(())
}
async fn clean_up_package(path: impl AsRef<Path>) -> Result<()> {
// todo case sensitivity for linux
Self::clean_up_dir(&path, "app").await?;
Self::clean_up_dir(&path, "option").await?;
Self::clean_up_dir(&path, "segatools").await?;
Self::clean_up_file(&path, "icon.png", true).await?;
Self::clean_up_file(&path, "manifest.json", true).await?;
Self::clean_up_file(&path, "README.md", true).await?;
Self::clean_up_file(&path, "post_load.ps1", false).await?;
// todo search for the proper dll
Self::clean_up_file(&path, "saekawa.dll", false).await?;
Self::clean_up_file(&path, "mempatcher32.dll", false).await?;
Self::clean_up_file(&path, "mempatcher64.dll", false).await?;
tokio::fs::remove_dir(path.as_ref())
.await
.map_err(|e| anyhow!("Could not delete {}: {}", path.as_ref().to_string_lossy(), e))?;
Ok(())
}
fn resolve_deps(&self, rmt: Remote, set: &mut HashSet<PkgKey>) -> Result<()> { fn resolve_deps(&self, rmt: Remote, set: &mut HashSet<PkgKey>) -> Result<()> {
for d in rmt.dependencies { for d in rmt.dependencies {
set.insert(d.clone()); set.insert(d.clone());

View File

@ -94,7 +94,7 @@ impl Profile {
} }
std::fs::write(&path, s) std::fs::write(&path, s)
.map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?; .map_err(|e| anyhow!("error when writing to {:?}: {}", path, e))?;
log::info!("profile written to {:?}", path); log::info!("profile saved to {:?}", path);
Ok(()) Ok(())
} }

View File

@ -154,4 +154,23 @@ impl PathStr for PathBuf {
pub fn bool_to_01(val: bool) -> &'static str { pub fn bool_to_01(val: bool) -> &'static str {
return if val { "1" } else { "0" } return if val { "1" } else { "0" }
}
// rm -r with checks
pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
let canon = path.as_ref().canonicalize()?;
if canon.to_string_lossy().len() < 10 {
return Err(anyhow!("invalid remove_dir_all target: too short"));
}
if canon.starts_with(data_dir().canonicalize()?)
|| canon.starts_with(config_dir().canonicalize()?)
|| canon.starts_with(cache_dir().canonicalize()?) {
tokio::fs::remove_dir_all(path).await
.map_err(|e| anyhow!("invalid remove_dir_all target: {:?}", e))?;
Ok(())
} else {
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
}
} }

View File

@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "STARTLINER", "productName": "STARTLINER",
"version": "0.6.1", "version": "0.7.0",
"identifier": "zip.patafour.startliner", "identifier": "zip.patafour.startliner",
"build": { "build": {
"beforeDevCommand": "bun run dev", "beforeDevCommand": "bun run dev",

View File

@ -13,6 +13,7 @@ import TabPanel from 'primevue/tabpanel';
import TabPanels from 'primevue/tabpanels'; import TabPanels from 'primevue/tabpanels';
import Tabs from 'primevue/tabs'; import Tabs from 'primevue/tabs';
import { listen } from '@tauri-apps/api/event'; import { listen } from '@tauri-apps/api/event';
import InfoPage from './InfoPage.vue';
import ModList from './ModList.vue'; import ModList from './ModList.vue';
import ModStore from './ModStore.vue'; import ModStore from './ModStore.vue';
import OptionList from './OptionList.vue'; import OptionList from './OptionList.vue';
@ -36,7 +37,8 @@ const client = useClientStore();
pkg.setupListeners(); pkg.setupListeners();
const currentTab: Ref<string | number> = ref(3); const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref(''); const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null); const isProfileDisabled = computed(() => prf.current === null);
@ -62,20 +64,11 @@ onMounted(async () => {
await Promise.all([prf.reloadList(), prf.reload()]); await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) { if (prf.current !== null) {
currentTab.value = 0; currentTab.value = 'loc';
await pkg.reloadAll(); await pkg.reloadAll();
} }
fetch_promise.then(async () => { await fetch_promise;
await invoke('install_package', {
key: 'segatools-mu3hook',
force: false,
});
await invoke('install_package', {
key: 'segatools-chusanhook',
force: false,
});
});
}); });
const errorVisible = ref(false); const errorVisible = ref(false);
@ -149,42 +142,47 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
:value="currentTab" :value="currentTab"
v-on:update:value=" v-on:update:value="
(value) => { (value) => {
currentTab = value; currentTab = value as any;
} }
" "
class="h-screen" class="h-screen"
> >
<div class="fixed w-full flex z-100"> <div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false"> <TabList class="grow" :show-navigators="false">
<Tab :value="3"><div class="pi pi-users"></div></Tab> <Tab value="users"><div class="pi pi-users"></div></Tab>
<Tab :disabled="isProfileDisabled" :value="0" <Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div ><div class="pi pi-box"></div
></Tab> ></Tab>
<Tab v-if="prf.current?.meta.game === 'chunithm'" :value="4" <Tab
v-if="prf.current?.meta.game === 'chunithm'"
value="patches"
><div class="pi pi-ticket"></div ><div class="pi pi-ticket"></div
></Tab> ></Tab>
<Tab <Tab
v-if="pkg.networkStatus === 'online'" v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled" :disabled="isProfileDisabled"
:value="1" value="rmt"
><div class="pi pi-download"></div ><div class="pi pi-download"></div
></Tab> ></Tab>
<Tab :disabled="isProfileDisabled" :value="2" <Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div ><div class="pi pi-cog"></div
></Tab> ></Tab>
<Tab value="info"
><div class="pi pi-info-circle"></div
></Tab>
<div class="grow"></div> <div class="grow"></div>
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
class="flex" class="flex"
v-if="[0, 1, 2].includes(currentTab as number)" v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
> >
<InputIcon class="self-center mr-2"> <InputIcon class="self-center mr-2">
<i class="pi pi-search" /> <i class="pi pi-search" />
</InputIcon> </InputIcon>
<InputText <InputText
v-if="currentTab === 2" v-if="currentTab === 'cfg'"
style="min-width: 0; width: 25dvw" style="min-width: 0; width: 25dvw"
class="self-center" class="self-center"
size="small" size="small"
@ -234,28 +232,19 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
</TabList> </TabList>
</div> </div>
<TabPanels class="w-full grow mt-[3rem]"> <TabPanels class="w-full grow mt-[3rem]">
<TabPanel :value="0"> <TabPanel value="loc">
<ModList :search="pkgSearchTerm" /> <ModList :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel :value="1"> <TabPanel value="rmt">
<ModStore :search="pkgSearchTerm" /> <ModStore :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel :value="2"> <TabPanel value="cfg">
<OptionList /> <OptionList />
</TabPanel> </TabPanel>
<TabPanel :value="3"> <TabPanel value="users">
<ProfileList /> <ProfileList />
<br /><br /><br />
<footer>
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
</footer>
</TabPanel> </TabPanel>
<TabPanel :value="4"> <TabPanel value="patches">
<PatchList <PatchList
v-if=" v-if="
pkg.hasLocal('mempatcher-mempatcher') && pkg.hasLocal('mempatcher-mempatcher') &&
@ -268,8 +257,11 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
and enabled. and enabled.
</div> </div>
</TabPanel> </TabPanel>
<TabPanel value="info">
<InfoPage />
</TabPanel>
</TabPanels> </TabPanels>
<div v-if="currentTab === 5 || currentTab === 3"> <div v-if="currentTab === 'users' || currentTab === 'info'">
<img <img
v-if="prf.current?.meta.game === 'ongeki'" v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg" src="/sticker-ongeki.svg"

View File

@ -0,0 +1,56 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import ScrollPanel from 'primevue/scrollpanel';
import { invoke } from '../invoke';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
const changelog = ref('');
invoke('get_changelog').then((s) => (changelog.value = s as string));
</script>
<template>
<h1>About</h1>
STARTLINER is a simple launcher, configuration tool and mod manager for
O.N.G.E.K.I. and CHUNITHM.
<h1>Changelog</h1>
<ScrollPanel style="height: 200px">
<div class="changelog">
<vue-markdown-it
:source="changelog"
:options="{ typographer: true, breaks: true }"
/>
</div>
</ScrollPanel>
<footer class="mt-10 flex gap-3">
<Button
icon="pi pi-discord"
as="a"
target="_blank"
href="https://discord.gg/jxvzHjjEmc"
/>
<Button
icon="pi pi-github"
as="a"
target="_blank"
href="https://gitea.tendokyu.moe/akanyan/STARTLINER"
/>
</footer>
</template>
<style lang="css">
h1 {
font-size: 1.7rem;
}
.changelog h2 {
font-size: 1.4rem;
}
.changelog ul {
list-style-type: circle;
}
.changelog li {
margin-left: 40px;
}
</style>

View File

@ -15,7 +15,8 @@ const props = defineProps({
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const prf = usePrfStore(); const prf = usePrfStore();
const empty = ref(true); const groupCallIndex = ref(0);
const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]); const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', { invoke('get_game_packages', {
@ -45,7 +46,10 @@ const group = computed(() => {
({ namespace }) => namespace ({ namespace }) => namespace
) )
); );
empty.value = Object.keys(res).length === 0; if (groupCallIndex.value > 0) {
empty.value = Object.keys(res).length === 0;
}
groupCallIndex.value += 1;
return res; return res;
}); });
@ -81,5 +85,5 @@ const missing = computed(() => {
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()"> <Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
<ModListEntry v-for="p in namespace" :pkg="p" /> <ModListEntry v-for="p in namespace" :pkg="p" />
</Fieldset> </Fieldset>
<div v-if="empty" class="text-3xl"></div> <div v-if="empty === true" class="text-3xl fadein"></div>
</template> </template>

View File

@ -1,10 +1,12 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import Button from 'primevue/button';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
import InstallButton from './InstallButton.vue'; import InstallButton from './InstallButton.vue';
import LinkButton from './LinkButton.vue'; import LinkButton from './LinkButton.vue';
import ModTitlecard from './ModTitlecard.vue'; import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue'; import UpdateButton from './UpdateButton.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores'; import { usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature } from '../util'; import { hasFeature } from '../util';
@ -38,7 +40,7 @@ const model = computed({
v-model="model" v-model="model"
/> />
<InstallButton :pkg="pkg" /> <InstallButton :pkg="pkg" />
<!-- <Button <Button
rounded rounded
icon="pi pi-folder" icon="pi pi-folder"
severity="help" severity="help"
@ -46,8 +48,10 @@ const model = computed({
size="small" size="small"
class="ml-2 shrink-0" class="ml-2 shrink-0"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
v-on:click="pkg?.loc && open(pkg.loc.path ?? '')" v-on:click="
/> --> pkg?.loc?.path && invoke('open_file', { path: pkg.loc.path })
"
/>
<LinkButton v-if="pkgs.networkStatus === 'online'" :pkg="pkg" /> <LinkButton v-if="pkgs.networkStatus === 'online'" :pkg="pkg" />
</div> </div>
</template> </template>

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from 'vue'; import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import Divider from 'primevue/divider'; import Divider from 'primevue/divider';
import MultiSelect from 'primevue/multiselect'; import MultiSelect from 'primevue/multiselect';
import ToggleSwitch from 'primevue/toggleswitch'; import ToggleSwitch from 'primevue/toggleswitch';
@ -40,6 +41,39 @@ const list = () => {
empty.value = res.length === 0; empty.value = res.length === 0;
return res; return res;
}; };
const shouldShowRecommended = computed(() => {
if (prf.current!.meta.game === 'ongeki') {
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
return (
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
);
}
return false;
});
const getRecommendedTooltip = () => {
if (prf.current!.meta.game === 'ongeki') {
return 'segatools-mu3hook';
}
if (prf.current!.meta.game === 'chunithm') {
return 'segatools-chusanhook and mempatcher';
}
return '';
};
const installRecommended = () => {
if (prf.current!.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher');
}
};
</script> </script>
<template> <template>
@ -78,6 +112,14 @@ const list = () => {
</div> </div>
</div> </div>
<Divider /> <Divider />
<Button
v-if="shouldShowRecommended"
label="Install recommended packages"
v-tooltip="getRecommendedTooltip"
icon="pi pi-plus"
class="mb-3"
@click="installRecommended"
/>
<div v-for="p in list()" class="flex flex-row"> <div v-for="p in list()" class="flex flex-row">
<ModStoreEntry :pkg="p" /> <ModStoreEntry :pkg="p" />
</div> </div>

View File

@ -22,7 +22,6 @@ const prf = usePrfStore();
icon="pi pi-plus" icon="pi pi-plus"
class="chunithm-button profile-button" class="chunithm-button profile-button"
@click="() => prf.create('chunithm')" @click="() => prf.create('chunithm')"
v-tooltip="'!!! Experimental !!!'"
/> />
</div> </div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4"> <div class="mt-12 flex flex-col flex-wrap align-middle gap-4">

View File

@ -2,10 +2,12 @@
import { ref } from 'vue'; import { ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import InputText from 'primevue/inputtext'; import InputText from 'primevue/inputtext';
import * as path from '@tauri-apps/api/path';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePrfStore } from '../stores'; import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types'; import { ProfileMeta } from '../types';
const general = useGeneralStore();
const prf = usePrfStore(); const prf = usePrfStore();
const isEditing = ref(false); const isEditing = ref(false);
@ -110,7 +112,7 @@ const deleteProfile = async () => {
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
@click="isEditing = true" @click="isEditing = true"
/> />
<!-- <Button <Button
rounded rounded
icon="pi pi-folder" icon="pi pi-folder"
severity="help" severity="help"
@ -121,9 +123,13 @@ const deleteProfile = async () => {
@click=" @click="
path path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`) .join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(open) .then(async (path) => {
if (await invoke('file_exists', { path })) {
await invoke('open_file', { path });
}
})
" "
/> --> />
</div> </div>
</template> </template>

View File

@ -26,7 +26,7 @@ const startline = async (force: boolean, refresh: boolean) => {
} else if ('MissingLocalPackage' in o) { } else if ('MissingLocalPackage' in o) {
return `Package missing: ${o.MissingLocalPackage}`; return `Package missing: ${o.MissingLocalPackage}`;
} else if ('MissingDependency' in o) { } else if ('MissingDependency' in o) {
return `Dependency missing: ${o.MissingDependency}`; return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`;
} else if ('MissingTool' in o) { } else if ('MissingTool' in o) {
return `Tool missing: ${o.MissingTool}`; return `Tool missing: ${o.MissingTool}`;
} else { } else {

View File

@ -119,6 +119,7 @@ const checkSegatoolsIni = async (target: string) => {
return { title: pkgKey(p), value: pkgKey(p) }; return { title: pkgKey(p), value: pkgKey(p) };
}) })
" "
placeholder="none"
option-label="title" option-label="title"
option-value="value" option-value="value"
></Select> ></Select>

View File

@ -25,6 +25,15 @@ const updatesModel = computed({
await client.setAutoupdates(value); await client.setAutoupdates(value);
}, },
}); });
const verboseModel = computed({
get() {
return client.verbose;
},
async set(value: boolean) {
await client.setVerbose(value);
},
});
</script> </script>
<template> <template>
@ -45,12 +54,18 @@ const updatesModel = computed({
</OptionRow> </OptionRow>
<OptionRow <OptionRow
title="Offline mode" title="Offline mode"
tooltip="Disables the package store. Requires a restart." tooltip="Disables the package store. Applies after a restart."
> >
<ToggleSwitch v-model="offlineModel" /> <ToggleSwitch v-model="offlineModel" />
</OptionRow> </OptionRow>
<OptionRow title="Enable automatic updates"> <OptionRow title="Enable automatic updates">
<ToggleSwitch v-model="updatesModel" /> <ToggleSwitch v-model="updatesModel" />
</OptionRow> </OptionRow>
<OptionRow
title="Enable detailed logs"
tooltip="Applies after a restart."
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
</OptionCategory> </OptionCategory>
</template> </template>

View File

@ -193,8 +193,17 @@ export const usePkgStore = defineStore('pkg', {
pkg.js.busy = false; pkg.js.busy = false;
} }
} }
},
//if (rv === 'Deferred') { /* download progress */ } async installFromKey(key: string) {
try {
await invoke('install_package', {
key,
force: true,
});
} catch (err) {
console.error(err);
}
}, },
async updateAll() { async updateAll() {
@ -346,6 +355,7 @@ export const useClientStore = defineStore('client', () => {
const offlineMode = ref(false); const offlineMode = ref(false);
const enableAutoupdates = ref(true); const enableAutoupdates = ref(true);
const verbose = ref(false);
const scaleValue = (value: ScaleType) => const scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2; value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
@ -401,11 +411,15 @@ export const useClientStore = defineStore('client', () => {
} }
offlineMode.value = await invoke('get_global_config', { offlineMode.value = await invoke('get_global_config', {
field: 'OfflineMode', field: 'offline_mode',
}); });
enableAutoupdates.value = await invoke('get_global_config', { enableAutoupdates.value = await invoke('get_global_config', {
field: 'EnableAutoupdates', field: 'enable_autoupdates',
});
verbose.value = await invoke('get_global_config', {
field: 'verbose',
}); });
}; };
@ -438,17 +452,22 @@ export const useClientStore = defineStore('client', () => {
const setOfflineMode = async (value: boolean) => { const setOfflineMode = async (value: boolean) => {
offlineMode.value = value; offlineMode.value = value;
await invoke('set_global_config', { field: 'OfflineMode', value }); await invoke('set_global_config', { field: 'offline_mode', value });
}; };
const setAutoupdates = async (value: boolean) => { const setAutoupdates = async (value: boolean) => {
enableAutoupdates.value = value; enableAutoupdates.value = value;
await invoke('set_global_config', { await invoke('set_global_config', {
field: 'EnableAutoupdates', field: 'enable_autoupdates',
value, value,
}); });
}; };
const setVerbose = async (value: boolean) => {
verbose.value = value;
await invoke('set_global_config', { field: 'verbose', value });
};
getCurrentWindow().onResized(async ({ payload }) => { getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized // For whatever reason this is 0 when minimized
if (payload.width > 0) { if (payload.width > 0) {
@ -460,6 +479,7 @@ export const useClientStore = defineStore('client', () => {
scaleFactor, scaleFactor,
offlineMode, offlineMode,
enableAutoupdates, enableAutoupdates,
verbose,
timeout, timeout,
scaleModel, scaleModel,
load, load,
@ -467,5 +487,6 @@ export const useClientStore = defineStore('client', () => {
queueSave, queueSave,
setOfflineMode, setOfflineMode,
setAutoupdates, setAutoupdates,
setVerbose,
}; };
}); });