34 Commits

Author SHA1 Message Date
edef5cc6dc fix: also replace download URLs 2025-04-30 07:35:54 +00:00
2dad0de4f1 fix: update rainycolor's domain 2025-04-30 06:59:38 +00:00
14a65eb5bb fix: keyboard unbinding and IR fixes 2025-04-29 19:59:21 +00:00
0add9200a6 feat: new grouping options 2025-04-28 22:00:33 +00:00
ee49da3665 fix: category sort 2025-04-28 16:47:45 +00:00
f478ad9216 feat: add package creator 2025-04-28 16:44:04 +00:00
c59dbcc35c feat: add polish localization 2025-04-27 20:37:46 +00:00
91d38b58c4 fix: localization fixes 2025-04-27 20:30:22 +00:00
240f60b283 fix: some more polish 2025-04-27 18:53:01 +00:00
6a32ad65a5 feat: onboarding i18n 2025-04-27 07:35:38 +00:00
6cc7a537b6 feat: remember current tab 2025-04-27 05:56:04 +00:00
bf4c06ee2d fix: begin fixing linux support 2025-04-23 17:17:59 +02:00
f26d83f291 fix: misc cleanup 2025-04-23 14:24:35 +00:00
8b2c1a04ee fix: remove chuniio from segatools-chunithm.ini 2025-04-23 13:37:19 +00:00
ce03668252 feat: internationalization 2025-04-22 21:34:55 +00:00
58c692a879 chore: bump ver 2025-04-21 22:08:30 +00:00
e569d57788 feat: hex patches 2025-04-21 22:05:37 +00:00
b75cc8f240 docs: update README.md 2025-04-21 04:19:12 -12:00
407b34a884 feat: profile imports/exports 2025-04-21 04:15:52 -12:00
890d26e883 chore: bump ver 2025-04-20 06:38:44 +00:00
2aff5834b9 fix: chunithm crashing with mempatcher 2025-04-20 06:37:46 +00:00
69f2c83109 chore: update CHANGELOG.md 2025-04-19 20:13:11 +00:00
dbbd80c6c3 feat: add 'games' to the manifest 2025-04-19 20:09:32 +00:00
3479804dca feat: 0.12 update 2025-04-19 19:48:08 +00:00
aaeed669df chore: bump ver 2025-04-19 11:46:07 +00:00
7084f40404 fix: improve help pages 2025-04-19 11:44:16 +00:00
f7e9d7d7db docs: rewrite README.md 2025-04-18 19:55:42 +00:00
e87b661f08 feat: onboarding 2025-04-18 15:00:52 +00:00
5d2d407659 chore: update CHANGELOG.md 2025-04-18 06:59:55 +00:00
795e889bd0 fix: better keyboard
* Scale the font as necessary
* Fix CHUNITHM order
* Fix num-unlocked numpad
2025-04-18 06:55:59 +00:00
7071f19877 fix: don't switch primary, for real 2025-04-17 18:43:27 +00:00
a72ec25088 chore: update CHANGELOG.md 2025-04-17 07:49:03 +00:00
5893536daa chore: bump ver 2025-04-17 07:44:58 +00:00
e9550e8eee feat: global progress bar
Also fix me having no foresight and executing things
inside log::debug! macros
2025-04-17 07:44:05 +00:00
75 changed files with 3645 additions and 1014 deletions

View File

@ -1,3 +1,89 @@
## 0.18.3
- Updated Rainycolor's domain・真
## 0.18.2
- Updated Rainycolor's domain
## 0.18.1
- Keys can now be unbinded with Esc
- Fixed CHUNITHM IR behavior on actual keyboards
## 0.18.0
- Added new grouping options to the package list
## 0.17.0
- Added a package creation prompt
- Added a default package icon
## 0.16.0
- Fixed the clear cache button not working
- Fixed Linux builds
- Moved the store tab to the left
- "Reapply mods and start" renamed from "Refresh and start" to better convey the meaning
- "Reapply mods and start" is no longer necessary when enabling packages from the `local` namespace
- Various internationalization additions
- STARTLINER now remembers the recently open tab and re-opens it on the next session
- Added "Beta" to the title as STARTLINER is approaching feature-completeness
- Added full Polish localization :smciota:
## 0.15.0
- Added internationalization
- Moved some client options to the home page
- Added 'dll-game32' and 'dll-game64' entries for native mods that target both Chunithm and Ongeki
- Ongeki: added mempatcher support (amdaemon)
## 0.14.0
- Added the custom FREE PLAY patch for Verse
## 0.13.0
- Added profile imports/exports
- Fixed error when trying to open an empty Ongeki profile
- Switched the default color scheme from invisible to purple
## 0.12.1
- Chunithm: fixed crash when using mempatcher
## 0.12.0
- Ongeki: cache and mu3.ini config are now split per-profile (requires mu3-mods 3.7+)
- Ongeki: added the few config options of mu3.ini that aren't available in TestMenuConfig, or require a restart
- Chunithm: added Lumi+ patches
- Added support for a non-standard `games` manifest entry intended for local packages
- Expected to be an array containing "ongeki", "chunithm" or both
- Example: { "games": ["ongeki"] }
- Added a button linking to the profile config folder
- Fixed the button linking to the data folder showing up when the folder does not exist
- Uninstalled tool packages are no longer automatically deselected, as that caused issues
## 0.11.1
- Improved help pages
## 0.11.0
- Added help pages
## 0.10.1
- Fixed the order of cells in the CHUNITHM keyboard
- Fixed numpad bindings with numlock disabled
- Disabled primary monitor cleanup when "don't switch primary monitor" is enabled
## 0.10.0
- Added a global progress bar
- Fixed issues with downloading under certain conditions
## 0.9.0
- Added a light/dark theme switcher

View File

@ -1,17 +1,24 @@
Looking for
- maimai DX players willing to help develop/test maimai DX support
- translators (any language other than English)
# STARTLINER
A simple and easy to use launcher, configuration tool and mod manager
for O.N.G.E.K.I. and CHUNITHM, using [Rainycolor Watercolor](https://rainy.patafour.zip).
This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
STARTLINER is four things:
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainycolor.org),
- a configuration GUI for segatools,
- a glorified `start.bat` clicker, with automatic monitor setup and rollback,
- [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).
STARTLINER's core design principle is to modify, configure and launch games without tampering with them.
This makes it possible to keep data cleaner than ever, and to have several configurations pointing at the same data.
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
## Features
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
- Segatools configuration
- Monitor configuration with automatic rollback
- Support for multiple configurations pointing at the same data
## Usage
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:

View File

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

231
bun.lock
View File

@ -8,15 +8,15 @@
"@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.4.1",
"@tailwindcss/vite": "^4.1.4",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1",
"@tauri-apps/plugin-dialog": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@tauri-apps/plugin-updater": "^2.7.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
@ -24,13 +24,14 @@
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"roboto-fontface": "^0.10.0",
"tailwindcss": "^4.1.3",
"tailwindcss": "^4.1.4",
"tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13",
"vuetify": "^3.8.1",
"vue-i18n": "^11.1.3",
"vuetify": "^3.8.2",
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "^2.5.0",
"@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
@ -38,11 +39,11 @@
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2",
"sass": "1.77.8",
"sass-embedded": "^1.86.3",
"sass-embedded": "^1.87.0",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.2.6",
"vite": "^6.3.2",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8",
},
@ -145,6 +146,12 @@
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="],
"@intlify/core-base": ["@intlify/core-base@11.1.3", "", { "dependencies": { "@intlify/message-compiler": "11.1.3", "@intlify/shared": "11.1.3" } }, "sha512-cMuHunYO7LE80azTitcvEbs1KJmtd6g7I5pxlApV3Jo547zdO3h31/0uXpqHc+Y3RKt1wo2y68RGSx77Z1klyA=="],
"@intlify/message-compiler": ["@intlify/message-compiler@11.1.3", "", { "dependencies": { "@intlify/shared": "11.1.3", "source-map-js": "^1.0.2" } }, "sha512-7rbqqpo2f5+tIcwZTAG/Ooy9C8NDVwfDkvSeDPWUPQW+Dyzfw2o9H103N5lKBxO7wxX9dgCDjQ8Umz73uYw3hw=="],
"@intlify/shared": ["@intlify/shared@11.1.3", "", {}, "sha512-pTFBgqa/99JRA2H1qfyqv97MKWJrYngXBA/I0elZcYxvJgcCw3mApAoPW3mJ7vx3j+Ti0FyKUFZ4hWxdjKaxvA=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
@ -183,97 +190,101 @@
"@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.6", "", { "os": "android", "cpu": "arm" }, "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.40.0", "", { "os": "android", "cpu": "arm" }, "sha512-+Fbls/diZ0RDerhE8kyC6hjADCXA1K4yVNlH0EYfd2XjyH0UGgzaQ8MlT0pCXAThfxv3QUAczHaL+qSv1E4/Cg=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.6", "", { "os": "android", "cpu": "arm64" }, "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.40.0", "", { "os": "android", "cpu": "arm64" }, "sha512-PPA6aEEsTPRz+/4xxAmaoWDqh67N7wFbgFUJGMnanCFs0TV99M0M8QhhaSCks+n6EbQoFvLQgYOGXxlMGQe/6w=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.40.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GwYOcOakYHdfnjjKwqpTGgn5a6cUX7+Ra2HeNj/GdXvO2VJOOXCiYYlRFU4CubFM67EhbmzLOmACKEfvp3J1kQ=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.40.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-CoLEGJ+2eheqD9KBSxmma6ld01czS52Iw0e2qMZNpPDlf7Z9mj8xmMemxEucinev4LgHalDPczMyxzbq+Q+EtA=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.6", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.40.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-r7yGiS4HN/kibvESzmrOB/PxKMhPTlz+FcGvoUIKYoTyGd5toHp48g1uZy1o1xQvybwwpqpe010JrcGG2s5nkg=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.40.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mVDxzlf0oLzV3oZOr0SMJ0lSDd3xC4CmnWJ8Val8isp9jRGl5Dq//LLDSPFrasS7pSm6m5xAcKaw3sHXhBjoRw=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.6", "", { "os": "linux", "cpu": "arm" }, "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-y/qUMOpJxBMy8xCXD++jeu8t7kzjlOCkoxxajL58G62PJGBZVl/Gwpm7JK9+YvlB701rcQTzjUZ1JgUoPTnoQA=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.6", "", { "os": "linux", "cpu": "arm" }, "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.40.0", "", { "os": "linux", "cpu": "arm" }, "sha512-GoCsPibtVdJFPv/BOIvBKO/XmwZLwaNWdyD8TKlXuqp0veo2sHE+A/vpMQ5iSArRUz/uaoj4h5S6Pn0+PdhRjg=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-L5ZLphTjjAD9leJzSLI7rr8fNqJMlGDKlazW2tX4IUF9P7R5TMQPElpH82Q7eNIDQnQlAyiNVfRPfP2vM5Avvg=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.40.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATZvCRGCDtv1Y4gpDIXsS+wfFeFuLwVxyUBSLawjgXK2tRE6fnsQEkE4csQQYWlBlsFztRzCnBvWVfcae/1qxQ=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.6", "", { "os": "linux", "cpu": "none" }, "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-wG9e2XtIhd++QugU5MD9i7OnpaVb08ji3P1y/hNbxrQ3sYEelKJOq1UJ5dXczeo6Hj2rfDEL5GdtkMSVLa/AOg=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.6", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.40.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-vgXfWmj0f3jAUvC7TZSU/m/cOE558ILWDzS7jBhiCAFpY2WEBn5jqgbqvmzlMjtp8KlLcBlXVD2mkTSEQE6Ixw=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.6", "", { "os": "linux", "cpu": "none" }, "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-uJkYTugqtPZBS3Z136arevt/FsKTF/J9dEMTX/cwR7lsAW4bShzI2R0pJVw+hcBTWF4dxVckYh72Hk3/hWNKvA=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.6", "", { "os": "linux", "cpu": "s390x" }, "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.40.0", "", { "os": "linux", "cpu": "none" }, "sha512-rKmSj6EXQRnhSkE22+WvrqOqRtk733x3p5sWpZilhmjnkHkpeCgWsFFo0dGnUGeA+OZjRl3+VYq+HyCOEuwcxQ=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.6", "", { "os": "linux", "cpu": "x64" }, "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.40.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-SpnYlAfKPOoVsQqmTFJ0usx0z84bzGOS9anAC0AZ3rdSo3snecihbhFTlJZ8XMwzqAcodjFU4+/SM311dqE5Sw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.6", "", { "os": "linux", "cpu": "x64" }, "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-RcDGMtqF9EFN8i2RYN2W+64CdHruJ5rPqrlYw+cgM3uOVPSsnAQps7cpjXe9be/yDp8UC7VLoCoKC8J3Kn2FkQ=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.40.0", "", { "os": "linux", "cpu": "x64" }, "sha512-HZvjpiUmSNx5zFgwtQAV1GaGazT2RWvqeDi0hV+AtC8unqqDSsaFjPxfsO6qPtKRRg25SisACWnJ37Yio8ttaw=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.40.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-UtZQQI5k/b8d7d3i9AZmA/t+Q4tk3hOC0tMOMSq2GlMYOfxbesxG4mJSeDp0EHs30N9bsfwUvs3zF4v/RzOeTQ=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.6", "", { "os": "win32", "cpu": "x64" }, "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.40.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-+m03kvI2f5syIqHXCZLPVYplP8pQch9JHyXKZ3AGMKlg8dCyr2PKHjwRLiW53LTrN/Nc3EqHOKxUxzoSPdKddA=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.3", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.3" } }, "sha512-H/6r6IPFJkCfBJZ2dKZiPJ7Ueb2wbL592+9bQEl2r73qbX6yGnmQVIfiUvDRB2YI0a3PWDrzUwkvQx1XW1bNkA=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.40.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lpPE1cLfP5oPzVjKMx10pgBmKELQnFJXHgvtHCtuJWOv8MxqdEIMNtgHgBFf7Ea2/7EuVwa9fodWUfXAlXZLZQ=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.3", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.3", "@tailwindcss/oxide-darwin-arm64": "4.1.3", "@tailwindcss/oxide-darwin-x64": "4.1.3", "@tailwindcss/oxide-freebsd-x64": "4.1.3", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.3", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.3", "@tailwindcss/oxide-linux-arm64-musl": "4.1.3", "@tailwindcss/oxide-linux-x64-gnu": "4.1.3", "@tailwindcss/oxide-linux-x64-musl": "4.1.3", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.3", "@tailwindcss/oxide-win32-x64-msvc": "4.1.3" } }, "sha512-t16lpHCU7LBxDe/8dCj9ntyNpXaSTAgxWm1u2XQP5NiIu4KGSyrDJJRlK9hJ4U9yJxx0UKCVI67MJWFNll5mOQ=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.4", "", { "dependencies": { "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.29.2", "tailwindcss": "4.1.4" } }, "sha512-MT5118zaiO6x6hNA04OWInuAiP1YISXql8Z+/Y8iisV5nuhM8VXlyhRuqc2PEviPszcXI66W44bCIk500Oolhw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.3", "", { "os": "android", "cpu": "arm64" }, "sha512-cxklKjtNLwFl3mDYw4XpEfBY+G8ssSg9ADL4Wm6//5woi3XGqlxFsnV5Zb6v07dxw1NvEX2uoqsxO/zWQsgR+g=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.4", "", { "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.4", "@tailwindcss/oxide-darwin-arm64": "4.1.4", "@tailwindcss/oxide-darwin-x64": "4.1.4", "@tailwindcss/oxide-freebsd-x64": "4.1.4", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.4", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.4", "@tailwindcss/oxide-linux-arm64-musl": "4.1.4", "@tailwindcss/oxide-linux-x64-gnu": "4.1.4", "@tailwindcss/oxide-linux-x64-musl": "4.1.4", "@tailwindcss/oxide-wasm32-wasi": "4.1.4", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.4", "@tailwindcss/oxide-win32-x64-msvc": "4.1.4" } }, "sha512-p5wOpXyOJx7mKh5MXh5oKk+kqcz8T+bA3z/5VWWeQwFrmuBItGwz8Y2CHk/sJ+dNb9B0nYFfn0rj/cKHZyjahQ=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mqkf2tLR5VCrjBvuRDwzKNShRu99gCAVMkVsaEOFvv6cCjlEKXRecPu9DEnxp6STk5z+Vlbh1M5zY3nQCXMXhw=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.4", "", { "os": "android", "cpu": "arm64" }, "sha512-xMMAe/SaCN/vHfQYui3fqaBDEXMu22BVwQ33veLc8ep+DNy7CWN52L+TTG9y1K397w9nkzv+Mw+mZWISiqhmlA=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-7sGraGaWzXvCLyxrc7d+CCpUN3fYnkkcso3rCzwUmo/LteAl2ZGCDlGvDD8Y/1D3ngxT8KgDj1DSwOnNewKhmg=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-JGRj0SYFuDuAGilWFBlshcexev2hOKfNkoX+0QTksKYq2zgF9VY/vVMq9m8IObYnLna0Xlg+ytCi2FN2rOL0Sg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.3", "", { "os": "freebsd", "cpu": "x64" }, "sha512-E2+PbcbzIReaAYZe997wb9rId246yDkCwAakllAWSGqe6VTg9hHle67hfH6ExjpV2LSK/siRzBUs5wVff3RW9w=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-sdDeLNvs3cYeWsEJ4H1DvjOzaGios4QbBTNLVLVs0XQ0V95bffT3+scptzYGPMjm7xv4+qMhCDrkHwhnUySEzA=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.3", "", { "os": "linux", "cpu": "arm" }, "sha512-GvfbJ8wjSSjbLFFE3UYz4Eh8i4L6GiEYqCtA8j2Zd2oXriPuom/Ah/64pg/szWycQpzRnbDiJozoxFU2oJZyfg=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.4", "", { "os": "freebsd", "cpu": "x64" }, "sha512-VHxAqxqdghM83HslPhRsNhHo91McsxRJaEnShJOMu8mHmEj9Ig7ToHJtDukkuLWLzLboh2XSjq/0zO6wgvykNA=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-35UkuCWQTeG9BHcBQXndDOrpsnt3Pj9NVIB4CgNiKmpG8GnCNXeMczkUpOoqcOhO6Cc/mM2W7kaQ/MTEENDDXg=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.4", "", { "os": "linux", "cpu": "arm" }, "sha512-OTU/m/eV4gQKxy9r5acuesqaymyeSCnsx1cFto/I1WhPmi5HDxX1nkzb8KYBiwkHIGg7CTfo/AcGzoXAJBxLfg=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-dm18aQiML5QCj9DQo7wMbt1Z2tl3Giht54uVR87a84X8qRtuXxUqnKQkRDK5B4bCOmcZ580lF9YcoMkbDYTXHQ=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-hKlLNvbmUC6z5g/J4H+Zx7f7w15whSVImokLPmP6ff1QqTVE+TxUM9PGuNsjHvkvlHUtGTdDnOvGNSEUiXI1Ww=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-LMdTmGe/NPtGOaOfV2HuO7w07jI3cflPrVq5CXl+2O93DCewADK0uW1ORNAcfu2YxDUS035eY2W38TxrsqngxA=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-X3As2xhtgPTY/m5edUtddmZ8rCruvBvtxYLMw9OsZdH01L2gS2icsHRwxdU0dMItNfVmrBezueXZCHxVeeb7Aw=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-aalNWwIi54bbFEizwl1/XpmdDrOaCjRFQRgtbv9slWjmNPuJJTIKPHf5/XXDARc9CneW9FkSTqTbyvNecYAEGw=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-2VG4DqhGaDSmYIu6C4ua2vSLXnJsb/C9liej7TuSO04NK+JJJgJucDUgmX6sn7Gw3Cs5ZJ9ZLrnI0QRDOjLfNQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-PEj7XR4OGTGoboTIAdXicKuWl4EQIjKHKuR+bFy9oYN7CFZo0eu74+70O4XuERX4yjqVZGAkCdglBODlgqcCXg=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.4", "", { "os": "linux", "cpu": "x64" }, "sha512-v+mxVgH2kmur/X5Mdrz9m7TsoVjbdYQT0b4Z+dr+I4RvreCNXyCFELZL/DO0M1RsidZTrm6O1eMnV6zlgEzTMQ=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-T8gfxECWDBENotpw3HR9SmNiHC9AOJdxs+woasRZ8Q/J4VHN0OMs7F+4yVNZ9EVN26Wv6mZbK0jv7eHYuLJLwA=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.4", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@emnapi/wasi-threads": "^1.0.1", "@napi-rs/wasm-runtime": "^0.2.8", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-2TLe9ir+9esCf6Wm+lLWTMbgklIjiF0pbmDnwmhR9MksVOq+e8aP3TSsXySnBDDvTTVd/vKu1aNttEGj3P6l8Q=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.3", "", { "dependencies": { "@tailwindcss/node": "4.1.3", "@tailwindcss/oxide": "4.1.3", "tailwindcss": "4.1.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-lUI/QaDxLtlV52Lho6pu07CG9pSnRYLOPmKGIQjyHdTBagemc6HmgZxyjGAQ/5HMPrNeWBfTVIpQl0/jLXvWHQ=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-VlnhfilPlO0ltxW9/BgfLI5547PYzqBMPIzRrk4W7uupgCt8z6Trw/tAj6QUtF2om+1MH281Pg+HHUJoLesmng=="],
"@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.4", "", { "os": "win32", "cpu": "x64" }, "sha512-+7S63t5zhYjslUGb8NcgLpFXD+Kq1F/zt5Xv5qTv7HaFTG/DHyHD9GA6ieNAxhgyA4IcKa/zy7Xx4Oad2/wuhw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.4.1", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.4.1", "@tauri-apps/cli-darwin-x64": "2.4.1", "@tauri-apps/cli-linux-arm-gnueabihf": "2.4.1", "@tauri-apps/cli-linux-arm64-gnu": "2.4.1", "@tauri-apps/cli-linux-arm64-musl": "2.4.1", "@tauri-apps/cli-linux-riscv64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-gnu": "2.4.1", "@tauri-apps/cli-linux-x64-musl": "2.4.1", "@tauri-apps/cli-win32-arm64-msvc": "2.4.1", "@tauri-apps/cli-win32-ia32-msvc": "2.4.1", "@tauri-apps/cli-win32-x64-msvc": "2.4.1" }, "bin": { "tauri": "tauri.js" } }, "sha512-9Ta81jx9+57FhtU/mPIckDcOBtPTUdKM75t4+aA0X84b8Sclb0jy1xA8NplmcRzp2fsfIHNngU2NiRxsW5+yOQ=="],
"@tailwindcss/vite": ["@tailwindcss/vite@4.1.4", "", { "dependencies": { "@tailwindcss/node": "4.1.4", "@tailwindcss/oxide": "4.1.4", "tailwindcss": "4.1.4" }, "peerDependencies": { "vite": "^5.2.0 || ^6" } }, "sha512-4UQeMrONbvrsXKXXp/uxmdEN5JIJ9RkH7YVzs6AMxC/KC1+Np7WZBaNIco7TEjlkthqxZbt8pU/ipD+hKjm80A=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.4.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QME7s8XQwy3LWClTVlIlwXVSLKkeJ/z88pr917Mtn9spYOjnBfsgHAgGdmpWD3NfJxjg7CtLbhH49DxoFL+hLg=="],
"@tauri-apps/api": ["@tauri-apps/api@2.5.0", "", {}, "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.4.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-/r89IcW6Ya1sEsFUEH7wLNruDTj7WmDWKGpPy7gATFtQr5JEY4heernqE82isjTUimnHZD8SCr0jA3NceI4ybw=="],
"@tauri-apps/cli": ["@tauri-apps/cli@2.5.0", "", { "optionalDependencies": { "@tauri-apps/cli-darwin-arm64": "2.5.0", "@tauri-apps/cli-darwin-x64": "2.5.0", "@tauri-apps/cli-linux-arm-gnueabihf": "2.5.0", "@tauri-apps/cli-linux-arm64-gnu": "2.5.0", "@tauri-apps/cli-linux-arm64-musl": "2.5.0", "@tauri-apps/cli-linux-riscv64-gnu": "2.5.0", "@tauri-apps/cli-linux-x64-gnu": "2.5.0", "@tauri-apps/cli-linux-x64-musl": "2.5.0", "@tauri-apps/cli-win32-arm64-msvc": "2.5.0", "@tauri-apps/cli-win32-ia32-msvc": "2.5.0", "@tauri-apps/cli-win32-x64-msvc": "2.5.0" }, "bin": { "tauri": "tauri.js" } }, "sha512-rAtHqG0Gh/IWLjN2zTf3nZqYqbo81oMbqop56rGTjrlWk9pTTAjkqOjSL9XQLIMZ3RbeVjveCqqCA0s8RnLdMg=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.4.1", "", { "os": "linux", "cpu": "arm" }, "sha512-9tDijkRB+CchAGjXxYdY9l/XzFpLp1yihUtGXJz9eh+3qIoRI043n3e+6xmU8ZURr7XPnu+R4sCmXs6HD+NCEQ=="],
"@tauri-apps/cli-darwin-arm64": ["@tauri-apps/cli-darwin-arm64@2.5.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-VuVAeTFq86dfpoBDNYAdtQVLbP0+2EKCHIIhkaxjeoPARR0sLpFHz2zs0PcFU76e+KAaxtEtAJAXGNUc8E1PzQ=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-pnFGDEXBAzS4iDYAVxTRhAzNu3K2XPGflYyBc0czfHDBXopqRgMyj5Q9Wj7HAwv6cM8BqzXINxnb2ZJFGmbSgA=="],
"@tauri-apps/cli-darwin-x64": ["@tauri-apps/cli-darwin-x64@2.5.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-hUF01sC06cZVa8+I0/VtsHOk9BbO75rd+YdtHJ48xTdcYaQ5QIwL4yZz9OR1AKBTaUYhBam8UX9Pvd5V2/4Dpw=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.4.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-Hp0zXgeZNKmT+eoJSCxSBUm2QndNuRxR55tmIeNm3vbyUMJN/49uW7nurZ5fBPsacN4Pzwlx1dIMK+Gnr9A69w=="],
"@tauri-apps/cli-linux-arm-gnueabihf": ["@tauri-apps/cli-linux-arm-gnueabihf@2.5.0", "", { "os": "linux", "cpu": "arm" }, "sha512-LQKqttsK252LlqYyX8R02MinUsfFcy3+NZiJwHFgi5Y3+ZUIAED9cSxJkyNtuY5KMnR4RlpgWyLv4P6akN1xhg=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.4.1", "", { "os": "linux", "cpu": "none" }, "sha512-3T3bo2E4fdYRvzcXheWUeQOVB+LunEEi92iPRgOyuSVexVE4cmHYl+MPJF+EUV28Et0hIVTsHibmDO0/04lAFg=="],
"@tauri-apps/cli-linux-arm64-gnu": ["@tauri-apps/cli-linux-arm64-gnu@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-mTQufsPcpdHg5RW0zypazMo4L55EfeE5snTzrPqbLX4yCK2qalN7+rnP8O8GT06xhp6ElSP/Ku1M2MR297SByQ=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-kLN0FdNONO+2i+OpU9+mm6oTGufRC00e197TtwjpC0N6K2K8130w7Q3FeODIM2CMyg0ov3tH+QWqKW7GNhHFzg=="],
"@tauri-apps/cli-linux-arm64-musl": ["@tauri-apps/cli-linux-arm64-musl@2.5.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-rQO1HhRUQqyEaal5dUVOQruTRda/TD36s9kv1hTxZiFuSq3558lsTjAcUEnMAtBcBkps20sbyTJNMT0AwYIk8Q=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.4.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8exvA5Ub9eg66a6hsMQKJIkf63QAf9OdiuFKOsEnKZkNN2x0NLgfvEcqdw88VY0UMs9dBoZ1AGbWMeYnLrLwQ=="],
"@tauri-apps/cli-linux-riscv64-gnu": ["@tauri-apps/cli-linux-riscv64-gnu@2.5.0", "", { "os": "linux", "cpu": "none" }, "sha512-7oS18FN46yDxyw1zX/AxhLAd7T3GrLj3Ai6s8hZKd9qFVzrAn36ESL7d3G05s8wEtsJf26qjXnVF4qleS3dYsA=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.4.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-4JFrslsMCJQG1c573T9uqQSAbF3j/tMKkMWzsIssv8jvPiP++OG61A2/F+y9te9/Q/O95cKhDK63kaiO5xQaeg=="],
"@tauri-apps/cli-linux-x64-gnu": ["@tauri-apps/cli-linux-x64-gnu@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SG5sFNL7VMmDBdIg3nO3EzNRT306HsiEQ0N90ILe3ZABYAVoPDO/ttpCO37ApLInTzrq/DLN+gOlC/mgZvLw1w=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.4.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-9eXfFORehYSCRwxg2KodfmX/mhr50CI7wyBYGbPLePCjr5z0jK/9IyW6r0tC+ZVjwpX48dkk7hKiUgI25jHjzA=="],
"@tauri-apps/cli-linux-x64-musl": ["@tauri-apps/cli-linux-x64-musl@2.5.0", "", { "os": "linux", "cpu": "x64" }, "sha512-QXDM8zp/6v05PNWju5ELsVwF0VH1n6b5pk2E6W/jFbbiwz80Vs1lACl9pv5kEHkrxBj+aWU/03JzGuIj2g3SkQ=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.4.1", "", { "os": "win32", "cpu": "x64" }, "sha512-60a4Ov7Jrwqz2hzDltlS7301dhSAmM9dxo+IRBD3xz7yobKrgaHXYpWvnRomYItHcDd51VaKc9292H8/eE/gsw=="],
"@tauri-apps/cli-win32-arm64-msvc": ["@tauri-apps/cli-win32-arm64-msvc@2.5.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-pFSHFK6b+o9y4Un8w0gGLwVyFTZaC3P0kQ7umRt/BLDkzD5RnQ4vBM7CF8BCU5nkwmEBUCZd7Wt3TWZxe41o6Q=="],
"@tauri-apps/cli-win32-ia32-msvc": ["@tauri-apps/cli-win32-ia32-msvc@2.5.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-EArv1IaRlogdLAQyGlKmEqZqm5RfHCUMhJoedWu7GtdbOMUfSAz6FMX2boE1PtEmNO4An+g188flLeVErrxEKg=="],
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.5.0", "", { "os": "win32", "cpu": "x64" }, "sha512-lj43EFYbnAta8pd9JnUq87o+xRUR0odz+4rixBtTUwUgdRdwQ2V9CzFtsMu6FQKpFQ6mujRK6P1IEwhL6ADRsQ=="],
"@tauri-apps/plugin-cli": ["@tauri-apps/plugin-cli@2.2.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-rvNhMog9rHr01Xk+trBFKJ0eZICIvPkm9GX6ogB89/0hROU/lf+a/sb4vC0wtSeR7zrJuCSxwxYuvHCZheaYFA=="],
@ -287,7 +298,7 @@
"@tauri-apps/plugin-shell": ["@tauri-apps/plugin-shell@2.2.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-G1GFYyWe/KlCsymuLiNImUgC8zGY0tI0Y3p8JgBCWduR5IEXlIJS+JuG1qtveitwYXlfJrsExt3enhv5l2/yhA=="],
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.7.0", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-oBug5UCH2wOsoYk0LW5LEMAT51mszjg11s8eungRH26x/qOrEjLvnuJJoxVVr9nsWowJ6vnpXKS+lUMfFTlvHQ=="],
"@tauri-apps/plugin-updater": ["@tauri-apps/plugin-updater@2.7.1", "", { "dependencies": { "@tauri-apps/api": "^2.0.0" } }, "sha512-1OPqEY/z7NDVSeTEMIhD2ss/vXWdpfZ5Th2Mk0KtPR/RA6FKuOTDGZQhxoyYBk0pcZJ+nNZUbl/IujDCLBApjA=="],
"@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@5.2.2", "", { "dependencies": { "@babel/generator": "^7.26.5", "@babel/parser": "^7.26.7", "@babel/traverse": "^7.26.7", "@babel/types": "^7.26.7", "javascript-natural-sort": "^0.7.1", "lodash": "^4.17.21" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-svelte", "svelte"] }, "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA=="],
@ -461,6 +472,8 @@
"fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
"fdir": ["fdir@6.4.4", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg=="],
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
@ -653,7 +666,7 @@
"roboto-fontface": ["roboto-fontface@0.10.0", "", {}, "sha512-OlwfYEgA2RdboZohpldlvJ1xngOins5d7ejqnIBWr9KaMxsnBqotpptRXTyfNRLnFpqzX6sTDt+X+a+6udnU8g=="],
"rollup": ["rollup@4.34.6", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.6", "@rollup/rollup-android-arm64": "4.34.6", "@rollup/rollup-darwin-arm64": "4.34.6", "@rollup/rollup-darwin-x64": "4.34.6", "@rollup/rollup-freebsd-arm64": "4.34.6", "@rollup/rollup-freebsd-x64": "4.34.6", "@rollup/rollup-linux-arm-gnueabihf": "4.34.6", "@rollup/rollup-linux-arm-musleabihf": "4.34.6", "@rollup/rollup-linux-arm64-gnu": "4.34.6", "@rollup/rollup-linux-arm64-musl": "4.34.6", "@rollup/rollup-linux-loongarch64-gnu": "4.34.6", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6", "@rollup/rollup-linux-riscv64-gnu": "4.34.6", "@rollup/rollup-linux-s390x-gnu": "4.34.6", "@rollup/rollup-linux-x64-gnu": "4.34.6", "@rollup/rollup-linux-x64-musl": "4.34.6", "@rollup/rollup-win32-arm64-msvc": "4.34.6", "@rollup/rollup-win32-ia32-msvc": "4.34.6", "@rollup/rollup-win32-x64-msvc": "4.34.6", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ=="],
"rollup": ["rollup@4.40.0", "", { "dependencies": { "@types/estree": "1.0.7" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.40.0", "@rollup/rollup-android-arm64": "4.40.0", "@rollup/rollup-darwin-arm64": "4.40.0", "@rollup/rollup-darwin-x64": "4.40.0", "@rollup/rollup-freebsd-arm64": "4.40.0", "@rollup/rollup-freebsd-x64": "4.40.0", "@rollup/rollup-linux-arm-gnueabihf": "4.40.0", "@rollup/rollup-linux-arm-musleabihf": "4.40.0", "@rollup/rollup-linux-arm64-gnu": "4.40.0", "@rollup/rollup-linux-arm64-musl": "4.40.0", "@rollup/rollup-linux-loongarch64-gnu": "4.40.0", "@rollup/rollup-linux-powerpc64le-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-gnu": "4.40.0", "@rollup/rollup-linux-riscv64-musl": "4.40.0", "@rollup/rollup-linux-s390x-gnu": "4.40.0", "@rollup/rollup-linux-x64-gnu": "4.40.0", "@rollup/rollup-linux-x64-musl": "4.40.0", "@rollup/rollup-win32-arm64-msvc": "4.40.0", "@rollup/rollup-win32-ia32-msvc": "4.40.0", "@rollup/rollup-win32-x64-msvc": "4.40.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-Noe455xmA96nnqH5piFtLobsGbCij7Tu+tb3c1vYjNbTkfzGqXqQXG3wJaYXkRZuQ0vEYN4bhwg7QnIrqB5B+w=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
@ -661,47 +674,47 @@
"sass": ["sass@1.77.8", "", { "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", "source-map-js": ">=0.6.2 <2.0.0" }, "bin": { "sass": "sass.js" } }, "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ=="],
"sass-embedded": ["sass-embedded@1.86.3", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-android-arm": "1.86.3", "sass-embedded-android-arm64": "1.86.3", "sass-embedded-android-ia32": "1.86.3", "sass-embedded-android-riscv64": "1.86.3", "sass-embedded-android-x64": "1.86.3", "sass-embedded-darwin-arm64": "1.86.3", "sass-embedded-darwin-x64": "1.86.3", "sass-embedded-linux-arm": "1.86.3", "sass-embedded-linux-arm64": "1.86.3", "sass-embedded-linux-ia32": "1.86.3", "sass-embedded-linux-musl-arm": "1.86.3", "sass-embedded-linux-musl-arm64": "1.86.3", "sass-embedded-linux-musl-ia32": "1.86.3", "sass-embedded-linux-musl-riscv64": "1.86.3", "sass-embedded-linux-musl-x64": "1.86.3", "sass-embedded-linux-riscv64": "1.86.3", "sass-embedded-linux-x64": "1.86.3", "sass-embedded-win32-arm64": "1.86.3", "sass-embedded-win32-ia32": "1.86.3", "sass-embedded-win32-x64": "1.86.3" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-3pZSp24ibO1hdopj+W9DuiWsZOb2YY6AFRo/jjutKLBkqJGM1nJjXzhAYfzRV+Xn5BX1eTI4bBTE09P0XNHOZg=="],
"sass-embedded": ["sass-embedded@1.87.0", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0", "buffer-builder": "^0.2.0", "colorjs.io": "^0.5.0", "immutable": "^5.0.2", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", "varint": "^6.0.0" }, "optionalDependencies": { "sass-embedded-android-arm": "1.87.0", "sass-embedded-android-arm64": "1.87.0", "sass-embedded-android-ia32": "1.87.0", "sass-embedded-android-riscv64": "1.87.0", "sass-embedded-android-x64": "1.87.0", "sass-embedded-darwin-arm64": "1.87.0", "sass-embedded-darwin-x64": "1.87.0", "sass-embedded-linux-arm": "1.87.0", "sass-embedded-linux-arm64": "1.87.0", "sass-embedded-linux-ia32": "1.87.0", "sass-embedded-linux-musl-arm": "1.87.0", "sass-embedded-linux-musl-arm64": "1.87.0", "sass-embedded-linux-musl-ia32": "1.87.0", "sass-embedded-linux-musl-riscv64": "1.87.0", "sass-embedded-linux-musl-x64": "1.87.0", "sass-embedded-linux-riscv64": "1.87.0", "sass-embedded-linux-x64": "1.87.0", "sass-embedded-win32-arm64": "1.87.0", "sass-embedded-win32-ia32": "1.87.0", "sass-embedded-win32-x64": "1.87.0" }, "bin": { "sass": "dist/bin/sass.js" } }, "sha512-1IA3iTJNh4BkkA/nidKiVwbmkxr9o6LsPegycHMX/JYs255zpocN5GdLF1+onohQCJxbs5ldr8osKV7qNaNBjg=="],
"sass-embedded-android-arm": ["sass-embedded-android-arm@1.86.3", "", { "os": "android", "cpu": "arm" }, "sha512-UyeXrFzZSvrGbvrWUBcspbsbivGgAgebLGJdSqJulgSyGbA6no3DWQ5Qpdd6+OAUC39BlpPu74Wx9s4RrVuaFw=="],
"sass-embedded-android-arm": ["sass-embedded-android-arm@1.87.0", "", { "os": "android", "cpu": "arm" }, "sha512-Z20u/Y1kFDpMbgiloR5YPLxNuMVeKQRC8e/n68oAAxf3u7rDSmNn2msi7USqgT1f2zdBBNawn/ifbFEla6JiHw=="],
"sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.86.3", "", { "os": "android", "cpu": "arm64" }, "sha512-q+XwFp6WgAv+UgnQhsB8KQ95kppvWAB7DSoJp+8Vino8b9ND+1ai3cUUZPE5u4SnLZrgo5NtrbPvN5KLc4Pfyg=="],
"sass-embedded-android-arm64": ["sass-embedded-android-arm64@1.87.0", "", { "os": "android", "cpu": "arm64" }, "sha512-uqeZoBuXm3W2KhxolScAAfWOLHL21e50g7AxlLmG0he7WZsWw6e9kSnmq301iLIFp4kvmXYXbXbNKAeu9ItRYA=="],
"sass-embedded-android-ia32": ["sass-embedded-android-ia32@1.86.3", "", { "os": "android", "cpu": "ia32" }, "sha512-gTJjVh2cRzvGujXj5ApPk/owUTL5SiO7rDtNLrzYAzi1N5HRuLYXqk3h1IQY3+eCOBjGl7mQ9XyySbJs/3hDvg=="],
"sass-embedded-android-ia32": ["sass-embedded-android-ia32@1.87.0", "", { "os": "android", "cpu": "ia32" }, "sha512-hSWTqo2Igdig528cUb1W1+emw9d1J4+nqOoR4tERS04zcwRRFNDiuBT0o5meV7nkEwE982F+h57YdcRXj8gTtg=="],
"sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.86.3", "", { "os": "android", "cpu": "none" }, "sha512-Po3JnyiCS16kd6REo1IMUbFGYtvL9O0rmKaXx5vOuBaJD1LPy2LiSSp7TU7wkJ9IxsTDGzFaSeP1I9qb6D8VVg=="],
"sass-embedded-android-riscv64": ["sass-embedded-android-riscv64@1.87.0", "", { "os": "android", "cpu": "none" }, "sha512-kBAPSjiTBLy5ua/0LRNAJwOAARhzFU7gP35fYORJcdBuz1lkIVPVnid1lh9qQ6Ce9MOJcr7VKFtGnTuqVeig5A=="],
"sass-embedded-android-x64": ["sass-embedded-android-x64@1.86.3", "", { "os": "android", "cpu": "x64" }, "sha512-+7h3jdDv/0kUFx0BvxYlq2fa7CcHiDPlta6k5OxO5K6jyqJwo9hc0Z052BoYEauWTqZ+vK6bB5rv2BIzq4U9nA=="],
"sass-embedded-android-x64": ["sass-embedded-android-x64@1.87.0", "", { "os": "android", "cpu": "x64" }, "sha512-ZHMrNdtdMSpJUYco2MesnlPwDTZftD3pqkkOMI2pbqarPoFUKJtP5k80nwCM0sJGtqfNE+O16w9yPght0CMiJg=="],
"sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.86.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EgLwV4ORm5Hr0DmIXo0Xw/vlzwLnfAiqD2jDXIglkBsc5czJmo4/IBdGXOP65TRnsgJEqvbU3aQhuawX5++x9A=="],
"sass-embedded-darwin-arm64": ["sass-embedded-darwin-arm64@1.87.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-7TK1JWJdCIRSdZv5CJv/HpDz/wIfwUy2FoPz9sVOEj1pDTH0N+VfJd5VutCddIdoQN9jr0ap8vwkc65FbAxV2A=="],
"sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.86.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-dfKhfrGPRNLWLC82vy/vQGmNKmAiKWpdFuWiePRtg/E95pqw+sCu6080Y6oQLfFu37Iq3MpnXiSpDuSo7UnPWA=="],
"sass-embedded-darwin-x64": ["sass-embedded-darwin-x64@1.87.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-2JiQzt7FmgUC4MYT2QvbeH/Bi3e76WEhaYoc5P3WyTW8unsHksyTdMuTuYe0Qf9usIyt6bmm5no/4BBw7c8Cig=="],
"sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.86.3", "", { "os": "linux", "cpu": "arm" }, "sha512-+fVCIH+OR0SMHn2NEhb/VfbpHuUxcPtqMS34OCV3Ka99LYZUJZqth4M3lT/ppGl52mwIVLNYzR4iLe6mdZ6mYA=="],
"sass-embedded-linux-arm": ["sass-embedded-linux-arm@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-z5P6INMsGXiUcq1sRRbksyQUhalFFYjTEexuxfSYdK3U2YQMADHubQh8pGzkWvFRPOpnh83RiGuwvpaARYHnsw=="],
"sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.86.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-tYq5rywR53Qtc+0KI6pPipOvW7a47ETY69VxfqI9BR2RKw2hBbaz0bIw6OaOgEBv2/XNwcWb7a4sr7TqgkqKAA=="],
"sass-embedded-linux-arm64": ["sass-embedded-linux-arm64@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-5z+mwJCbGZcg+q+MwdEVSh0ogFK7OSAe175Gsozzr/Izw34Q+RGUw9O82jsV2c4YNuTAQvzEHgIO5cvNvt3Quw=="],
"sass-embedded-linux-ia32": ["sass-embedded-linux-ia32@1.86.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-CmQ5OkqnaeLdaF+bMqlYGooBuenqm3LvEN9H8BLhjkpWiFW8hnYMetiqMcJjhrXLvDw601KGqA5sr/Rsg5s45g=="],
"sass-embedded-linux-ia32": ["sass-embedded-linux-ia32@1.87.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Xzcp+YPp0iakGL148Jl57CO+MxLuj2jsry3M+rc1cSnDlvkjNVs6TMxaL70GFeV5HdU2V60voYcgE7adDUtJjw=="],
"sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.86.3", "", { "os": "linux", "cpu": "arm" }, "sha512-SEm65SQknI4pl+mH5Xf231hOkHJyrlgh5nj4qDbiBG6gFeutaNkNIeRgKEg3cflXchCr8iV/q/SyPgjhhzQb7w=="],
"sass-embedded-linux-musl-arm": ["sass-embedded-linux-musl-arm@1.87.0", "", { "os": "linux", "cpu": "arm" }, "sha512-4PyqOWhRzyu06RRmpCCBOJdF4BOv7s446wrV6yODtEyyfSIDx3MJabo3KT0oJ1lTWSI/aU3R89bKx0JFXcIHHw=="],
"sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.86.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-4zOr2C/eW89rxb4ozTfn7lBzyyM5ZigA1ZSRTcAR26Qbg/t2UksLdGnVX9/yxga0d6aOi0IvO/7iM2DPPRRotg=="],
"sass-embedded-linux-musl-arm64": ["sass-embedded-linux-musl-arm64@1.87.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-HWE5eTRCoKzFZWsxOjDMTF5m4DDTQ0n7NJxSYiUXPBDydr9viPXbGOMYG7WVJLjiF7upr7DYo/mfp/SNTMlZyg=="],
"sass-embedded-linux-musl-ia32": ["sass-embedded-linux-musl-ia32@1.86.3", "", { "os": "linux", "cpu": "ia32" }, "sha512-84Tcld32LB1loiqUvczWyVBQRCChm0wNLlkT59qF29nxh8njFIVf9yaPgXcSyyjpPoD9Tu0wnq3dvVzoMCh9AQ=="],
"sass-embedded-linux-musl-ia32": ["sass-embedded-linux-musl-ia32@1.87.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-aQaPvlRn3kh93PLQvl6BcFKu8Ji92+42blFEkg6nMVvmugD5ZwH2TGFrX25ibx4CYxRpMS4ssF7a0i7vy5HB1Q=="],
"sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.86.3", "", { "os": "linux", "cpu": "none" }, "sha512-IxEqoiD7vdNpiOwccybbV93NljBy64wSTkUOknGy21SyV43C8uqESOwTwW9ywa3KufImKm8L3uQAW/B0KhJMWg=="],
"sass-embedded-linux-musl-riscv64": ["sass-embedded-linux-musl-riscv64@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-o5DxcqiFzET3KRWo+futHr/lhAMBP3tJGGx8YIgpHQYfvDMbsvE0hiFC+nZ/GF9dbcGd+ceIQwfvE5mcc7Gsjw=="],
"sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.86.3", "", { "os": "linux", "cpu": "x64" }, "sha512-ePeTPXUxPK6JgHcUfnrkIyDtyt+zlAvF22mVZv6y1g/PZFm1lSfX+Za7TYHg9KaYqaaXDiw6zICX4i44HhR8rA=="],
"sass-embedded-linux-musl-x64": ["sass-embedded-linux-musl-x64@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-dKxWsu9Wu/CyfzQmHdeiGqrRSzJ85VUjbSx+aP1/7ttmps3SSg+YW95PuqnCOa7GSuSreC3dKKpXHTywUxMLQA=="],
"sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.86.3", "", { "os": "linux", "cpu": "none" }, "sha512-NuXQ72dwfNLe35E+RaXJ4Noq4EkFwM65eWwCwxEWyJO9qxOx1EXiCAJii6x8kkOh5daWuMU0VAI1B9RsJaqqQQ=="],
"sass-embedded-linux-riscv64": ["sass-embedded-linux-riscv64@1.87.0", "", { "os": "linux", "cpu": "none" }, "sha512-Sy3ESZ4FwBiijvmTA9n+0p0w3MNCue1AgINVPzpAY27EFi0h49eqQm9SWfOkFqmkFS2zFRYowdQOr5Bbr2gOXA=="],
"sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.86.3", "", { "os": "linux", "cpu": "x64" }, "sha512-t8be9zJ5B82+og9bQmIQ83yMGYZMTMrlGA+uGWtYacmwg6w3093dk91Fx0YzNSZBp3Tk60qVYjCZnEIwy60x0g=="],
"sass-embedded-linux-x64": ["sass-embedded-linux-x64@1.87.0", "", { "os": "linux", "cpu": "x64" }, "sha512-+UfjakOcHHKTnEqB3EZ+KqzezQOe1emvy4Rs+eQhLyfekpYuNze/qlRvYxfKTmrtvDiUrIto8MXsyZfMLzkuMA=="],
"sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.86.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-4ghuAzjX4q8Nksm0aifRz8hgXMMxS0SuymrFfkfJlrSx68pIgvAge6AOw0edoZoe0Tf5ZbsWUWamhkNyNxkTvw=="],
"sass-embedded-win32-arm64": ["sass-embedded-win32-arm64@1.87.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-m1DS6FYUE0/fv+vt38uQB/kxR4UjnyD+2zcSc298pFmA0aYh/XZIPWw7RxG1HL3KLE1ZrGyu3254MPoxRhs3ig=="],
"sass-embedded-win32-ia32": ["sass-embedded-win32-ia32@1.86.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-tCaK4zIRq9mLRPxLzBAdYlfCuS/xLNpmjunYxeWkIwlJo+k53h1udyXH/FInnQ2GgEz0xMXyvH3buuPgzwWYsw=="],
"sass-embedded-win32-ia32": ["sass-embedded-win32-ia32@1.87.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-JztXLo59GMe2E6g+kCsyiERYhtZgkcyDYx6CrXoSTE5WaE+RbxRiCCCv8/1+hf406f08pUxJ8G0Ody7M5urtBA=="],
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.86.3", "", { "os": "win32", "cpu": "x64" }, "sha512-zS+YNKfTF4SnOfpC77VTb0qNZyTXrxnAezSoRV0xnw6HlY+1WawMSSB6PbWtmbvyfXNgpmJUttoTtsvJjRCucg=="],
"sass-embedded-win32-x64": ["sass-embedded-win32-x64@1.87.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4nQErpauvhgSo+7ClumGdjdf9sGx+U9yBgvhI0+zUw+D5YvraVgvA0Lk8Wuwntx2PqnvKUk8YDr/vxHJostv4Q=="],
"semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
@ -725,12 +738,14 @@
"sync-message-port": ["sync-message-port@1.1.3", "", {}, "sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg=="],
"tailwindcss": ["tailwindcss@4.1.3", "", {}, "sha512-2Q+rw9vy1WFXu5cIxlvsabCwhU2qUwodGq03ODhLJ0jW4ek5BUtoCsnLB0qG+m8AHgEsSJcJGDSDe06FXlP74g=="],
"tailwindcss": ["tailwindcss@4.1.4", "", {}, "sha512-1ZIUqtPITFbv/DxRmDr5/agPqJwF69d24m9qmM1939TJehgY539CtzeZRjbLt5G6fSy/7YqqYsfvoTEw9xUI2A=="],
"tailwindcss-primeui": ["tailwindcss-primeui@0.4.0", "", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-YYC7B7Yyzm1/4pEGgpf1ABAhbrKY++LuPoUamnKE7fTPO5Ct/Qr/dT+Uq2yiVhQnaW1zHQpYnThxfksaxhlDfQ=="],
"tapable": ["tapable@2.2.1", "", {}, "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ=="],
"tinyglobby": ["tinyglobby@0.2.13", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
@ -765,7 +780,7 @@
"varint": ["varint@6.0.0", "", {}, "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg=="],
"vite": ["vite@6.2.6", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw=="],
"vite": ["vite@6.3.2", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.3", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.12" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-ZSvGOXKGceizRQIZSz7TGJ0pS3QLlVY/9hwxVh17W3re67je1RKYzFHivZ/t0tubU78Vkyb9WnHPENSBCzbckg=="],
"vite-plugin-vuetify": ["vite-plugin-vuetify@2.1.1", "", { "dependencies": { "@vuetify/loader-shared": "^2.1.0", "debug": "^4.3.3", "upath": "^2.0.1" }, "peerDependencies": { "vite": ">=5", "vue": "^3.0.0", "vuetify": "^3.0.0" } }, "sha512-Pb7bKhQH8qPMzURmEGq2aIqCJkruFNsyf1NcrrtnjsOIkqJPMcBbiP0oJoO8/uAmyB5W/1JTbbUEsyXdMM0QHQ=="],
@ -775,9 +790,11 @@
"vue-eslint-parser": ["vue-eslint-parser@10.1.2", "", { "dependencies": { "debug": "^4.4.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.6.0", "lodash": "^4.17.21", "semver": "^7.6.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0" } }, "sha512-1guOfYgNlD7JH2popr/bt5vc7Mzt6quRCnEbqLgpMHvoHEGV1oImzdqrLd+oMD76cHt8ilBP4cda9WA72TLFDQ=="],
"vue-i18n": ["vue-i18n@11.1.3", "", { "dependencies": { "@intlify/core-base": "11.1.3", "@intlify/shared": "11.1.3", "@vue/devtools-api": "^6.5.0" }, "peerDependencies": { "vue": "^3.0.0" } }, "sha512-Pcylh9z9S5+CJAqgbRZ3EKxFIBIrtY5YUppU722GIT65+Nukm0TCqiQegZnNLCZkXGthxe0cpqj0AoM51H+6Gw=="],
"vue-tsc": ["vue-tsc@2.2.8", "", { "dependencies": { "@volar/typescript": "~2.4.11", "@vue/language-core": "2.2.8" }, "peerDependencies": { "typescript": ">=5.0.0" }, "bin": { "vue-tsc": "./bin/vue-tsc.js" } }, "sha512-jBYKBNFADTN+L+MdesNX/TB3XuDSyaWynKMDgR+yCSln0GQ9Tfb7JS2lr46s2LiFUT1WsmfWsSvIElyxzOPqcQ=="],
"vuetify": ["vuetify@3.8.1", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-3qReKBBWIIdJJmwnFU1blVIKHDtnLfIP7kk0MwUrrfjYkWmsDpsymtDnsukkTCnlJ1WvhLr64eQFosr0RVbj9w=="],
"vuetify": ["vuetify@3.8.2", "", { "peerDependencies": { "typescript": ">=4.7", "vite-plugin-vuetify": ">=2.1.0", "vue": "^3.5.0", "webpack-plugin-vuetify": ">=3.1.0" }, "optionalPeers": ["typescript", "vite-plugin-vuetify", "webpack-plugin-vuetify"] }, "sha512-UJNFP4egmKJTQ3V3MKOq+7vIUKO7/Fko5G6yUsOW2Rm0VNBvAjgO6VY6EnK3DTqEKN6ugVXDEPw37NQSTGLZvw=="],
"webpack-virtual-modules": ["webpack-virtual-modules@0.6.2", "", {}, "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="],
@ -805,8 +822,30 @@
"@primeuix/forms/@primeuix/utils": ["@primeuix/utils@0.4.1", "", {}, "sha512-5+1NLfyna+gLRPeFTo+xlR0tfPVLuVdidbeahAMLkQga5Rw0LxyUBCyD2/Zv2JkV69o2T+hpEDyddl3VdnYoBw=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.9", "", { "dependencies": { "@emnapi/core": "^1.4.0", "@emnapi/runtime": "^1.4.0", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-OKRBiajrrxB9ATokgEQoG87Z25c67pCpYcCwmXYX8PBftC9pBfN18gnm/fh1wurSLEKIAt+QRFLFCQISrb66Jg=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tauri-apps/plugin-cli/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tauri-apps/plugin-deep-link/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tauri-apps/plugin-dialog/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tauri-apps/plugin-fs/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.3.0", "", {}, "sha512-33Z+0lX2wgZbx1SPFfqvzI6su63hCBkbzv+5NexeYjIx7WA9htdOKoRR7Dh3dJyltqS5/J8vQFyybiRoaL0hlA=="],
"@tauri-apps/plugin-shell/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="],
"@vue/compiler-sfc/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
@ -829,14 +868,34 @@
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"rollup/@types/estree": ["@types/estree@1.0.7", "", {}, "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ=="],
"sass-embedded/immutable": ["immutable@5.0.3", "", {}, "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw=="],
"unplugin-vue-components/unplugin": ["unplugin@1.16.1", "", { "dependencies": { "acorn": "^8.14.0", "webpack-virtual-modules": "^0.6.2" } }, "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w=="],
"vue-i18n/@vue/devtools-api": ["@vue/devtools-api@6.6.4", "", {}, "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g=="],
"@eslint/config-array/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@eslint/eslintrc/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"eslint-plugin-vue/vue-eslint-parser/eslint-scope": ["eslint-scope@7.2.2", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg=="],
@ -846,5 +905,17 @@
"eslint-plugin-vue/vue-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="],
"eslint/minimatch/brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/runtime/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@tybys/wasm-util/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime/@emnapi/core/@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
}
}

View File

@ -14,15 +14,15 @@
"@mdi/font": "7.4.47",
"@primevue/forms": "^4.3.3",
"@primevue/themes": "^4.3.3",
"@tailwindcss/vite": "^4.1.3",
"@tauri-apps/api": "^2.4.1",
"@tailwindcss/vite": "^4.1.4",
"@tauri-apps/api": "^2.5.0",
"@tauri-apps/plugin-cli": "^2.2.0",
"@tauri-apps/plugin-deep-link": "~2.2.1",
"@tauri-apps/plugin-dialog": "~2.2.1",
"@tauri-apps/plugin-fs": "^2.2.1",
"@tauri-apps/plugin-opener": "^2.2.6",
"@tauri-apps/plugin-shell": "~2.2.1",
"@tauri-apps/plugin-updater": "^2.7.0",
"@tauri-apps/plugin-updater": "^2.7.1",
"@trivago/prettier-plugin-sort-imports": "^5.2.2",
"@types/markdown-it": "^14.1.2",
"markdown-it": "^14.1.0",
@ -30,13 +30,14 @@
"primeicons": "^7.0.0",
"primevue": "^4.3.3",
"roboto-fontface": "^0.10.0",
"tailwindcss": "^4.1.3",
"tailwindcss": "^4.1.4",
"tailwindcss-primeui": "^0.4.0",
"vue": "^3.5.13",
"vuetify": "^3.8.1"
"vue-i18n": "^11.1.3",
"vuetify": "^3.8.2"
},
"devDependencies": {
"@tauri-apps/cli": "^2.4.1",
"@tauri-apps/cli": "^2.5.0",
"@tsconfig/node22": "^22.0.1",
"@types/node": "^22.14.1",
"@vitejs/plugin-vue": "^5.2.3",
@ -44,11 +45,11 @@
"@vue/tsconfig": "^0.5.1",
"npm-run-all2": "^7.0.2",
"sass": "1.77.8",
"sass-embedded": "^1.86.3",
"sass-embedded": "^1.87.0",
"typescript": "^5.8.3",
"unplugin-fonts": "^1.3.1",
"unplugin-vue-components": "^0.27.5",
"vite": "^6.2.6",
"vite": "^6.3.2",
"vite-plugin-vuetify": "^2.1.1",
"vue-tsc": "^2.2.8"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

BIN
public/no-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

370
rust/Cargo.lock generated
View File

@ -134,9 +134,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.97"
version = "1.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
[[package]]
name = "arbitrary"
@ -162,7 +162,7 @@ dependencies = [
"enumflags2",
"futures-channel",
"futures-util",
"rand 0.9.0",
"rand 0.9.1",
"raw-window-handle",
"serde",
"serde_repr",
@ -208,9 +208,9 @@ dependencies = [
[[package]]
name = "async-compression"
version = "0.4.22"
version = "0.4.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07"
dependencies = [
"flate2",
"futures-core",
@ -488,11 +488,11 @@ dependencies = [
[[package]]
name = "block2"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d59b4c170e16f0405a2e95aff44432a0d41aa97675f3d52623effe95792a037"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [
"objc2 0.6.0",
"objc2 0.6.1",
]
[[package]]
@ -521,9 +521,9 @@ dependencies = [
[[package]]
name = "brotli-decompressor"
version = "4.0.2"
version = "4.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74fa05ad7d803d413eb8380983b092cbbaf9a85f151b871360e7b00cd7060b37"
checksum = "a334ef7c9e23abf0ce748e8cd309037da93e606ad52eb372e4ce327a0dcfbdfd"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
@ -644,9 +644,9 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.18"
version = "1.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525046617d8376e3db1deffb079e91cef90a89fc3ca5c185bbf8c9ecdd15cd5c"
checksum = "8e3a13707ac958681c13b39b458c073d0d9bc8a22cb1b2f4c8e55eb72c13f362"
dependencies = [
"jobserver",
"libc",
@ -732,18 +732,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.35"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944"
checksum = "eccb054f56cbd38340b380d4a8e69ef1f02f1af43db2f0cc817a4774d80ae071"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.35"
version = "4.5.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9"
checksum = "efd9466fac8543255d3b1fcad4762c5e116ffe808c8a3043d4263cd4fd4862a2"
dependencies = [
"anstream",
"anstyle",
@ -1199,9 +1199,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a0d569e003ff27784e0e14e4a594048698e0c0f0b66cabcb51511be55a7caa0"
dependencies = [
"bitflags 2.9.0",
"block2 0.6.0",
"block2 0.6.1",
"libc",
"objc2 0.6.0",
"objc2 0.6.1",
]
[[package]]
name = "dispatch2"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.1",
]
[[package]]
@ -1995,9 +2005,9 @@ dependencies = [
[[package]]
name = "h2"
version = "0.4.8"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2"
checksum = "75249d144030531f8dee69fe9cea04d3edf809a017ae445e2abdff6629e86633"
dependencies = [
"atomic-waker",
"bytes",
@ -2536,9 +2546,9 @@ dependencies = [
[[package]]
name = "jiff"
version = "0.2.6"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f33145a5cbea837164362c7bd596106eb7c5198f97d1ba6f6ebb3223952e488"
checksum = "5a064218214dc6a10fbae5ec5fa888d80c45d611aba169222fc272072bf7aef6"
dependencies = [
"jiff-static",
"log",
@ -2549,9 +2559,9 @@ dependencies = [
[[package]]
name = "jiff-static"
version = "0.2.6"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43ce13c40ec6956157a3635d97a1ee2df323b263f09ea14165131289cb0f5c19"
checksum = "199b7932d97e325aff3a7030e141eafe7f2c6268e1d1b24859b753a627f45254"
dependencies = [
"proc-macro2",
"quote",
@ -2697,9 +2707,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.171"
version = "0.2.172"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
[[package]]
name = "libloading"
@ -2770,12 +2780,6 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.27"
@ -2908,10 +2912,10 @@ dependencies = [
"dpi",
"gtk",
"keyboard-types",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"once_cell",
"png",
"serde",
@ -3056,9 +3060,9 @@ dependencies = [
[[package]]
name = "objc2"
version = "0.6.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3531f65190d9cff863b77a99857e74c314dd16bf56c538c4b57c7cbc3f3a6e59"
checksum = "88c6597e14493ab2e44ce58f2fdecf095a51f12ca57bec060a11c57332520551"
dependencies = [
"objc2-encode",
"objc2-exception-helper",
@ -3066,75 +3070,77 @@ dependencies = [
[[package]]
name = "objc2-app-kit"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5906f93257178e2f7ae069efb89fbd6ee94f0592740b5f8a1512ca498814d0fb"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.9.0",
"block2 0.6.0",
"block2 0.6.1",
"libc",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-cloud-kit",
"objc2-core-data",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-core-image",
"objc2-foundation 0.3.0",
"objc2-quartz-core 0.3.0",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
]
[[package]]
name = "objc2-cloud-kit"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c1948a9be5f469deadbd6bcb86ad7ff9e47b4f632380139722f7d9840c0d42c"
checksum = "17614fdcd9b411e6ff1117dfb1d0150f908ba83a7df81b1f118005fe0a8ea15d"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-data"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f860f8e841f6d32f754836f51e6bc7777cd7e7053cf18528233f6811d3eceb4"
checksum = "291fbbf7d29287518e8686417cf7239c74700fd4b607623140a7d4a3c834329d"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daeaf60f25471d26948a1c2f840e3f7d86f4109e3af4e8e4b5cd70c39690d925"
checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"dispatch2 0.3.0",
"objc2 0.6.1",
]
[[package]]
name = "objc2-core-graphics"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dca602628b65356b6513290a21a6405b4d4027b8b250f0b98dddbb28b7de02"
checksum = "989c6c68c13021b5c2d6b71456ebb0f9dc78d752e86a98da7c716f4f9470f5a4"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"dispatch2 0.3.0",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-io-surface",
]
[[package]]
name = "objc2-core-image"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ffa6bea72bf42c78b0b34e89c0bafac877d5f80bf91e159a5d96ea7f693ca56"
checksum = "79b3dc0cc4386b6ccf21c157591b34a7f44c8e75b064f85502901ab2188c007e"
dependencies = [
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
]
[[package]]
@ -3166,25 +3172,25 @@ dependencies = [
[[package]]
name = "objc2-foundation"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a21c6c9014b82c39515db5b396f91645182611c97d24637cf56ac01e5f8d998"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.0",
"block2 0.6.0",
"block2 0.6.1",
"libc",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-core-foundation",
]
[[package]]
name = "objc2-io-surface"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "161a8b87e32610086e1a7a9e9ec39f84459db7b3a0881c1f16ca5a2605581c19"
checksum = "7282e9ac92529fa3457ce90ebb15f4ecbc383e8338060960760fa2cf75420c3c"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-core-foundation",
]
@ -3202,14 +3208,14 @@ dependencies = [
[[package]]
name = "objc2-osa-kit"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1ac59da3ceebc4a82179b35dc550431ad9458f9cc326e053f49ba371ce76c5a"
checksum = "26bb88504b5a050dbba515d2414607bf5e57dd56b107bc5f0351197a3e7bdc5d"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
]
[[package]]
@ -3227,39 +3233,39 @@ dependencies = [
[[package]]
name = "objc2-quartz-core"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fb3794501bb1bee12f08dcad8c61f2a5875791ad1c6f47faa71a0f033f20071"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-ui-kit"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "777a571be14a42a3990d4ebedaeb8b54cd17377ec21b92e8200ac03797b3bee1"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.9.0",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
]
[[package]]
name = "objc2-web-kit"
version = "0.3.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b717127e4014b0f9f3e8bba3d3f2acec81f1bde01f656823036e823ed2c94dce"
checksum = "91672909de8b1ce1c2252e95bbee8c1649c9ad9d14b9248b3d7b4c47903c47ad"
dependencies = [
"bitflags 2.9.0",
"block2 0.6.0",
"objc2 0.6.0",
"block2 0.6.1",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
]
[[package]]
@ -3375,8 +3381,8 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
dependencies = [
"objc2 0.6.0",
"objc2-foundation 0.3.0",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
"objc2-osa-kit",
"serde",
"serde_json",
@ -3810,9 +3816,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
[[package]]
name = "proc-macro2"
version = "1.0.94"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
@ -3854,7 +3860,7 @@ checksum = "b820744eb4dc9b57a3398183639c511b5a26d2ed702cedd3febaa1393caa22cc"
dependencies = [
"bytes",
"getrandom 0.3.2",
"rand 0.9.0",
"rand 0.9.1",
"ring",
"rustc-hash",
"rustls",
@ -3922,13 +3928,12 @@ dependencies = [
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha 0.9.0",
"rand_core 0.9.3",
"zerocopy",
]
[[package]]
@ -4119,17 +4124,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c844748fdc82aae252ee4594a89b6e7ebef1063de7951545564cbc4e57075d"
dependencies = [
"ashpd",
"block2 0.6.0",
"dispatch2",
"block2 0.6.1",
"dispatch2 0.2.0",
"glib-sys",
"gobject-sys",
"gtk-sys",
"js-sys",
"log",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -4607,9 +4612,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
version = "1.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
dependencies = [
"libc",
]
@ -4918,9 +4923,9 @@ dependencies = [
[[package]]
name = "tao"
version = "0.32.8"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63c8b1020610b9138dd7b1e06cf259ae91aa05c30f3bd0d6b42a03997b92dec1"
checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
dependencies = [
"bitflags 2.9.0",
"core-foundation 0.10.0",
@ -4939,9 +4944,9 @@ dependencies = [
"ndk",
"ndk-context",
"ndk-sys",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"once_cell",
"parking_lot",
"raw-window-handle",
@ -4949,8 +4954,8 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows-core 0.60.1",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-version",
"x11-dl",
]
@ -4985,9 +4990,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tauri"
version = "2.4.1"
version = "2.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d08db1ff9e011e04014e737ec022610d756c0eae0b3b3a9037bccaf3003173a"
checksum = "e7b0bc1aec81bda6bc455ea98fcaed26b3c98c1648c627ad6ff1c704e8bf8cbc"
dependencies = [
"anyhow",
"bytes",
@ -5006,9 +5011,10 @@ dependencies = [
"log",
"mime",
"muda",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"percent-encoding",
"plist",
"raw-window-handle",
@ -5031,14 +5037,14 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.61.1",
]
[[package]]
name = "tauri-build"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fd20e4661c2cce65343319e6e8da256958f5af958cafc47c0d0af66a55dcd17"
checksum = "d7a0350f0df1db385ca5c02888a83e0e66655c245b7443db8b78a70da7d7f8fc"
dependencies = [
"anyhow",
"cargo_toml",
@ -5058,9 +5064,9 @@ dependencies = [
[[package]]
name = "tauri-codegen"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458258b19032450ccf975840116ecf013e539eadbb74420bd890e8c56ab2b1a4"
checksum = "f93f035551bf7b11b3f51ad9bc231ebbe5e085565527991c16cf326aa38cdf47"
dependencies = [
"base64 0.22.1",
"brotli",
@ -5085,9 +5091,9 @@ dependencies = [
[[package]]
name = "tauri-macros"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d402813d3b9c773a0fa58697c457c771f10e735498fdcb7b343264d18e5a601f"
checksum = "8db4df25e2d9d45de0c4c910da61cd5500190da14ae4830749fee3466dddd112"
dependencies = [
"heck 0.5.0",
"proc-macro2",
@ -5099,9 +5105,9 @@ dependencies = [
[[package]]
name = "tauri-plugin"
version = "2.1.1"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4190775d6ff73fe66d9af44c012739a2659720efd9c0e1e56a918678038699d"
checksum = "37a5ebe6a610d1b78a94650896e6f7c9796323f408800cef436e0fa0539de601"
dependencies = [
"anyhow",
"glob",
@ -5120,7 +5126,7 @@ version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5458ae16eac81bdbe8d9da2a9f3e01e8cdedbc381cc1727c01127542c8a61c5"
dependencies = [
"clap 4.5.35",
"clap 4.5.37",
"log",
"serde",
"serde_json",
@ -5199,7 +5205,7 @@ dependencies = [
"dunce",
"glob",
"objc2-app-kit",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"open",
"schemars",
"serde",
@ -5208,7 +5214,7 @@ dependencies = [
"tauri-plugin",
"thiserror 2.0.12",
"url",
"windows",
"windows 0.60.0",
"zbus",
]
@ -5251,9 +5257,9 @@ dependencies = [
[[package]]
name = "tauri-plugin-updater"
version = "2.7.0"
version = "2.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82da763248e635d60ee4aed56c862290e523acc838d83097171f9a544708387"
checksum = "73f05c38afd77a4b8fd98e8fb6f1cdbb5fbb8a46ba181eb2758b05321e3c6209"
dependencies = [
"base64 0.22.1",
"dirs",
@ -5283,37 +5289,39 @@ dependencies = [
[[package]]
name = "tauri-runtime"
version = "2.5.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00ada7ac2f9276f09b8c3afffd3215fd5d9bff23c22df8a7c70e7ef67cacd532"
checksum = "00f004905d549854069e6774533d742b03cacfd6f03deb08940a8677586cbe39"
dependencies = [
"cookie",
"dpi",
"gtk",
"http",
"jni",
"objc2 0.6.1",
"objc2-ui-kit",
"raw-window-handle",
"serde",
"serde_json",
"tauri-utils",
"thiserror 2.0.12",
"url",
"windows",
"windows 0.61.1",
]
[[package]]
name = "tauri-runtime-wry"
version = "2.5.1"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf2e5842c57e154af43a20a49c7efee0ce2578c20b4c2bdf266852b422d2e421"
checksum = "f85d056f4d4b014fe874814034f3416d57114b617a493a4fe552580851a3f3a2"
dependencies = [
"gtk",
"http",
"jni",
"log",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"once_cell",
"percent-encoding",
"raw-window-handle",
@ -5324,15 +5332,15 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.61.1",
"wry",
]
[[package]]
name = "tauri-utils"
version = "2.3.1"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f037e66c7638cc0a2213f61566932b9a06882b8346486579c90e4b019bac447"
checksum = "b2900399c239a471bcff7f15c4399eb1a8c4fe511ba2853e07c996d771a5e0a4"
dependencies = [
"anyhow",
"brotli",
@ -5731,19 +5739,19 @@ dependencies = [
[[package]]
name = "tray-icon"
version = "0.20.0"
version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d433764348e7084bad2c5ea22c96c71b61b17afe3a11645710f533bd72b6a2b5"
checksum = "9f7eee98ec5c90daf179d55c20a49d8c0d043054ce7c26336c09a24d31f14fa0"
dependencies = [
"crossbeam-channel",
"dirs",
"libappindicator",
"muda",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-core-graphics",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"once_cell",
"png",
"serde",
@ -6185,15 +6193,15 @@ dependencies = [
[[package]]
name = "webview2-com"
version = "0.36.0"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0d606f600e5272b514dbb66539dd068211cc20155be8d3958201b4b5bd79ed3"
checksum = "b542b5cfbd9618c46c2784e4d41ba218c336ac70d44c55e47b251033e7d85601"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows-core 0.60.1",
"windows-implement 0.59.0",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-implement 0.60.0",
"windows-interface",
]
@ -6210,13 +6218,13 @@ dependencies = [
[[package]]
name = "webview2-com-sys"
version = "0.36.0"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfb27fccd3c27f68e9a6af1bcf48c2d82534b8675b83608a4d81446d095a17ac"
checksum = "8ae2d11c4a686e4409659d7891791254cf9286d3cfe0eef54df1523533d22295"
dependencies = [
"thiserror 2.0.12",
"windows",
"windows-core 0.60.1",
"windows 0.61.1",
"windows-core 0.61.0",
]
[[package]]
@ -6256,10 +6264,10 @@ version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9bec5a31f3f9362f2258fd0e9c9dd61a9ca432e7306cc78c444258f0dce9a9c"
dependencies = [
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"raw-window-handle",
"windows-sys 0.59.0",
"windows-version",
@ -6271,11 +6279,24 @@ version = "0.60.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddf874e74c7a99773e62b1c671427abf01a425e77c3d3fb9fb1e4883ea934529"
dependencies = [
"windows-collections",
"windows-collections 0.1.1",
"windows-core 0.60.1",
"windows-future",
"windows-future 0.1.1",
"windows-link",
"windows-numerics",
"windows-numerics 0.1.1",
]
[[package]]
name = "windows"
version = "0.61.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
dependencies = [
"windows-collections 0.2.0",
"windows-core 0.61.0",
"windows-future 0.2.0",
"windows-link",
"windows-numerics 0.2.0",
]
[[package]]
@ -6287,6 +6308,15 @@ dependencies = [
"windows-core 0.60.1",
]
[[package]]
name = "windows-collections"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
dependencies = [
"windows-core 0.61.0",
]
[[package]]
name = "windows-core"
version = "0.60.1"
@ -6323,6 +6353,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-future"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32"
dependencies = [
"windows-core 0.61.0",
"windows-link",
]
[[package]]
name = "windows-implement"
version = "0.59.0"
@ -6372,6 +6412,16 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-numerics"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
dependencies = [
"windows-core 0.61.0",
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.4.0"
@ -6771,12 +6821,12 @@ checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wry"
version = "0.50.5"
version = "0.51.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b19b78efae8b853c6c817e8752fc1dbf9cab8a8ffe9c30f399bd750ccf0f0730"
checksum = "c886a0a9d2a94fd90cfa1d929629b79cfefb1546e2c7430c63a47f0664c0e4e2"
dependencies = [
"base64 0.22.1",
"block2 0.6.0",
"block2 0.6.1",
"cookie",
"crossbeam-channel",
"dpi",
@ -6790,10 +6840,10 @@ dependencies = [
"kuchikiki",
"libc",
"ndk",
"objc2 0.6.0",
"objc2 0.6.1",
"objc2-app-kit",
"objc2-core-foundation",
"objc2-foundation 0.3.0",
"objc2-foundation 0.3.1",
"objc2-ui-kit",
"objc2-web-kit",
"once_cell",
@ -6807,8 +6857,8 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows-core 0.60.1",
"windows 0.61.1",
"windows-core 0.61.0",
"windows-version",
"x11-dl",
]
@ -7075,15 +7125,13 @@ dependencies = [
[[package]]
name = "zopfli"
version = "0.8.1"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]

View File

@ -1,7 +1,8 @@
use std::hash::{DefaultHasher, Hash, Hasher};
use std::path::Path;
use std::time::SystemTime;
use crate::model::config::GlobalConfig;
use crate::model::patch::PatchFileVec;
use crate::model::patch::{PatchFileVec, PatchList};
use crate::pkg::{Feature, Status};
use crate::profiles::types::Profile;
use crate::{model::misc::Game, pkg::PkgKey};
@ -165,4 +166,22 @@ impl AppData {
panic!("unable to initialize the logger? {:?}", e);
}
}
pub fn patches_enabled(&self, game_target: impl AsRef<Path>, amd_target: impl AsRef<Path>) -> Result<Vec<&PatchList>> {
let ch1 = sha256::try_digest(game_target.as_ref())?;
let ch2 = sha256::try_digest(amd_target.as_ref())?;
let mut res = Vec::new();
for pfile in &self.patch_vec.0 {
for plist in &pfile.0 {
let this_hash = plist.sha256.to_ascii_lowercase();
log::debug!("checking {}", this_hash);
if this_hash == ch1 || this_hash == ch2 {
log::debug!("enabling {this_hash}");
res.push(plist);
}
}
}
Ok(res)
}
}

View File

@ -1,11 +1,12 @@
use ini::Ini;
use log;
use std::collections::{BTreeMap, HashMap};
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::PathBuf;
use tokio::sync::Mutex;
use tokio::fs;
use tauri::{AppHandle, Manager, State};
use crate::model::config::GlobalConfigField;
use crate::model::local::PackageManifest;
use crate::model::misc::Game;
use crate::model::patch::Patch;
use crate::modules::package::prepare_dlls;
@ -65,13 +66,20 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
let mut amd_dlls = Vec::new();
if let Some(p) = &appd.profile {
hash = appd.sum_packages(p);
(game_dlls, amd_dlls) = prepare_dlls(p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())?
(game_dlls, amd_dlls) = prepare_dlls(p.meta.game, p.mod_pkgs(), &appd.pkgs).map_err(|e| e.to_string())?
}
if let Some(p) = &appd.profile {
log::debug!("{}", hash);
let patches_enabled = appd.patches_enabled(
&p.data.sgt.target,
&p.data.sgt.target.parent().unwrap().join("amdaemon.exe")
).map_err(|e| e.to_string())?;
#[cfg(target_os = "windows")]
let info = p.prepare_display()
.map_err(|e| e.to_string())?;
let lineup_res = p.line_up(hash, refresh, &appd.patch_vec).await
let lineup_res = p.line_up(hash, refresh, patches_enabled).await
.map_err(|e| e.to_string());
#[cfg(target_os = "windows")]
@ -159,6 +167,65 @@ pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key:
.map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn create_package(
name: String,
description: String,
website: String,
r#type: String,
games: Vec<Game>
) -> Result<(), String> {
log::debug!("invoke: create_package");
let dir = util::pkg_dir_of("local", &name);
if dir.exists() {
return Err("Package already exists".to_owned());
}
let mut installers = Vec::new();
if r#type == "segatools" {
let mut map = BTreeMap::new();
map.insert(
"identifier".to_owned(),
serde_json::Value::String("segatools".to_owned())
);
installers.push(map);
} else if r#type == "native" {
let mut map = BTreeMap::new();
map.insert(
"identifier".to_owned(),
serde_json::Value::String("native_mod".to_owned())
);
map.insert(
"dll-game".to_owned(),
serde_json::Value::String("some.dll".to_owned())
);
map.insert(
"dll-amdaemon".to_owned(),
serde_json::Value::String("another.dll".to_owned())
);
installers.push(map);
}
let manifest = PackageManifest {
name,
version_number: "1.0.0".to_owned(),
description,
website_url: website,
dependencies: BTreeSet::new(),
installers,
games: Some(games)
};
std::fs::create_dir(&dir).map_err(|e| e.to_string())?;
let json = serde_json::to_string_pretty(&manifest).map_err(|e| e.to_string())?;
std::fs::write(dir.join("manifest.json"), json).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
log::debug!("invoke: reload_all_packages");
@ -184,12 +251,16 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
#[tauri::command]
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> {
log::debug!("invoke: get_game_packages {game}");
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Option<Game>) -> Result<Vec<PkgKey>, ()> {
log::debug!("invoke: get_game_packages {game:?}");
let appd = state.lock().await;
Ok(appd.pkgs.get_game_list(game))
if let Some(game) = game {
Ok(appd.pkgs.get_game_list(game))
} else {
Ok(Vec::new())
}
}
#[tauri::command]
@ -398,10 +469,61 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
}
#[tauri::command]
pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
pub async fn create_shortcut(_app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
log::debug!("invoke: create_shortcut({:?})", profile_meta);
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
#[cfg(target_os = "windows")]
return util::create_shortcut(_app, &profile_meta).map_err(|e| e.to_string());
#[cfg(not(target_os = "windows"))]
return Err("unsupported".to_owned());
}
#[tauri::command]
pub async fn export_profile(state: State<'_, Mutex<AppData>>, export_keychip: bool, files: Vec<String>) -> Result<(), String> {
log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len());
let appd = state.lock().await;
match &appd.profile {
Some(p) => {
p.export(export_keychip, files)
.map_err(|e| e.to_string())?;
}
None => {
let err = "export_profile: no profile".to_owned();
log::error!("{}", err);
return Err(err);
}
}
Ok(())
}
#[tauri::command]
pub async fn import_profile(path: PathBuf) -> Result<(), String> {
log::debug!("invoke: import_profile({:?})", path);
Profile::import(path).map_err(|e| e.to_string())
}
#[tauri::command]
pub async fn clear_cache(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
log::debug!("invoke: clear_cache");
let appd = state.lock().await;
if let Some(p) = &appd.profile {
let dir = p.data_dir().join("mu3-mods-cache");
let path = dir.join("data_cache.bin");
if path.exists() {
std::fs::remove_file(path).map_err(|e| e.to_string())?;
}
let path = dir.join("data_fumen_analysis_cache.bin");
if path.exists() {
std::fs::remove_file(path).map_err(|e| e.to_string())?;
}
}
Ok(())
}
#[tauri::command]
@ -409,7 +531,7 @@ pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
log::debug!("invoke: list_platform_capabilities");
#[cfg(target_os = "windows")]
return Ok(vec!["display".to_owned()]);
return Ok(vec!["display".to_owned(), "shortcut".to_owned(), "chunithm".to_owned()]);
#[cfg(target_os = "linux")]
return Ok(vec!["wine".to_owned()]);

View File

@ -1,5 +1,4 @@
use std::{collections::HashSet, path::PathBuf};
use futures::Stream;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tokio::fs::File;
@ -15,7 +14,7 @@ pub struct DownloadHandler {
#[derive(Serialize, Deserialize, Clone)]
pub struct DownloadTick {
pkg_key: PkgKey,
ratio: f32
ratio: f32,
}
impl DownloadHandler {
@ -50,14 +49,15 @@ impl DownloadHandler {
let mut cache_file_w = File::create(&zip_path_part).await?;
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
let first_hint = byte_stream.size_hint().0 as f32;
let mut total_bytes = 0;
log::info!("downloading: {}", rmt.download_url);
while let Some(item) = byte_stream.next().await {
let i = item?;
app.emit("download-tick", DownloadTick {
total_bytes += i.len();
_ = app.emit("download-progress", DownloadTick {
pkg_key: pkg_key.clone(),
ratio: 1.0f32 - (byte_stream.size_hint().0 as f32) / first_hint
ratio: (total_bytes as f32) / (rmt.file_size as f32),
})?;
cache_file_w.write_all(&mut i.as_ref()).await?;
}

View File

@ -102,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
});
app.listen("download-end", closure!(clone apph, |ev| {
log::debug!("download-end triggered: {}", ev.payload());
let raw = ev.payload();
log::debug!("download-end triggered: {}", raw);
let key = PkgKey(raw[1..raw.len()-1].to_owned());
let apph = apph.clone();
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
log::debug!("download-end install {:?}", appd.pkgs.install_package(&key, true, false).await);
let res = appd.pkgs.install_package(&key, true, false).await;
log::debug!("download-end install {:?}", res);
});
}));
@ -126,19 +127,21 @@ pub async fn run(_args: Vec<String>) {
}));
app.listen("install-end-prelude", closure!(clone apph, |ev| {
log::debug!("install-end-prelude triggered: {}", ev.payload());
let payload = serde_json::from_str::<Payload>(ev.payload());
log::debug!("install-end-prelude triggered: {:?}", payload);
let apph = apph.clone();
if let Ok(payload) = payload {
tauri::async_runtime::spawn(async move {
let mutex = apph.state::<Mutex<AppData>>();
let mut appd = mutex.lock().await;
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
log::debug!(
"install-end-prelude toggle {:?}",
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
res
);
use tauri::Emitter;
log::debug!("install-end {:?}", apph.emit("install-end", payload));
let res = apph.emit("install-end", payload);
log::debug!("install-end {:?}", res);
});
} else {
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
@ -190,6 +193,7 @@ pub async fn run(_args: Vec<String>) {
cmd::install_package,
cmd::delete_package,
cmd::toggle_package,
cmd::create_package,
cmd::list_profiles,
cmd::init_profile,
@ -202,6 +206,9 @@ pub async fn run(_args: Vec<String>) {
cmd::save_current_profile,
cmd::load_segatools_ini,
cmd::create_shortcut,
cmd::export_profile,
cmd::import_profile,
cmd::clear_cache,
cmd::get_global_config,
cmd::set_global_config,
@ -232,7 +239,8 @@ pub async fn run(_args: Vec<String>) {
let mutex = app.state::<Mutex<AppData>>();
let appd = mutex.lock().await;
if let Some(p) = &appd.profile {
log::debug!("save: {:?}", p.save());
let res = p.save();
log::debug!("save: {:?}", res);
app.exit(0);
}
});
@ -329,9 +337,9 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
fn open_window(apph: AppHandle) -> anyhow::Result<()> {
let config = apph.config().clone();
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
.inner_size(900f64, 480f64)
.min_inner_size(900f64, 480f64)
.title(format!("STARTLINER {} Beta", config.version.unwrap_or_default()))
.inner_size(900f64, 600f64)
.min_inner_size(900f64, 600f64)
.build()?;
Ok(())

View File

@ -1,20 +1,24 @@
use std::collections::{BTreeMap, BTreeSet};
use serde::{Deserialize, Serialize};
use crate::pkg::{Status, PkgKey, PkgKeyVersion};
use crate::pkg::{PkgKey, PkgKeyVersion};
use super::misc::Game;
// manifest.json
#[derive(Deserialize)]
#[derive(Serialize, Deserialize)]
pub struct PackageManifest {
pub name: String,
pub version_number: String,
pub website_url: String,
pub description: String,
pub dependencies: BTreeSet<PkgKeyVersion>,
#[serde(default)]
pub installers: Vec<BTreeMap<String, serde_json::Value>>
pub installers: Vec<BTreeMap<String, serde_json::Value>>,
#[serde(default)]
pub games: Option<Vec<Game>>,
}
pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
@ -22,6 +26,5 @@ pub type PackageList = BTreeMap<PkgKey, PackageListEntry>;
#[derive(Serialize, Deserialize, Clone)]
pub struct PackageListEntry {
pub version: String,
pub status: Status,
pub games: Vec<Game>,
}

View File

@ -71,10 +71,17 @@ impl Game {
pub fn has_module(&self, module: ProfileModule) -> bool {
match self {
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard}),
Game::Ongeki => make_bitflags!(ProfileModule::{Segatools | Display | Network | BepInEx | Mu3Ini | Keyboard | Mempatcher}),
Game::Chunithm => make_bitflags!(ProfileModule::{Segatools | Display | Network | Keyboard | Mempatcher}),
}.contains(module)
}
pub fn bitness(&self) -> i32 {
match self {
Game::Ongeki => 64,
Game::Chunithm => 32,
}
}
}
impl std::fmt::Display for Game {

View File

@ -42,6 +42,7 @@ pub struct Patch {
pub enum PatchData {
Normal(NormalPatch),
Number(NumberPatch),
Hex(HexPatch),
}
#[derive(Deserialize, Serialize, Clone, Debug)]
@ -65,6 +66,12 @@ pub struct NumberPatch {
pub max: i32
}
#[derive(Deserialize, Serialize, Clone, Debug)]
pub struct HexPatch {
pub offset: u64,
pub off: Vec<u8>,
}
impl Serialize for Patch {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where S: Serializer {
@ -83,6 +90,11 @@ impl Serialize for Patch {
state.serialize_field("size", &patch.size)?;
state.serialize_field("min", &patch.min)?;
state.serialize_field("max", &patch.max)?;
},
PatchData::Hex(patch) => {
state.serialize_field("type", "hex")?;
state.serialize_field("offset", &patch.offset)?;
state.serialize_field("off", &patch.off)?;
}
}
state.end()
@ -114,6 +126,23 @@ impl<'de> serde::Deserialize<'de> for Patch {
.ok_or_else(|| de::Error::missing_field("max"))?
).map_err(|_| de::Error::missing_field("max"))?
}),
Some("hex") => {
let mut off_list = Vec::new();
for off in value.get("off").and_then(Value::as_array).unwrap() {
off_list.push(u8::try_from(
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
).map_err(|_| de::Error::missing_field("off"))?);
}
// for off in value.get("off").and_then(Value::as_str).unwrap().bytes() {
// off_list.push(off);
// }
PatchData::Hex(HexPatch {
offset: value.get("offset")
.and_then(Value::as_u64)
.ok_or_else(|| de::Error::missing_field("offset"))?,
off: off_list
})
},
None => {
let mut patches = vec![];
for patch in value.get("patches").and_then(Value::as_array).unwrap() {

View File

@ -108,7 +108,10 @@ impl Display {
Game::Ongeki => 60,
},
borderless_fullscreen: true,
#[cfg(target_os = "windows")]
dont_switch_primary: false,
#[cfg(not(target_os = "windows"))]
dont_switch_primary: true,
monitor_index_override: None,
}
}
@ -141,7 +144,7 @@ pub struct BepInEx {
pub console: bool,
}
#[derive(Deserialize, Serialize, Clone)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct Wine {
pub runtime: PathBuf,
@ -166,11 +169,26 @@ pub enum Mu3Audio {
Excl2Ch,
}
#[derive(Deserialize, Serialize, Clone, Debug, Default)]
#[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)]
pub struct Mu3Ini {
pub audio: Option<Mu3Audio>,
pub sample_rate: i32,
pub blacklist: Option<(i32, i32)>,
pub gp: i32,
pub enable_bonus_tracks: bool,
}
impl Default for Mu3Ini {
fn default() -> Self {
Self {
audio: Some(Mu3Audio::Shared),
sample_rate: 48_000,
blacklist: Some((10000, 19999)),
gp: 999,
enable_bonus_tracks: true
}
}
}
#[derive(Deserialize, Serialize, Clone, Debug)]

View File

@ -22,4 +22,5 @@ pub struct V1Version {
pub icon: String,
pub dependencies: BTreeSet<PkgKeyVersion>,
pub download_url: String,
pub file_size: i64,
}

View File

@ -0,0 +1,8 @@
use ini::Ini;
use crate::model::{misc::Game, profile::Display};
impl Display {
pub fn line_up(&self, _game: Game, _ini: &mut Ini) {
// nop
}
}

View File

@ -6,14 +6,14 @@ use tauri::{AppHandle, Listener};
#[derive(Clone)]
pub struct DisplayInfo {
pub primary: String,
pub primary: Option<String>,
pub set: Option<DisplaySet>,
}
impl Default for DisplayInfo {
fn default() -> Self {
DisplayInfo {
primary: "default".to_owned(),
primary: None,
set: query_displays().ok(),
}
}
@ -60,7 +60,7 @@ impl Display {
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
let res = DisplayInfo {
primary: primary.name().to_owned(),
primary: if self.dont_switch_primary { None } else { Some(primary.name().to_owned()) },
set: Some(display_set.clone()),
};
@ -132,12 +132,14 @@ impl Display {
let display_set = info.set.as_ref()
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
let primary = display_set
.displays()
.find(|display| display.name() == info.primary)
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
if let Some(info_primary) = &info.primary {
let primary = display_set
.displays()
.find(|display| display.name() == info_primary)
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
primary.set_primary()?;
primary.set_primary()?;
}
display_set.apply()?;
displayz::refresh()?;

View File

@ -117,12 +117,16 @@ impl Keyboard {
}
}
Keyboard::Chunithm(kb) => {
let mut enabled_ir = false;
if kb.enabled {
for (i, cell) in kb.cell.iter().enumerate() {
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
}
for (i, ir) in kb.ir.iter().enumerate() {
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string());
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), (*ir).to_string());
if i > 0 && *ir != 0 {
enabled_ir = true;
}
}
ini.with_section(Some("io3"))
.set("test", kb.test.to_string())
@ -140,8 +144,13 @@ impl Keyboard {
.set("service", "0")
.set("coin", "0");
}
ini.with_section(Some("io3"))
.set("ir", "0");
if enabled_ir {
ini.with_section(Some("io3"))
.set("ir", "0");
} else {
ini.with_section(Some("io3"))
.set("ir", kb.ir[0].to_string());
}
}
}

View File

@ -1,25 +1,23 @@
use std::path::Path;
use anyhow::Result;
use crate::model::patch::{Patch, PatchData, PatchFileVec, PatchSelection, PatchSelectionData};
use crate::model::patch::{Patch, PatchData, PatchList, PatchSelection, PatchSelectionData};
impl PatchSelection {
pub async fn render_to_file(
&self,
filename: &str,
patches: &PatchFileVec,
patch_lists: &Vec<&PatchList>,
path: impl AsRef<Path>
) -> Result<()> {
let mut res = "".to_owned();
for file in &patches.0 {
for list in &file.0 {
if list.filename != filename {
continue;
}
for patch in &list.patches {
if let Some(selection) = self.0.get(&patch.id) {
res += &Self::render(filename, patch, selection);
}
for list in patch_lists {
if list.filename != filename {
continue;
}
for patch in &list.patches {
if let Some(selection) = self.0.get(&patch.id) {
res += &Self::render(filename, patch, selection);
}
}
}
@ -52,6 +50,21 @@ impl PatchSelection {
} else {
log::error!("invalid number patch {:?}", patch);
}
},
PatchData::Hex(data) => {
if let PatchSelectionData::Hex(val) = sel {
res += &format!("{} F+{:X} ", filename, data.offset);
for byte in val {
res += &format!("{:02X}", byte);
}
res += " ";
for byte in &data.off {
res += &format!("{:02X}", byte);
}
} else {
log::error!("invalid number patch {:?}", patch);
}
}
}
format!("{}\n", res)

View File

@ -7,4 +7,7 @@ pub mod keyboard;
pub mod mempatcher;
#[cfg(target_os = "windows")]
pub mod display_windows;
pub mod display_windows;
#[cfg(target_os = "linux")]
pub mod display_linux;

View File

@ -1,11 +1,11 @@
use std::path::Path;
use anyhow::Result;
use anyhow::{anyhow, Result};
use ini::Ini;
use crate::model::profile::{Mu3Audio, Mu3Ini};
impl Mu3Ini {
pub fn line_up(&self, game_path: impl AsRef<Path>) -> Result<()> {
let file = game_path.as_ref().join("mu3.ini");
pub fn line_up(&self, data_dir: impl AsRef<Path>, cfg_dir: impl AsRef<Path>) -> Result<()> {
let file = cfg_dir.as_ref().join("mu3.ini");
if !file.exists() {
std::fs::write(&file, "")?;
@ -20,9 +20,26 @@ impl Mu3Ini {
Mu3Audio::Excl2Ch => "2",
};
ini.with_section(Some("Sound")).set("WasapiExclusive", value);
ini.with_section(Some("Sound"))
.set("WasapiExclusive", value)
.set("SampleRate", self.sample_rate.to_string());
}
if let Some(blacklist) = self.blacklist {
ini.with_section(Some("Extra"))
.set("BlacklistMin", blacklist.0.to_string())
.set("BlacklistMax", blacklist.1.to_string());
}
let cache_path = data_dir.as_ref().join("mu3-mods-cache");
let cache_path = cache_path.to_str()
.ok_or_else(|| anyhow!("Invalid cache path"))?;
ini.with_section(Some("Extra"))
.set("GP", self.gp.to_string())
.set("CacheDir", cache_path)
.set("UnlockBonusTracks", crate::util::bool_to_01(self.enable_bonus_tracks));
ini.write_to_file(file)?;
Ok(())

View File

@ -1,17 +1,26 @@
use anyhow::Result;
use std::collections::BTreeSet;
use std::path::PathBuf;
use crate::model::misc::Game;
use crate::pkg::{PkgKey, Status};
use crate::pkg_store::PackageStore;
use crate::util;
use crate::profiles::types::ProfilePaths;
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, mut redo_bepinex: bool) -> Result<()> {
log::debug!("begin prepare packages");
let pfx_dir = p.data_dir();
let opt_dir = pfx_dir.join("option");
for m in pkgs {
let (namespace, _) = m.split()?;
if namespace == "local" {
log::info!("package with the 'local' namespace enabled -- force refreshing");
redo_bepinex = true;
}
}
if redo_bepinex {
if pfx_dir.join("BepInEx").exists() {
util::remove_dir_all(pfx_dir.join("BepInEx")).await?;
@ -60,6 +69,7 @@ pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgK
}
pub fn prepare_dlls(
game: Game,
enabled_pkgs: &BTreeSet<PkgKey>,
store: &PackageStore,
) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
@ -72,6 +82,16 @@ pub fn prepare_dlls(
if let Some(game_dll) = &dlls.game {
res_game.push(pkg.path().join(game_dll.clone()));
}
if let Some(game32_dll) = &dlls.game32 {
if game.bitness() == 32 {
res_game.push(pkg.path().join(game32_dll.clone()));
}
}
if let Some(game64_dll) = &dlls.game64 {
if game.bitness() == 64 {
res_game.push(pkg.path().join(game64_dll.clone()));
}
}
if let Some(amd_dll) = &dlls.amd {
res_amd.push(pkg.path().join(amd_dll.clone()));
}

View File

@ -5,30 +5,30 @@ use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, Confi
use crate::pkg_store::PackageStore;
impl Segatools {
pub fn fix(&mut self, store: &PackageStore) {
macro_rules! remove_if_nonpresent {
($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
if let Ok(pkg) = $store.get($key) {
if pkg.loc.is_none() {
$item = $emptyval;
}
} else {
$item = $emptyval;
}
}
}
pub fn fix(&mut self, _store: &PackageStore) {
// macro_rules! remove_if_nonpresent {
// ($item:expr,$key:expr,$emptyval:expr,$store:expr) => {
// if let Ok(pkg) = $store.get($key) {
// if pkg.loc.is_none() {
// $item = $emptyval;
// }
// } else {
// $item = $emptyval;
// }
// }
// }
if let Some(key) = &self.hook {
remove_if_nonpresent!(self.hook, key, None, store);
}
if let IOSelection::Custom(key) = &self.io2 {
remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
}
match &self.aime {
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
_ => {},
}
// if let Some(key) = &self.hook {
// remove_if_nonpresent!(self.hook, key, None, store);
// }
// if let IOSelection::Custom(key) = &self.io2 {
// remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
// }
// match &self.aime {
// Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
// Aime::Other(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
// _ => {},
// }
}
pub fn load_from_ini(&mut self, ini: &Ini, config_dir: impl AsRef<Path>) -> Result<()> {
log::debug!("loading sgt");

View File

@ -12,13 +12,18 @@ impl PatchFileVec {
std::fs::create_dir(&path)?;
}
std::fs::write(path.join("builtin-chunithm.json5"), include_bytes!("../static/standard-chunithm.json5"))?;
std::fs::write(path.join("builtin-amdaemon.json5"), include_bytes!("../static/standard-amdaemon.json5"))?;
let mut res = Vec::new();
for f in std::fs::read_dir(path)? {
let f = f?;
let f = f.path();
res.push(
serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?)?
);
let f = &f.path();
match serde_json5::from_str::<PatchFile>(&std::fs::read_to_string(f)?) {
Ok(parsed) => res.push(parsed),
Err(e) => {
log::error!("Error parsing {f:?}: {e}");
anyhow::bail!("Error parsing {f:?}: {e}");
}
}
}
Ok(PatchFileVec(res))
}
@ -26,20 +31,21 @@ impl PatchFileVec {
pub fn find_patches(&self, target: impl AsRef<Path>) -> Result<Vec<Patch>> {
let checksum = try_digest(target.as_ref())?;
let mut res = Vec::new();
let mut res_patches = Vec::new();
for pfile in &self.0 {
for plist in &pfile.0 {
log::debug!("checking {}", plist.sha256);
if plist.sha256 == checksum {
let this_hash = plist.sha256.to_ascii_lowercase();
log::debug!("checking {}", this_hash);
if this_hash == checksum {
let mut cloned = plist.clone().patches;
res.append(&mut cloned);
res_patches.append(&mut cloned);
}
}
}
if res.len() == 0 {
if res_patches.len() == 0 {
log::warn!("no matching patchset for {:?} ({})", target.as_ref(), checksum);
}
Ok(res)
Ok(res_patches)
}
}

View File

@ -41,6 +41,8 @@ pub enum Status {
#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)]
pub struct DLLs {
pub game: Option<String>,
pub game32: Option<String>,
pub game64: Option<String>,
pub amd: Option<String>
}
@ -81,6 +83,7 @@ pub struct Remote {
pub nsfw: bool,
pub categories: Vec<String>,
pub dependencies: BTreeSet<PkgKey>,
pub file_size: i64,
}
impl PkgKey {
@ -106,19 +109,20 @@ impl Package {
loc: None,
rmt: Some(Remote {
package_url: p.package_url,
download_url: v.download_url,
download_url: v.download_url.replace("https://rainy.patafour.zip/", "https://www.rainycolor.org/"),
icon: v.icon,
deprecated: p.is_deprecated,
nsfw: p.has_nsfw_content,
version: v.version_number,
categories: p.categories,
dependencies: Self::sanitize_deps(v.dependencies)
dependencies: Self::sanitize_deps(v.dependencies),
file_size: v.file_size
}),
source: PackageSource::Rainy,
})
}
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<Package> {
pub async fn from_dir(dir: PathBuf, source: PackageSource) -> Result<(Package, Option<Vec<Game>>)> {
let str = fs::read_to_string(dir.join("manifest.json")).await?;
let mft: local::PackageManifest = serde_json::from_str(&str)?;
@ -131,7 +135,7 @@ impl Package {
let status = Self::parse_status(&mft, &dir);
let dependencies = Self::sanitize_deps(mft.dependencies);
Ok(Package {
Ok((Package {
namespace: Self::dir_to_namespace(&dir)?,
name: mft.name.clone(),
description: mft.description.clone(),
@ -144,7 +148,7 @@ impl Package {
}),
rmt: None,
source
})
}, mft.games))
}
pub fn key(&self) -> PkgKey {
@ -229,10 +233,12 @@ impl Package {
if dir.as_ref().join("app").join("data").exists() {
return Status::Unsupported;
}
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None });
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, game32: None, game64: None, amd: None });
} else {
let mut flags = BitFlags::default();
let mut game_dll = None;
let mut game32_dll = None;
let mut game64_dll = None;
let mut amd_dll = None;
for installer in &mft.installers {
if let Some(serde_json::Value::String(id)) = installer.get("identifier") {
@ -260,6 +266,16 @@ impl Package {
flags |= Feature::Mod;
game_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-game32") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game32_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-game64") {
flags |= Feature::GameDLL;
flags |= Feature::Mod;
game64_dll = Some(path.to_owned());
}
if let Some(serde_json::Value::String(path)) = installer.get("dll-amdaemon") {
flags |= Feature::AmdDLL;
flags |= Feature::Mod;
@ -270,8 +286,8 @@ impl Package {
}
}
}
log::debug!("{} parse result: {:?} {:?} {:?}", mft.name, flags, game_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
log::debug!("{} parse result: {:?} {:?} {:?} {:?} {:?}", mft.name, flags, game_dll, game32_dll, game64_dll, amd_dll);
Status::OK(flags, DLLs { game: game_dll, game32: game32_dll, game64: game64_dll, amd: amd_dll })
}
}

View File

@ -21,7 +21,7 @@ pub struct PackageStore {
offline: bool,
}
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Serialize, Deserialize, Debug)]
pub struct Payload {
pub pkg: PkgKey
}
@ -83,7 +83,7 @@ impl PackageStore {
pub async fn reload_package(&mut self, key: PkgKey) {
let dir = util::pkg_dir().join(&key.0);
if let Ok(pkg) = Package::from_dir(dir, PackageSource::Rainy).await {
if let Ok((pkg, _)) = Package::from_dir(dir, PackageSource::Rainy).await {
self.update_nonremote(key, pkg);
} else {
log::error!("couldn't reload {}", key);
@ -102,7 +102,13 @@ impl PackageStore {
}
while let Some(res) = futures.join_next().await {
if let Ok(Ok(pkg)) = res {
if let Ok(Ok((pkg, locally_declared_games))) = res {
if let Some(games) = locally_declared_games {
self.meta_list.insert(pkg.key(), PackageListEntry {
version: pkg.loc.as_ref().unwrap().version.clone(),
games
});
}
self.update_nonremote(pkg.key(), pkg);
}
}
@ -126,7 +132,7 @@ impl PackageStore {
prelude::*,
};
let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?;
let response = reqwest::get(format!("https://www.rainycolor.org/c/{game}/api/v1/package/")).await?;
let reader = response
.bytes_stream()
@ -152,7 +158,6 @@ impl PackageStore {
PackageListEntry {
// from_rainy() is guaranteed to include rmt
version: r.rmt.as_ref().unwrap().version.clone(),
status: Status::Unchecked,
games: vec![ game ],
}
});

View File

@ -1,6 +1,6 @@
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
use crate::{model::{misc::Game, patch::{PatchList, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
use tauri::Emitter;
use std::process::Stdio;
use crate::model::profile::BepInEx;
@ -10,8 +10,23 @@ use std::fs::File;
use tokio::process::Command;
use tokio::task::JoinSet;
#[cfg(target_os = "windows")]
use crate::modules::display_windows::DisplayInfo;
pub mod template;
pub mod types;
#[cfg(target_os = "linux")]
pub trait RawArg {
fn raw_arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Command;
}
#[cfg(target_os = "linux")]
impl RawArg for Command {
fn raw_arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Command {
return self.arg::<S>(arg);
}
}
impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
meta.name = fixed_name(&meta, true);
@ -28,14 +43,14 @@ impl Profile {
bepinex: if meta.game == Game::Ongeki { Some(BepInEx::default()) } else { None },
#[cfg(not(target_os = "windows"))]
wine: crate::model::profile::Wine::default(),
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini { audio: None, blacklist: None }) } else { None },
mu3_ini: if meta.game == Game::Ongeki { Some(Mu3Ini::default()) } else { None },
keyboard:
if meta.game == Game::Ongeki {
Some(Keyboard::Ongeki(OngekiKeyboard::default()))
} else {
Some(Keyboard::Chunithm(ChunithmKeyboard::default()))
},
patches: if meta.game == Game::Chunithm { Some(PatchSelection(BTreeMap::new())) } else { None }
patches: Some(PatchSelection(BTreeMap::new()))
},
meta: meta.clone()
};
@ -67,6 +82,21 @@ impl Profile {
data.sgt.io2 = IOSelection::Custom(io);
data.sgt.io = None;
}
if let Some(ini) = &mut data.mu3_ini {
if ini.audio.is_none() {
ini.audio = Some(crate::model::profile::Mu3Audio::Shared);
}
if ini.blacklist.is_none() {
ini.blacklist = Some((10000, 19999));
}
} else {
data.mu3_ini = Some(Mu3Ini::default());
}
if data.patches.is_none() {
data.patches = Some(PatchSelection(BTreeMap::new()));
}
Self::load_existing_mu3_ini(&data, &ProfileMeta { game, name: name.clone() })?;
}
if game == Game::Chunithm {
if data.keyboard.is_none() {
@ -160,6 +190,7 @@ impl Profile {
self.data.patches = source.patches;
}
}
#[cfg(target_os = "windows")]
pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display {
None => None,
@ -168,7 +199,7 @@ impl Profile {
Ok(info)
}
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patch_files: &PatchFileVec) -> Result<()> {
pub async fn line_up(&self, pkg_hash: String, refresh: bool, patchlists_enabled: Vec<&PatchList>) -> Result<()> {
if !self.data_dir().exists() {
tokio::fs::create_dir(self.data_dir()).await?;
}
@ -203,13 +234,13 @@ impl Profile {
}
if let Some(mu3ini) = &self.data.mu3_ini {
mu3ini.line_up(&self.data.sgt.target.parent().unwrap())?;
mu3ini.line_up(&self.data_dir(), &self.config_dir())?;
}
if let Some(patches) = &self.data.patches {
futures::try_join!(
patches.render_to_file("amdaemon.exe", patch_files, self.data_dir().join("patch-amd.mph")),
patches.render_to_file("chusanApp.exe", patch_files, self.data_dir().join("patch-game.mph"))
patches.render_to_file("amdaemon.exe", &patchlists_enabled, self.data_dir().join("patch-amd.mph")),
patches.render_to_file("chusanApp.exe", &patchlists_enabled, self.data_dir().join("patch-game.mph"))
)?;
}
@ -236,8 +267,8 @@ impl Profile {
}
#[cfg(target_os = "linux")]
{
game_builder = Command::new(&self.wine.runtime);
amd_builder = Command::new(&self.wine.runtime);
game_builder = Command::new(&self.data.wine.runtime);
amd_builder = Command::new(&self.data.wine.runtime);
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder.arg("cmd.exe");
@ -283,6 +314,14 @@ impl Profile {
"ONGEKI_LANG_PATH",
self.data_dir().join("lang"),
)
.env(
"MU3_MODS_CONFIG_PATH",
self.config_dir().join("mu3.ini"),
)
.env(
"STARTLINER",
"1"
)
.current_dir(&exe_dir)
.raw_arg("-d")
.raw_arg("-k")
@ -325,8 +364,8 @@ impl Profile {
#[cfg(target_os = "linux")]
{
amd_builder.env("WINEPREFIX", &self.wine.prefix);
game_builder.env("WINEPREFIX", &self.wine.prefix);
amd_builder.env("WINEPREFIX", &self.data.wine.prefix);
game_builder.env("WINEPREFIX", &self.data.wine.prefix);
}
let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?;
@ -403,6 +442,19 @@ impl Profile {
Ok(false)
}
}
fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> {
if let Some(parent) = data.sgt.target.parent() {
let mu3_ini_target_path = parent.join("mu3.ini");
let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini");
log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path);
if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() {
std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?;
log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path);
}
}
Ok(())
}
}
impl ProfilePaths for Profile {

View File

@ -0,0 +1,90 @@
use std::{fs::File, io::{Read, Write}, path::PathBuf};
use zip::{write::FileOptions, ZipArchive, ZipWriter};
use crate::util;
use super::{Profile, ProfilePaths};
impl Profile {
fn find_template_json(archive: &mut ZipArchive<File>) -> anyhow::Result<String> {
if let Ok(mut file) = archive.by_name("template.json") {
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
Ok(String::from_utf8(contents)?)
} else {
anyhow::bail!("invalid template: no template.json found")
}
}
pub fn import(path: PathBuf) -> anyhow::Result<()> {
let file = File::open(path)?;
let mut archive = ZipArchive::new(file)?;
match Self::find_template_json(&mut archive) {
Ok(raw_p) => {
let p = serde_json::from_str::<Profile>(&raw_p)?;
let dir = util::config_dir().join(format!("profile-{}-{}", &p.meta.game, &p.meta.name));
if dir.exists() {
anyhow::bail!("profile {} already exists", &p.meta.name);
}
std::fs::create_dir(&dir)?;
archive.extract(&dir)?;
std::fs::remove_file(dir.join("template.json"))?;
std::fs::write(dir.join("profile.json"), serde_json::to_string_pretty(&p.data)?)?;
}
Err(e) => {
return Err(e);
}
}
Ok(())
}
pub fn export(&self, export_keychip: bool, extra_files: Vec<String>) -> anyhow::Result<()> {
let mut prf = self.clone();
let dir = util::config_dir().join("exports");
if !dir.exists() {
std::fs::create_dir(&dir)?;
}
let path = dir.join(format!("{}-{}-template.zip", &self.meta.game, &self.meta.name));
{
let sgt = &mut prf.data.sgt;
sgt.target = PathBuf::new();
if sgt.amfs.is_absolute() {
sgt.amfs = PathBuf::new();
}
if sgt.option.is_absolute() {
sgt.option = PathBuf::new();
}
if sgt.appdata.is_absolute() {
sgt.appdata = PathBuf::new();
}
}
{
let network = &mut prf.data.network;
if network.local_path.is_absolute() {
network.local_path = PathBuf::new();
}
if !export_keychip {
network.keychip = String::new();
}
}
let file = File::create(&path)?;
let mut zip = ZipWriter::new(file);
let options: FileOptions<'_, ()> = FileOptions::default();
zip.start_file("template.json", options)?;
zip.write_all(&serde_json::to_string_pretty(&prf)?.as_bytes())?;
for file in extra_files {
log::debug!("extra file: {file}");
zip.start_file(&file, options)?;
zip.write_all(&std::fs::read(self.config_dir().join(file))?)?;
}
zip.finish()?;
Ok(())
}
}

View File

@ -152,6 +152,7 @@ impl PathStr for PathBuf {
}
}
#[allow(dead_code)]
pub fn bool_to_01(val: bool) -> &'static str {
return if val { "1" } else { "0" }
}
@ -199,7 +200,7 @@ pub fn create_shortcut(
obj.SetDescription(&format!("{} {} (STARTLINER)", &meta.game.print(), &meta.name))?;
obj.SetArguments(&format!("--start --game {} --profile {}", &meta.game, &meta.name))?;
obj.SetIconLocation(
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
target_dir.join(format!("icon-{}.ico", &meta.game)).to_str().ok_or_else(|| anyhow!("Illegal icon path"))?,
0
)?;

View File

@ -64,19 +64,4 @@ controllerLedOutputOpeNITHM=0
; [60]-[62]: right side partition LEDs
;
; Board 2 is the slider and has 31 LEDs:
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
; -----------------------------------------------------------------------------
; Custom IO settings
; -----------------------------------------------------------------------------
[chuniio]
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
; (will use chu2to3 engine internally)
;path=
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
; x86 chuniio to path32, x64 to path64. Both are necessary.
;path32=
;path64=
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers

View File

@ -0,0 +1,105 @@
[
{
// CHUNITHM Luminous+
filename: 'amdaemon.exe',
version: '2.25.00',
sha256: '00FB867D1EE821033101B8773FAC116A45DF1939D23C38E9DAFC9B86CD5A3777',
patches: [
{
id: 'standard-localhost',
name: "Allow 127.0.0.1/localhost as the network server",
patches: [
{ offset: 0x6E28A4, off: [0x31, 0x32, 0x37, 0x2F], on: [0x30, 0x2F, 0x38, 0x00] },
{ offset: 0x3C94C4, off: [0xFF, 0x15, 0xC6, 0x2F, 0x1B, 0x00, 0x8B], on: [0x33, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3] }
]
},
{
id: 'standard-credit-freeze',
name: "Infinite credits",
patches: [
{ offset: 0x2BBBC8, off: [0x28], on: [0x08] }
]
}
]
},
{
// CHUNITHM Verse
filename: 'amdaemon.exe',
version: '2.30.00',
sha256: 'd4809220578374865370e31c541ed6e406b854d8c26cfe7464c2c15145113bfd',
patches: [
{
id: 'standard-localhost',
name: 'Allow 127.0.0.1/localhost as the network server',
patches: [
{
offset: 0x6e1ca4,
off: [0x31, 0x32, 0x37, 0x2f],
on: [0x30, 0x2f, 0x38, 0x00],
},
{
offset: 0x3c88c4,
off: [0xff, 0x15, 0xc6, 0x2f, 0x1b, 0x00, 0x8b],
on: [0x33, 0xc0, 0x48, 0x83, 0xc4, 0x28, 0xc3],
},
],
},
{
id: 'standard-credit-freeze',
name: 'Credit freeze',
tooltip: 'Prevents credits from being used. At least one credit must be available to start the game or purchase premium tickets.',
patches: [{ offset: 0x2bafc8, off: [0x28], on: [0x08] }],
},
{
id: 'standard-openssl-fix',
name: 'OpenSSL SHA crash bug fix',
tooltip: 'Fix crashes on 10th generation and newer Intel CPUs',
patches: [
{ offset: 0x4d4a43, off: [0x48], on: [0x4c] },
{ offset: 0x4d4a4b, off: [0x48], on: [0x49] },
],
},
],
},
{
// Ongeki
filename: "amdaemon.exe",
version: "46d47eab",
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
patches: [
{
id: 'standard-localhost',
name: "Allow 127.0.0.1/localhost as the network server",
patches: [
{
offset: 0x69042C,
off: [0x31, 0x32, 0x37, 0x2F],
on: [0x30, 0x2F, 0x38, 0x00]
},
{
offset: 0x3A0E34,
off: [0xFF, 0x15, 0x86, 0xD5, 0x19, 0x00, 0x8B],
on: [0x33, 0xC0, 0x48, 0x83, 0xC4, 0x28, 0xC3]
}
]
},
{
id: 'standard-credit-freeze',
name: "Credit freeze",
tooltip: "Prevents credits from being used. At least one credit must be available to start the game or purchase premium tickets.",
patches: [
{ offset: 0x2AB4E8, off: [0x28], on: [0x08] }
]
},
{
id: 'standard-openssl-fix',
name: "OpenSSL SHA crash bug fix",
tooltip: "Fixes crashing on 10th generation and newer Intel CPUs",
patches: [
{ offset: 0x4A4C43, off: [0x48], on: [0x4C] },
{ offset: 0x4A4C4B, off: [0x48], on: [0x49] }
]
},
]
},
]

View File

@ -1,4 +1,132 @@
[
{
filename: 'chusanApp.exe',
version: '2.26.00',
sha256: 'AD2DCC02CE52B3FFF24A2919F8617854581DD2E2C0378EA13D84438FCCA2D522',
patches: [
{
id: 'standard-shared-audio',
name: "Force shared audio mode, system audio sample rate must be 48000Hz",
tooltip: "Improves compatibility, but may increase latency",
patches: [
{offset: 0xF233DA, off: [0x01], on: [0x00]}
]
},
{
id: 'standard-2ch',
name: "Force 2 channel audio output",
tooltip: "May cause bass overload",
patches: [
{offset: 0xF234B1, off: [0x75, 0x3f], on: [0x90, 0x90]}
]
},
{
id: 'standard-song-timer',
name: "Disable song select timer",
patches: [
{offset: 0xA03916, off: [0x74], on: [0xeb]}
]
},
{
id: 'standard-map-timer',
name: "Map selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0x965B37,
default: 30,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-ticket-timer',
name: "Ticket selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0x9592C2,
default: 60,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-course-timer',
name: "Course selection timer",
tooltip: "If set to negative, the timer becomes 968 + value (e.g. 968 + -1 = 967)",
type: "number",
offset: 0xA0EADB,
default: 30,
size: 1,
min: -128,
max: 127,
},
{
id: 'standard-unlimited-tracks',
name: "Unlimited maximum tracks",
tooltip: "Must check to play more than 7 tracks per credit",
patches: [
{offset: 0x71E2E0, off: [0xf0], on: [0xc0]}
]
},
{
id: 'standard-maximum-tracks',
type: "number",
name: "Maximum tracks",
offset: 0x3980C1,
default: 3,
size: 1,
min: 3,
max: 12
},
{
id: 'standard-no-encryption',
name: "No encryption",
tooltip: "Will also disable TLS",
patches: [
{offset: 0x1DE29E8, off: [0xE1], on: [0x00]},
{offset: 0x1DE29EC, off: [0xE1], on: [0x00]}
]
},
{
id: 'standard-no-tls',
name: "No TLS",
tooltip: "Title server workaround",
patches: [
{offset: 0xF06447, off: [0x80], on: [0x00]}
]
},
{
id: 'standard-head-to-head',
name: "Patch for head-to-head play",
tooltip: "Fix infinite sync while trying to connect to head to head play",
patches: [
{offset: 0x6533A3, off: [0x01], on: [0x00]}
]
},
{
id: 'standard-bypass-1080p',
name: "Bypass 1080p monitor check",
patches: [
{offset: 0x1CCBF, off: [0x81, 0xbc, 0x24, 0xb8, 0x02, 0x00, 0x00, 0x80, 0x07, 0x00, 0x00, 0x75, 0x1f, 0x81, 0xbc, 0x24, 0xbc, 0x02, 0x00, 0x00, 0x38, 0x04, 0x00, 0x00, 0x75, 0x12], on: [0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90]}
]
},
{
id: 'standard-bypass-120hz',
name: "Bypass 120Hz monitor check",
patches: [
{offset: 0x1CCB1, off: [0x85, 0xc0], on: [0xeb, 0x30]}
]
},
{
id: 'standard-force-free-play-text',
name: "Force FREE PLAY credit text",
tooltip: "Replaces the credit count with FREE PLAY",
patches: [
{offset: 0x3875A4, off: [0x3c, 0x01], on: [0x38, 0xc0]}
]
},
],
},
{
filename: 'chusanApp.exe',
version: '2.30.00',
@ -176,43 +304,25 @@
},
],
},
],
},
{
filename: 'amdaemon.exe',
version: '2.30.00',
sha256: 'd4809220578374865370e31c541ed6e406b854d8c26cfe7464c2c15145113bfd',
patches: [
{
id: 'standard-localhost',
name: 'Allow 127.0.0.1/localhost as the network server',
patches: [
{
offset: 0x6e1ca4,
off: [0x31, 0x32, 0x37, 0x2f],
on: [0x30, 0x2f, 0x38, 0x00],
},
{
offset: 0x3c88c4,
off: [0xff, 0x15, 0xc6, 0x2f, 0x1b, 0x00, 0x8b],
on: [0x33, 0xc0, 0x48, 0x83, 0xc4, 0x28, 0xc3],
},
],
id: 'standard-custom-free-play-length',
type: 'number',
name: 'Custom FREE PLAY text length',
tooltip: 'Changes the length of the text displayed when Force FREE PLAY credit text is enabled',
danger: 'If this is longer than 11 characters, \"Force FREE PLAY credit text\" MUST be enabled.',
offset: 0x3875A9,
size: 1,
default: 9,
min: 0,
max: 27,
},
{
id: 'standard-credit-freeze',
name: 'Credit freeze',
tooltip: 'Prevents credits from being used. At least one credit must be available to start the game or purchase premium tickets.',
patches: [{ offset: 0x2bafc8, off: [0x28], on: [0x08] }],
},
{
id: 'standard-openssl-fix',
name: 'OpenSSL SHA crash bug fix',
tooltip: 'Fix crashes on 10th generation and newer Intel CPUs',
patches: [
{ offset: 0x4d4a43, off: [0x48], on: [0x4c] },
{ offset: 0x4d4a4b, off: [0x48], on: [0x49] },
],
id: 'standard-custom-free-play-text',
type: 'hex',
name: 'Custom FREE PLAY text',
tooltip: 'Replace the FREE PLAY text when using Infinite credits',
offset: 0x1A5DFB4,
off: [0x46, 0x52, 0x45, 0x45, 0x20, 0x50, 0x4c, 0x41, 0x59],
},
],
},

View File

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

View File

@ -20,15 +20,16 @@ import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue';
import { invoke } from '../invoke';
import {
useClientStore,
useGeneralStore,
usePkgStore,
usePrfStore,
} from '../stores';
import { Dirs } from '../types';
import { messageSplit, shouldPreferDark } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
@ -37,10 +38,10 @@ const prf = usePrfStore();
const general = useGeneralStore();
const client = useClientStore();
client.load();
pkg.setupListeners();
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null);
@ -56,17 +57,11 @@ listen<undefined>('update-end', (_) => {
});
onMounted(async () => {
invoke('list_directories').then((d) => {
general.dirs = d as Dirs;
client.load();
});
const fetch_promise = pkg.fetch(true);
await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) {
currentTab.value = 'loc';
await pkg.reloadAll();
}
@ -88,6 +83,60 @@ listen<string>('launch-error', (event) => {
errorMessage.value = event.payload;
errorHeader.value = 'Launch error';
});
interface DownloadingStatus {
ratio: number;
pkg_key: string;
}
const downloading_status: Ref<DownloadingStatus[]> = ref([]);
const download_value = computed(() => {
return (
downloading_status.value.map((v) => v.ratio).reduce((a, v) => a * v) *
100
);
});
const downloadProgressText = computed(() => {
if (download_value.value < 7) {
return '';
}
let pkgs = `${downloading_status.value.length} package${downloading_status.value.length === 1 ? '' : 's'}`;
if (download_value.value < 14) {
return pkgs;
} else {
return `${pkgs} (${Math.floor(download_value.value)}%)`;
}
});
listen<DownloadingStatus>('download-progress', (event) => {
let status = downloading_status.value.find(
(v) => v.pkg_key === event.payload.pkg_key
);
if (status === undefined) {
status = {
ratio: 0,
pkg_key: event.payload.pkg_key,
};
downloading_status.value.push(status);
}
status.ratio = event.payload.ratio;
const remove = () => {
if (status !== undefined) {
downloading_status.value = downloading_status.value.filter(
(v) => v.pkg_key !== event.payload.pkg_key
);
}
};
if (status.ratio === 1.0) {
remove();
}
setTimeout(() => remove, 10_000);
});
</script>
<template>
@ -102,6 +151,16 @@ listen<string>('launch-error', (event) => {
: 'main-scale-xl'
"
>
<div
v-if="downloading_status.length > 0"
class="download-progress-bg"
></div>
<ProgressBar
v-if="downloading_status.length > 0"
:value="download_value"
class="download-progress"
>{{ downloadProgressText }}</ProgressBar
>
<ConfirmDialog>
<template #message="{ message }">
<ScrollPanel
@ -147,34 +206,34 @@ listen<string>('launch-error', (event) => {
<Tabs
lazy
:value="currentTab"
:value="client.currentTab"
v-on:update:value="
(value) => {
currentTab = value as any;
client.currentTab = value as string;
client.save();
}
"
class="h-screen"
>
<div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false">
<Tab value="users"><div class="pi pi-users"></div></Tab>
<Tab value="users"><div class="pi pi-home"></div></Tab>
<Tab
:disabled="
isProfileDisabled || pkg.networkStatus !== 'online'
"
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div
></Tab>
<Tab
v-if="
prf.current?.meta.game === 'chunithm' &&
prf.current.data.sgt.target.length > 0
"
v-if="(prf.current?.data.sgt.target.length ?? 0) > 0"
value="patches"
><div class="pi pi-ticket"></div
></Tab>
<Tab
v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled"
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div
></Tab>
@ -187,17 +246,21 @@ listen<string>('launch-error', (event) => {
<div class="flex gap-4">
<div
class="flex"
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
v-if="
['loc', 'rmt', 'cfg'].includes(
client.currentTab
)
"
>
<InputIcon class="self-center mr-2">
<i class="pi pi-search" />
</InputIcon>
<InputText
v-if="currentTab === 'cfg'"
v-if="client.currentTab === 'cfg'"
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
placeholder="Search"
:placeholder="t('search')"
v-model="general.cfgSearchTerm"
/>
<InputText
@ -205,7 +268,7 @@ listen<string>('launch-error', (event) => {
style="min-width: 0; width: 25dvw"
class="self-center"
size="small"
placeholder="Search"
:placeholder="t('search')"
v-model="pkgSearchTerm"
/>
</div>
@ -243,19 +306,19 @@ listen<string>('launch-error', (event) => {
</TabList>
</div>
<TabPanels class="w-full grow mt-[3rem]">
<TabPanel value="loc">
<TabPanel value="loc" v-if="!isProfileDisabled">
<ModList :search="pkgSearchTerm" />
</TabPanel>
<TabPanel value="rmt">
<TabPanel value="rmt" v-if="!isProfileDisabled">
<ModStore :search="pkgSearchTerm" />
</TabPanel>
<TabPanel value="cfg">
<TabPanel value="cfg" v-if="!isProfileDisabled">
<OptionList />
</TabPanel>
<TabPanel value="users">
<ProfileList />
</TabPanel>
<TabPanel value="patches">
<TabPanel value="patches" v-if="!isProfileDisabled">
<PatchList
v-if="
pkg.hasLocal('mempatcher-mempatcher') &&
@ -286,7 +349,12 @@ listen<string>('launch-error', (event) => {
<InfoPage />
</TabPanel>
</TabPanels>
<div v-if="currentTab === 'users' || currentTab === 'info'">
<div
v-if="
client.currentTab === 'users' ||
client.currentTab === 'info'
"
>
<img
v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg"
@ -353,4 +421,23 @@ body {
.p-progressbar-label {
transition-duration: 0s !important;
}
.download-progress {
position: fixed !important;
bottom: 0;
left: 5vw;
width: 90vw;
z-index: 10000 !important;
margin: 20px auto;
}
.download-progress-bg {
position: fixed;
bottom: 0;
left: 0;
width: 100vw;
height: 60px;
background-color: var(--p-surface-900);
border-top: 1px solid var(--p-surface-600);
z-index: 998;
}
</style>

View File

@ -65,7 +65,7 @@ const filePick = async () => {
<template>
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
<div v-else>
<div class="primitive-base" v-else>
<Button
v-if="exists"
icon="pi pi-pen-to-square"
@ -102,12 +102,20 @@ const filePick = async () => {
font-family: monospace;
white-space: nowrap;
position: fixed;
top: 10vh;
left: 10vw;
height: 80vh;
width: 80vw;
top: 50%;
left: 50%;
height: 500px;
width: 800px;
margin-left: -400px;
margin-top: -250px;
z-index: 1000;
padding: 20px;
border-radius: 20px;
background-color: #151515;
color: #ddd;
}
.primitive-base ::-webkit-scrollbar {
display: none;
}
</style>

View File

@ -12,11 +12,11 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
<template>
<h1>About</h1>
STARTLINER is a simple launcher, configuration tool and mod manager for
STARTLINER is a 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">
<div class="markdown">
<vue-markdown-it
:source="changelog"
:options="{ typographer: true, breaks: true }"
@ -44,13 +44,20 @@ h1 {
font-size: 1.7rem;
}
.changelog h2 {
.markdown h3 {
font-size: 1.2rem;
}
.markdown h2 {
font-size: 1.4rem;
}
.changelog ul {
.markdown ul {
list-style-type: circle;
}
.changelog li {
.markdown li {
margin-left: 40px;
}
.markdown a {
text-decoration: underline;
}
</style>

View File

@ -1,4 +1,5 @@
<script setup lang="ts">
import { ref } from 'vue';
import Button from 'primevue/button';
import { invoke } from '../invoke';
import { usePkgStore } from '../stores';
@ -11,20 +12,26 @@ const props = defineProps({
pkg: Object as () => Package,
});
const deleting = ref(false);
const remove = async () => {
if (props.pkg === undefined) {
return;
}
deleting.value = true;
await invoke('delete_package', {
key: pkgKey(props.pkg),
});
deleting.value = false;
};
</script>
<template>
<Button
v-if="pkg?.loc && !pkg?.js.busy"
v-if="pkg?.loc && !pkg?.js.downloading"
rounded
icon="pi pi-trash"
severity="danger"
@ -32,7 +39,7 @@ const remove = async () => {
size="small"
class="self-center ml-4"
style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy"
:loading="deleting"
v-on:click="remove()"
/>
@ -45,7 +52,7 @@ const remove = async () => {
size="small"
class="self-center ml-4"
style="width: 2rem; height: 2rem"
:loading="pkg?.js.busy"
:loading="pkg?.js.downloading"
v-on:click="async () => await pkgs.install(pkg)"
/>
</template>

View File

@ -1,8 +1,12 @@
<script setup lang="ts">
import { computed, ref } from 'vue';
import InputText from 'primevue/inputtext';
import { fromKeycode, toKeycode } from '../keyboard';
import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
@ -15,9 +19,55 @@ const handleKey = (
) => {
event.preventDefault();
const keycode = toKeycode(event.code);
let keycode = toKeycode(event.code);
if (keycode !== null && button !== undefined) {
const data = prf.current!.data.keyboard!.data as any;
if (event.getModifierState('NumLock') === false) {
switch (event.code) {
case 'NumpadDecimal':
keycode = toKeycode('Delete');
break;
case 'Numpad0':
keycode = toKeycode('Insert');
break;
case 'Numpad1':
keycode = toKeycode('End');
break;
case 'Numpad2':
keycode = toKeycode('ArrowDown');
break;
case 'Numpad3':
keycode = toKeycode('PageDown');
break;
case 'Numpad4':
keycode = toKeycode('ArrowLeft');
break;
case 'Numpad5':
keycode = toKeycode('Clear');
break;
case 'Numpad6':
keycode = toKeycode('ArrowRight');
break;
case 'Numpad7':
keycode = toKeycode('Home');
break;
case 'Numpad8':
keycode = toKeycode('ArrowUp');
break;
case 'Numpad9':
keycode = toKeycode('PageUp');
break;
default:
break;
}
}
if (event.code === 'Escape') {
keycode = 0;
}
if (index !== undefined) {
data[button][index] = keycode;
} else {
@ -75,7 +125,7 @@ const handleMouse = (
}
};
const getKey = (key: keyof OngekiButtons, index?: number) =>
const getKey = (key: keyof OngekiButtons, index?: number): any =>
computed(() => {
const data = prf.current!.data.keyboard?.data as any;
const keycode =
@ -85,147 +135,56 @@ const getKey = (key: keyof OngekiButtons, index?: number) =>
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '';
});
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};
defineProps({
const props = defineProps({
small: Boolean,
verySmall: Boolean,
tall: Boolean,
tooltip: String,
button: String,
color: String,
index: Number,
});
const modelValue = computed(() => {
return getKey(props.button as keyof OngekiButtons, props.index).value;
});
const fontSize = computed(() => {
if (!props.small) {
return '1rem';
}
const len = modelValue.value.length;
if (len < 5) {
return '1rem';
}
if (len < 7) {
return '0.75rem';
}
return '0.5rem';
});
</script>
<template>
<InputText
:style="{
width: small ? '3em' : '5em',
height: small ? '3em' : tall ? '10em' : '5em',
fontSize: small ? '0.9em' : '1em',
width: small ? '2.8rem' : '5rem',
height:
small && tall
? '5rem'
: small
? '2.8rem'
: tall
? '10rem'
: '5rem',
fontSize,
backgroundColor: color,
}"
unstyled
class="text-center buttoninputtext"
v-tooltip="tooltip"
v-tooltip="
tooltip
? `${tooltip}: ${modelValue} ${tooltip.startsWith('ir') ? `\n${t('cfg.keyboard.irTooltip')}` : ''}`
: undefined
"
@contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown="
@ -233,7 +192,7 @@ defineProps({
handleMouse(button as keyof OngekiButtons, ev, index)
"
@focusout="() => (hasClickedM1Once = false)"
:model-value="getKey(button as keyof OngekiButtons, index) as any"
:model-value="modelValue"
/>
</template>
@ -241,5 +200,7 @@ defineProps({
.buttoninputtext {
border-radius: 6px;
border: 1px solid rgba(200, 200, 200, 0.3);
overflow: scroll !important;
text-align: center !important;
}
</style>

View File

@ -1,65 +1,312 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Fieldset from 'primevue/fieldset';
import InputText from 'primevue/inputtext';
import MultiSelect from 'primevue/multiselect';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import { emit } from '@tauri-apps/api/event';
import ModListEntry from './ModListEntry.vue';
import ModTitlecard from './ModTitlecard.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { Package } from '../types';
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Feature, Game, Package } from '../types';
import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
search: String,
});
const pkgs = usePkgStore();
const client = useClientStore();
const prf = usePrfStore();
const groupCallIndex = ref(0);
const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', {
game: prf.current?.meta.game,
}).then((list) => {
gameSublist.value = list as string[];
const loadPackages = () => {
invoke('get_game_packages', {
game: prf.current?.meta.game ?? null,
}).then((list) => {
gameSublist.value = list as string[];
});
};
loadPackages();
const allCategories = computed(() => {
const res = new Set<string>();
for (const pkg of pkgs.allLocal) {
for (const cat of pkg.rmt?.categories ?? []) {
res.add(cat);
}
}
return [...res.values()].sort((a, b) => a.localeCompare(b));
});
const group = computed(() => {
const res = Object.assign(
{},
Object.groupBy(
pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter(
(p) =>
props.search === undefined ||
p.name
.toLowerCase()
.includes(props.search.toLowerCase()) ||
p.namespace
.toLowerCase()
.includes(props.search.toLowerCase())
)
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
({ namespace }) => namespace
)
);
if (groupCallIndex.value > 0) {
empty.value = Object.keys(res).length === 0;
const local = computed(() => {
return pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter((p) => p.namespace === 'local');
});
const groupedList = computed(() => {
const searchedPkgs = pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p)))
.filter((p) => p.namespace !== 'local')
.filter(
(p) =>
props.search === undefined ||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
p.namespace.toLowerCase().includes(props.search.toLowerCase())
);
let grouped;
if (client.pkgListMode === 'namespace') {
grouped = Object.groupBy(searchedPkgs, ({ namespace }) => namespace);
} else if (client.pkgListMode === 'type') {
grouped = {
standard: [] as Package[],
native: [] as Package[],
segatools: [] as Package[],
unsupported: [] as Package[] | undefined,
};
grouped.unsupported = [];
for (const pkg of searchedPkgs) {
const loc = pkg.loc;
if (!loc || !loc.status || typeof loc.status === 'string') {
grouped.unsupported.push(pkg);
} else {
if (
loc.status.OK[0] &
(Feature.GameDLL | Feature.Mempatcher | Feature.AmdDLL)
) {
grouped.native.push(pkg);
} else if (loc.status.OK[0] & Feature.Mod) {
grouped.standard.push(pkg);
}
if (
loc.status.OK[0] &
(Feature.AMNet |
Feature.Aime |
Feature.ChuniIO |
Feature.ChusanHook |
Feature.Mu3IO |
Feature.Mu3Hook)
) {
grouped.segatools.push(pkg);
}
}
}
if (grouped.unsupported.length === 0) {
delete grouped.unsupported;
}
} else {
grouped = {} as { [key: string]: Package[] };
for (const pkg of searchedPkgs) {
for (const cat of pkg.rmt?.categories ?? []) {
if (client.hiddenCategories.includes(cat)) {
continue;
}
if (!(cat in grouped)) {
grouped[cat] = [] as Package[];
}
grouped[cat].push(pkg);
}
}
}
groupCallIndex.value += 1;
let res: [string, Package[]][] = [];
for (const [k, v] of Object.entries(grouped)) {
if (v !== undefined) {
res.push([k, v]);
}
}
if (
client.pkgListMode === 'namespace' ||
client.pkgListMode === 'category'
) {
res.sort((a, b) => `${a[0]}`.localeCompare(`${b[0]}`));
} else if (client.pkgListMode === 'type') {
for (const entry of res) {
entry[0] = t(`pkglist.${entry[0]}`);
}
}
return res;
});
const missing = computed(() => {
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
});
const dialogVisible = ref(false);
const defaultModel = {
name: '',
description: '',
website: '',
type: 'rainy',
games: [] as string[],
};
const creatorModel = ref({ ...defaultModel });
const gameModel = (game: Game) =>
computed({
get() {
return (creatorModel.value.games as string[]).includes(game);
},
set(v: boolean) {
creatorModel.value.games = creatorModel.value.games.filter(
(g) => g !== game
);
if (v) {
creatorModel.value.games.push(game);
}
},
});
const gameModelOngeki = gameModel('ongeki');
const gameModelChunithm = gameModel('chunithm');
</script>
<template>
<Fieldset legend="Missing" v-if="(missing?.length ?? 0) > 0">
<Dialog
modal
:visible="dialogVisible"
:closable="false"
:header="t('creator.header')"
:style="{ width: '500px', scale: client.scaleValue }"
class="creation-dialog"
>
<div style="position: absolute; left: 250px; top: 25px">
<a
href="https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format"
target="_blank"
style="text-decoration: underline"
class="self-center"
>{{ t('creator.packageFormat') }}</a
>
</div>
<h2>{{ t('creator.basic') }}</h2>
<div class="flex flex-col gap-3">
<InputText
size="small"
style="width: 100%"
:placeholder="t('creator.name')"
v-model="creatorModel.name"
/>
<InputText
size="small"
style="width: 100%"
:placeholder="t('creator.description')"
v-model="creatorModel.description"
/>
<InputText
size="small"
style="width: 100%"
:placeholder="t('creator.website')"
v-model="creatorModel.website"
/>
</div>
<h2>{{ t('creator.type') }}</h2>
<div class="flex flex-col items-center">
<SelectButton
:options="[
{ title: t('creator.rainy'), value: 'rainy' },
{ title: t('creator.native'), value: 'native' },
{ title: t('creator.segatools'), value: 'segatools' },
]"
v-model="creatorModel.type"
:allow-empty="false"
option-label="title"
option-value="value"
/>
</div>
<h2>{{ t('creator.games') }}</h2>
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<div class="grow">{{ t('game.ongeki') }}</div>
<ToggleSwitch v-model="gameModelOngeki" />
</div>
<div class="flex flex-row">
<div class="grow">{{ t('game.chunithm') }}</div>
<ToggleSwitch v-model="gameModelChunithm" />
</div>
</div>
<div class="flex flex-row mt-5">
<Button
class="ml-auto mr-1"
style="width: 80px"
:label="t('ok')"
:disabled="creatorModel.games.length === 0"
@click="
async () => {
await invoke('create_package', creatorModel);
await pkgs.reloadAll();
loadPackages();
dialogVisible = false;
creatorModel = { ...defaultModel };
}
"
/>
<Button
class="mr-auto ml-1"
style="width: 80px"
:label="t('cancel')"
@click="
() => (
(dialogVisible = false),
(creatorModel = { ...defaultModel })
)
"
/>
</div>
</Dialog>
<div class="flex flex-row">
<SelectButton
:options="[
{ title: t('pkglist.namespace'), value: 'namespace' },
{ title: t('pkglist.type'), value: 'type' },
{ title: t('pkglist.category'), value: 'category' },
]"
v-model="client.pkgListMode"
v-on:update:model-value="
client.save();
emit('reload-icons');
"
:allow-empty="false"
option-label="title"
option-value="value"
/>
<div
class="grow text-right mr-2 self-center"
v-if="client.pkgListMode === 'category'"
>
{{ t('pkglist.exclusions') }}
</div>
<MultiSelect
v-if="client.pkgListMode === 'category'"
style="width: 30%"
:showToggleAll="false"
v-model="client.hiddenCategories"
v-on:value-change="
client.save();
emit('reload-icons');
"
:options="allCategories"
class="w-full grow"
/>
</div>
<Fieldset :legend="t('pkglist.missing')" v-if="(missing?.length ?? 0) > 0">
<div class="flex items-center" v-for="p in missing">
<ModTitlecard
show-namespace
@ -82,8 +329,28 @@ const missing = computed(() => {
/>
</div>
</Fieldset>
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
<ModListEntry v-for="p in namespace" :pkg="p" />
<Fieldset :legend="t('pkglist.local')">
<ModListEntry v-for="p in local" :pkg="p" />
<Button
rounded
icon="pi pi-plus"
severity="success"
aria-label="install"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
v-on:click="() => (dialogVisible = true)"
/>
</Fieldset>
<Fieldset v-for="[namespace, pkgs] in groupedList" :legend="namespace">
<ModListEntry v-for="p in pkgs" :pkg="p" />
</Fieldset>
<div v-if="empty === true" class="text-3xl fadein"></div>
</template>
<style lang="css" scoped>
.creation-dialog h2 {
margin-top: 0.6em;
margin-bottom: 0.4em;
font-size: 110%;
}
</style>

View File

@ -7,12 +7,16 @@ import LinkButton from './LinkButton.vue';
import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types';
import { hasFeature } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const pkgs = usePkgStore();
const client = useClientStore();
const props = defineProps({
pkg: Object as () => Package,
@ -36,14 +40,15 @@ if (unsupported.value === true && model.value === true) {
<template>
<div class="flex items-center">
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
<ModTitlecard
show-version
show-icon
show-description
:show-namespace="client.pkgListMode !== 'namespace'"
:pkg="pkg"
/>
<UpdateButton :pkg="pkg" />
<span
v-tooltip="
unsupported &&
'This package is currently incompatible with STARTLINER.'
"
>
<span v-tooltip="unsupported && t('store.incompatible')">
<ToggleSwitch
v-if="hasFeature(pkg, Feature.Mod) || unsupported === true"
class="scale-[1.33] shrink-0"

View File

@ -8,6 +8,9 @@ import ModStoreEntry from './ModStoreEntry.vue';
import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores';
import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const pkgs = usePkgStore();
const prf = usePrfStore();
@ -20,7 +23,7 @@ const props = defineProps({
const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', {
game: prf.current?.meta.game,
game: prf.current?.meta.game ?? null,
}).then((list) => {
gameSublist.value = list as string[];
});
@ -43,10 +46,10 @@ const list = () => {
};
const shouldShowRecommended = computed(() => {
if (prf.current!.meta.game === 'ongeki') {
if (prf.current?.meta.game === 'ongeki') {
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
if (prf.current?.meta.game === 'chunithm') {
return (
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
@ -55,21 +58,21 @@ const shouldShowRecommended = computed(() => {
return false;
});
const getRecommendedTooltip = () => {
if (prf.current!.meta.game === 'ongeki') {
const recommendedTooltip = computed(() => {
if (prf.current?.meta.game === 'ongeki') {
return 'segatools-mu3hook';
}
if (prf.current!.meta.game === 'chunithm') {
return 'segatools-chusanhook and mempatcher';
if (prf.current?.meta.game === 'chunithm') {
return 'segatools-chusanhook + mempatcher';
}
return '';
};
});
const installRecommended = () => {
if (prf.current!.meta.game === 'ongeki') {
if (prf.current?.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook');
}
if (prf.current!.meta.game === 'chunithm') {
if (prf.current?.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher');
}
@ -80,15 +83,17 @@ const installRecommended = () => {
<div class="flex gap-4 items-center">
<div class="flex flex-col gap-2">
<div class="flex gap-2">
<div class="grow">Show installed</div>
<div class="grow">{{ t('store.installed') }}</div>
<ToggleSwitch v-model="pkgs.showInstalled" />
</div>
<div class="flex gap-2">
<div class="text-amber-400 grow">Show deprecated</div>
<div class="text-amber-400 grow">
{{ t('store.deprecated') }}
</div>
<ToggleSwitch v-model="pkgs.showDeprecated" />
</div>
<!-- <div class="flex gap-2">
<div class="text-red-400 grow">Show NSFW</div>
<div class="text-red-400 grow">{{ t('store.nsfw') }}</div>
<ToggleSwitch v-model="pkgs.showNSFW" />
</div> -->
</div>
@ -96,7 +101,7 @@ const installRecommended = () => {
<MultiSelect
size="small"
:showToggleAll="false"
placeholder="Include categories"
:placeholder="t('store.includeCategories')"
v-model="pkgs.includeCategories"
:options="[...pkgs.availableCategories]"
class="w-full"
@ -104,7 +109,7 @@ const installRecommended = () => {
<MultiSelect
size="small"
:showToggleAll="false"
placeholder="Exclude categories"
:placeholder="t('store.excludeCategories')"
v-model="pkgs.excludeCategories"
:options="[...pkgs.availableCategories]"
class="w-full"
@ -114,8 +119,8 @@ const installRecommended = () => {
<Divider />
<Button
v-if="shouldShowRecommended"
label="Install recommended packages"
v-tooltip="getRecommendedTooltip"
:label="t('store.installRecommended')"
v-tooltip="recommendedTooltip"
icon="pi pi-plus"
class="mb-3"
@click="installRecommended"

View File

@ -1,9 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue';
import { ref } from 'vue';
import Chip from 'primevue/chip';
import { convertFileSrc } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '../invoke';
import { Feature, Package } from '../types';
import { hasFeature, needsUpdate } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
pkg: Object as () => Package,
@ -14,23 +19,34 @@ const props = defineProps({
showIcon: Boolean,
});
const iconSrc = computed(() => {
const icon = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
const icon = ref('/no-icon.png');
if (icon === undefined) {
return '';
} else if (icon.startsWith('https://')) {
return icon;
const reloadIcons = async () => {
const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
if (src === undefined) {
icon.value = '/no-icon.png';
} else if (src.startsWith('https://')) {
icon.value = src;
} else {
return convertFileSrc(icon);
const convt = convertFileSrc(src);
if (await invoke('file_exists', { path: src })) {
icon.value = convt;
} else {
icon.value = '/no-icon.png';
}
}
});
};
reloadIcons();
listen('reload-icons', reloadIcons);
</script>
<template>
<img
v-if="showIcon"
:src="iconSrc"
:src="icon"
class="self-center rounded-sm"
width="32px"
height="32px"
@ -38,7 +54,7 @@ const iconSrc = computed(() => {
<label class="m-3 align-middle text grow z-5 h-50px">
<div>
<span class="text-lg">
{{ pkg?.name ?? 'Untitled' }}
{{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
</span>
<span
v-if="pkg?.rmt?.deprecated"
@ -89,7 +105,12 @@ const iconSrc = computed(() => {
v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75"
>
by&nbsp;{{ pkg.namespace }}
&nbsp;{{
t('by', { namespace: pkg.namespace }).replaceAll(
' ',
'&nbsp;'
)
}}
</span>
<span class="m-2">
<span

View File

@ -0,0 +1,193 @@
<script setup lang="ts">
import { ComputedRef, computed, ref } from 'vue';
import Button from 'primevue/button';
import Carousel from 'primevue/carousel';
import Dialog from 'primevue/dialog';
import { fromKeycode } from '../keyboard';
import { useClientStore, usePrfStore } from '../stores';
import { prettyPrint } from '../util';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const client = useClientStore();
const props = defineProps({
visible: Boolean,
firstTime: Boolean,
onFinish: Function,
});
interface Datum {
text: string;
image: string;
}
const game = computed(() => prf.current?.meta.game);
const processText = computed(() => (s: string) => {
// Why do I have to do this
s = s
.split('\n')
.map((l) => l.trim())
.join('\n');
if (prf.current!.data.keyboard?.data.enabled) {
const testKey = prf.current!.data.keyboard?.data.test;
const readable = fromKeycode(testKey);
if (readable !== null) {
return s.replace(
'%TESTMENU%',
`${readable} ${t('onboarding.or')} ${t('onboarding.backButton')}`
);
}
}
return s.replace('%TESTMENU%', t('onboarding.backButton'));
});
const loadPage = computed(() => (title: string, messages?: object) => {
return {
text: t(`onboarding.${title}`, {
endlink: '</a>',
black: '<span class="bg-black text-white">',
end: '</span>',
...messages,
}),
image: `help-${title}.png`,
};
});
const data: ComputedRef<Datum[]> = computed(() => {
const res = [];
const [standard, systemProcessing, lever, server, finale] = [
loadPage.value('standard', {
bigblack: '<div class="p-2 mt-1 mb-1 bg-black text-white">',
endbig: '</div>',
}),
loadPage.value('ongeki-system-processing'),
loadPage.value('ongeki-lever'),
loadPage.value('chunithm-server', {
link: '<a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">',
}),
loadPage.value('finale', {
segaguide:
'<a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">',
twotorial: '<a href="https://two-torial.xyz/" target="_blank">',
}),
];
const standardOngeki = {
...standard,
image: '/help-standard-ongeki.png',
};
const standardChunithm = {
...standard,
image: '/help-standard-chunithm.png',
};
const finaleOngeki = {
...finale,
image: '/help-finale-ongeki.png',
};
const finaleChunithm = {
...finale,
image: '/help-finale-chunithm.png',
};
switch (prf.current?.meta.game) {
case 'ongeki':
res.push(systemProcessing);
res.push(standardOngeki);
res.push(lever);
res.push(finaleOngeki);
break;
case 'chunithm':
res.push(standardChunithm);
res.push(server);
res.push(finaleChunithm);
break;
default:
break;
}
return res;
});
const context = ref({
index: 0,
});
const exitLabel = computed(() => {
return props.firstTime === true &&
context.value.index < data.value.length - 1
? t('skip')
: t('close');
});
const page = ref(0);
</script>
<template>
<Dialog
modal
:visible="visible"
:closable="false"
:header="
firstTime
? `It looks like you're running ${game ? prettyPrint(game) : '<game>'} for the first time`
: `${game ? prettyPrint(game) : '<game>'} help`
"
:style="{ width: '760px', scale: client.scaleValue }"
v-on:show="() => ((context.index = 0), (page = 0))"
>
<Carousel
:value="data"
:num-visible="1"
:num-scroll="1"
:context="context"
:page="page"
v-on:update:page="(p) => ((context.index = p), (page = p))"
>
<template #item="slotProps">
<div class="md-container markdown">
<vue-markdown-it
:source="processText(slotProps.data?.text)"
:options="{
typographer: true,
breaks: true,
html: true,
}"
/>
</div>
<div
class="border border-surface-200 dark:border-surface-700 rounded m-2"
>
<img :src="slotProps.data.image" />
</div>
</template>
</Carousel>
<div style="width: 100%; text-align: center">
<Button
v-if="context.index < data.length - 1"
class="m-auto mr-4"
:label="t('next')"
@click="() => (page += 1)"
/>
<Button
class="m-auto"
:label="exitLabel"
@click="() => onFinish && onFinish()"
/>
</div>
</Dialog>
</template>
<style lang="css">
.p-dialog ::-webkit-scrollbar {
display: none;
}
.md-container {
height: 9.5rem;
}
</style>

View File

@ -1,5 +1,6 @@
<script setup lang="ts">
import { computed } from 'vue';
import InputNumber from 'primevue/inputnumber';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import FileEditor from './FileEditor.vue';
@ -13,56 +14,41 @@ import NetworkOptions from './options/Network.vue';
import SegatoolsOptions from './options/Segatools.vue';
import StartlinerOptions from './options/Startliner.vue';
import { usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const audioModel = computed({
const blacklistMinModel = computed({
get() {
return prf.current?.data.mu3_ini?.audio ?? null;
},
set(value: 'Shared' | 'Excl6Ch' | 'Excl2Ch') {
if (prf.current!.data.mu3_ini === undefined) {
prf.current!.data.mu3_ini = {};
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
return null;
}
prf.current!.data.mu3_ini!.audio = value;
return prf.current?.data.mu3_ini?.blacklist[0];
},
set(value: number) {
prf.current!.data.mu3_ini!.blacklist = [
value,
prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
];
},
});
// const blacklistMinModel = computed({
// get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null;
// }
// return prf.current?.data.mu3_ini?.blacklist[0];
// },
// set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) {
// prf.current!.data.mu3_ini = {};
// }
// prf.current!.data.mu3_ini!.blacklist = [
// value,
// prf.current!.data.mu3_ini!.blacklist?.[1] ?? 19999,
// ];
// },
// });
// const blacklistMaxModel = computed({
// get() {
// if (prf.current?.data.mu3_ini?.blacklist === undefined) {
// return null;
// }
// return prf.current?.data.mu3_ini?.blacklist[1];
// },
// set(value: number) {
// if (prf.current!.data.mu3_ini === undefined) {
// prf.current!.data.mu3_ini = {};
// }
// prf.current!.data.mu3_ini!.blacklist = [
// prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
// value,
// ];
// },
// });
const blacklistMaxModel = computed({
get() {
if (prf.current?.data.mu3_ini?.blacklist === undefined) {
return null;
}
return prf.current?.data.mu3_ini.blacklist[1];
},
set(value: number) {
prf.current!.data.mu3_ini!.blacklist = [
prf.current!.data.mu3_ini!.blacklist?.[0] ?? 10000,
value,
];
},
});
prf.reload();
</script>
@ -75,9 +61,9 @@ prf.reload();
<MiscOptions />
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'chunithm'"
v-if="prf.current?.meta.game === 'chunithm'"
>
<OptionRow title="Saekawa config">
<OptionRow :title="t('cfg.extensions.saekawa')">
<FileEditor
filename="saekawa.toml"
promptname="saekawa config file"
@ -85,51 +71,76 @@ prf.reload();
/> </OptionRow
></OptionCategory>
<OptionCategory
title="Extensions"
v-if="prf.current!.meta.game === 'ongeki'"
:title="t('cfg.extensions.title')"
v-if="prf.current?.meta.game === 'ongeki'"
>
<OptionRow title="Inohara config">
<OptionRow :title="t('cfg.extensions.inohara')">
<FileEditor
filename="inohara.cfg"
promptname="inohara config file"
extension="cfg"
/>
</OptionRow>
<OptionRow title="BepInEx console">
<OptionRow :title="t('cfg.extensions.bepInExConsole')">
<!-- @vue-expect-error -->
<ToggleSwitch v-model="prf.current!.data.bepinex.console" />
</OptionRow>
<OptionRow
title="Audio mode"
tooltip="Exclusive 2-channel mode requires a patch"
:title="t('cfg.extensions.audioMode')"
:tooltip="t('cfg.extensions.audioTooltip')"
>
<SelectButton
v-model="audioModel"
v-model="prf.current!.data.mu3_ini!.audio"
:options="[
{ title: 'Shared', value: 'Shared' },
{ title: 'Exclusive 6-channel', value: 'Excl6Ch' },
{ title: 'Exclusive 2-channel', value: 'Excl2Ch' },
{ title: t('cfg.extensions.audioShared'), value: 'Shared' },
{ title: t('cfg.extensions.audio6Ch'), value: 'Excl6Ch' },
{ title: t('cfg.extensions.audio2Ch'), value: 'Excl2Ch' },
]"
:allow-empty="true"
:allow-empty="false"
option-label="title"
option-value="value"
/></OptionRow>
<!-- <OptionRow
<OptionRow
:title="t('cfg.extensions.sampleRate')"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-ExclusiveAudio'
)
"
>
<SelectButton
v-model="prf.current!.data.mu3_ini!.sample_rate"
:disabled="prf.current!.data.mu3_ini!.audio === 'Shared'"
:options="[
{ title: '44.1KHz', value: 44100 },
{ title: '48KHz', value: 48000 },
{ title: '96KHz', value: 96000 },
{ title: '192KHz', value: 192000 },
]"
:allow-empty="false"
option-label="title"
option-value="value"
/></OptionRow>
<OptionRow
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-Blacklist')
"
class="number-input"
title="Song ID Blacklist"
tooltip="Requires a patch"
:title="t('cfg.extensions.blacklist')"
:tooltip="t('cfg.extensions.blacklistTooltip')"
><InputNumber
class="shrink"
size="small"
:min="10000"
:min="9000"
:max="99999"
placeholder="10000"
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMinModel" />
x
~
<InputNumber
class="shrink"
size="small"
@ -139,7 +150,36 @@ prf.reload();
:use-grouping="false"
:allow-empty="false"
v-model="blacklistMaxModel"
/></OptionRow> -->
/></OptionRow>
<OptionRow
class="number-input"
title="GP"
v-if="
prf.current?.data.mods.includes('7EVENDAYSHOLIDAYS-DisableGP')
"
><InputNumber
class="shrink"
size="small"
:min="0"
:max="9999"
:use-grouping="false"
:allow-empty="false"
v-model="prf.current!.data.mu3_ini!.gp"
/>
</OptionRow>
<OptionRow
:title="t('cfg.extensions.bonusTracks')"
:tooltip="t('cfg.extensions.bonusTracksTooltip')"
v-if="
prf.current?.data.mods.includes(
'7EVENDAYSHOLIDAYS-UnlockAllMusic'
)
"
>
<ToggleSwitch
v-model="prf.current!.data.mu3_ini!.enable_bonus_tracks"
/>
</OptionRow>
</OptionCategory>
<KeyboardOptions />
<StartlinerOptions />

View File

@ -1,9 +1,14 @@
<script setup lang="ts">
import { computed } from 'vue';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionRow from './OptionRow.vue';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
import { useI18n } from 'vue-i18n';
const { t, te } = useI18n();
const prf = usePrfStore();
@ -23,20 +28,40 @@ const setNumber = (key: string, val: number) => {
}
};
defineProps({
const props = defineProps({
patch: Object as () => Patch,
});
// One day, I will repent
const hexModel = computed({
get() {
const hex = (prf.current!.data.patches[props.patch!.id!] as any)?.hex;
if (hex !== undefined) {
return new TextDecoder().decode(new Int8Array(hex).buffer);
} else {
return 'FREE PLAY';
}
},
set(value: string) {
(prf.current!.data.patches[props.patch!.id!] as any) = {
hex: new TextEncoder().encode(value),
};
},
});
// Doesn't need to be reactive
const nameKey = `patch.${props.patch?.id}`;
const name = te(nameKey) ? t(nameKey) : props.patch?.name;
const tooltipKey = `patch.${props.patch?.id}-tooltip`;
const tooltip = te(tooltipKey) ? t(tooltipKey) : props.patch?.tooltip;
</script>
<template>
<OptionRow
:title="patch?.name"
:tooltip="patch?.tooltip"
:greytext="patch?.id"
>
<OptionRow :title="name" :tooltip="tooltip" :greytext="patch?.id">
<ToggleSwitch
v-if="patch?.type === undefined"
:model-value="prf.current!.data.patches[patch!.id!] !== undefined"
:model-value="prf.current!.data.patches?.[patch!.id!] !== undefined"
@update:model-value="(v: boolean) => toggleUnary(patch!.id!, v)"
/>
<InputNumber
@ -50,5 +75,6 @@ defineProps({
:max="patch?.max"
:placeholder="(patch?.default ?? 0).toString()"
/>
<InputText v-else-if="patch?.type === 'hex'" v-model="hexModel" />
</OptionRow>
</template>

View File

@ -1,11 +1,15 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
// import Select from 'primevue/select';
import * as path from '@tauri-apps/api/path';
import OptionCategory from './OptionCategory.vue';
import PatchEntry from './PatchEntry.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { Patch } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
@ -29,21 +33,22 @@ invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
target: amd,
})) as Patch[];
})();
const errorMessage =
"No compatible patches found. Make sure you're using unpacked and unpatched files.";
</script>
<template>
<OptionCategory title="chusanApp.exe" always-found>
<OptionCategory
v-if="prf.current?.meta.game === 'chunithm'"
title="chusanApp.exe"
always-found
>
<PatchEntry
v-if="gamePatches !== null"
v-for="p in gamePatches"
:patch="p"
/>
<div v-if="gamePatches === null">Loading...</div>
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
<div v-if="gamePatches !== null && gamePatches.length === 0">
{{ errorMessage }}
{{ t('patch.noneFound') }}
</div>
</OptionCategory>
<OptionCategory title="amdaemon.exe" always-found>
@ -52,9 +57,22 @@ const errorMessage =
v-for="p in amdPatches"
:patch="p"
/>
<div v-if="gamePatches === null">Loading...</div>
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
<div v-if="amdPatches !== null && amdPatches.length === 0">
{{ errorMessage }}
{{ t('patch.noneFound') }}
<!-- <br />
<Select
class="mt-3"
style="width: 400px"
:options="[
{},
{},
]"
:placeholder="t('patch.forceLoad')"
size="small"
option-label="title"
option-value="value"
></Select> -->
</div>
</OptionCategory>
</template>

View File

@ -1,33 +1,224 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import * as path from '@tauri-apps/api/path';
import { open } from '@tauri-apps/plugin-dialog';
import ProfileListEntry from './ProfileListEntry.vue';
import { usePrfStore } from '../stores';
import { invoke } from '../invoke';
import { useClientStore, useGeneralStore, usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const client = useClientStore();
const general = useGeneralStore();
const hasChunithm = ref(false);
const exportVisible = ref(false);
const exportKeychip = ref(false);
const files = new Set<string>();
(async () => {
hasChunithm.value = (
(await invoke('list_platform_capabilities')) as string[]
).includes('chunithm');
})();
const exportTemplate = async () => {
const fl = [...files.values()];
exportVisible.value = false;
await invoke('export_profile', {
exportKeychip: exportKeychip.value,
files: fl,
});
await invoke('open_file', {
path: await path.join(await general.configDir, 'exports'),
});
};
const fileList = {
ongeki: ['aime.txt', 'inohara.cfg', 'mu3.ini', 'segatools-base.ini'],
chunithm: ['aime.txt', 'saekawa.toml', 'segatools-base.ini'],
};
const fileListCurrent: Ref<string[]> = ref([]);
const recalcFileList = async () => {
const res: string[] = [];
files.clear();
for (const idx in fileList[prf.current!.meta.game]) {
const f = fileList[prf.current!.meta.game][idx];
const p = await path.join(await prf.configDir, f);
if (await invoke('file_exists', { path: p })) {
res.push(f);
files.add(f);
}
}
fileListCurrent.value = res;
};
const openExportDialog = async () => {
await recalcFileList();
exportVisible.value = true;
};
const importPick = async () => {
const res = await open({
multiple: false,
directory: false,
filters: [
{
name: t('profile.template'),
extensions: ['zip'],
},
],
});
if (res != null) {
await invoke('import_profile', { path: res });
await prf.reloadList();
}
};
</script>
<template>
<div v-if="prf.list.length === 0">
Welcome to STARTLINER! Start by creating a profile.
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
label="O.N.G.E.K.I. profile"
icon="pi pi-plus"
class="ongeki-button profile-button"
@click="() => prf.create('ongeki')"
/>
<Button
label="CHUNITHM profile"
icon="pi pi-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
<div v-for="p in prf.list">
<ProfileListEntry :p="p" />
<Dialog
modal
:visible="exportVisible"
:closable="false /*this shit doesn't work */"
:header="`${t('profile.export')} ${prf.current?.meta.name}`"
:style="{ width: '300px', scale: client.scaleValue }"
>
<div class="flex flex-col gap-4">
<div class="flex flex-row">
<div class="grow">{{ t('profile.export') }} keychip</div>
<ToggleSwitch v-model="exportKeychip" />
</div>
<div class="flex flex-row" v-for="f in fileListCurrent">
<div class="grow">{{ t('profile.export') }} {{ f }}</div>
<ToggleSwitch
:model-value="true"
@update:model-value="
(v) => {
if (v === true) {
files.add(f);
} else {
files.delete(f);
}
}
"
/>
</div>
<div style="width: 100%; text-align: center">
<Button
class="m-auto mr-3"
style="width: 80px"
label="OK"
@click="() => exportTemplate()"
/>
<Button
class="m-auto"
style="width: 80px"
label="Cancel"
@click="() => (exportVisible = false)"
/>
</div>
</div>
</Dialog>
<div style="float: left">
<div v-if="prf.list.length === 0">
{{ t('profile.welcome') }}
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
:label="t('profile.create', { game: t('game.ongeki') })"
icon="pi pi-file-plus"
class="ongeki-button profile-button"
@click="() => prf.create('ongeki')"
/>
<Button
v-if="hasChunithm"
:label="t('profile.create', { game: t('game.chunithm') })"
icon="pi pi-file-plus"
class="chunithm-button profile-button"
@click="() => prf.create('chunithm')"
/>
</div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button
:label="t('profile.importTemplate')"
icon="pi pi-file-import"
class="import-button profile-button"
@click="() => importPick()"
/>
<Button
:disabled="prf.current === null"
:label="t('profile.exportTemplate')"
icon="pi pi-file-export"
class="profile-button"
@click="() => openExportDialog()"
/>
</div>
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
<div v-for="p in prf.list">
<ProfileListEntry :p="p" />
</div>
</div>
</div>
<div style="float: right" class="mr-5 mt-3 flex flex-col gap-4 items-end">
<div>
<div class="pi pi-language mr-2"></div>
<Select
:model-value="client.locale"
@update:model-value="async (v) => await client.setLocale(v)"
style="width: 200px"
:options="[
{ title: 'English', value: 'en' },
// { title: '日本語', value: 'ja' },
{ title: 'Polski', value: 'pl' },
]"
size="small"
option-label="title"
option-value="value"
></Select>
</div>
<SelectButton
style="height: 50px"
v-model="client.scaleModel"
:options="[
{ title: 'S', size: '0.8em', value: 's' },
{ title: 'M', size: '1.0em', value: 'm' },
{ title: 'L', size: '1.2em', value: 'l' },
{ title: 'XL', size: '1.4em', value: 'xl' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
><template #option="slotProps">
<div :style="{ fontSize: slotProps.option.size }">
{{ slotProps.option.title }}
</div>
</template></SelectButton
>
<SelectButton
style="height: 50px"
:model-value="client.theme"
@update:model-value="(v) => client.setTheme(v)"
:options="[
{ title: 'System', value: 'system', icon: 'pi pi-home' },
{ title: 'Light', value: 'light', icon: 'pi pi-sun' },
{ title: 'Dark', value: 'dark', icon: 'pi pi-moon' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
><template #option="slotProps">
<div :class="slotProps.option.icon"></div> </template
></SelectButton>
</div>
</template>
@ -57,4 +248,14 @@ const prf = usePrfStore();
background-color: var(--p-yellow-300) !important;
border-color: var(--p-yellow-300) !important;
}
.import-button {
background-color: var(--p-purple-400) !important;
border-color: var(--p-purple-400) !important;
}
.import-button:hover,
.import-button:active {
background-color: var(--p-purple-300) !important;
border-color: var(--p-purple-300) !important;
}
</style>

View File

@ -7,6 +7,9 @@ import * as path from '@tauri-apps/api/path';
import { invoke } from '../invoke';
import { useGeneralStore, usePrfStore } from '../stores';
import { ProfileMeta } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const general = useGeneralStore();
const prf = usePrfStore();
@ -60,11 +63,23 @@ const deleteProfile = async () => {
const promptDeleteProfile = async () => {
confirmDialog.require({
message: `Are you sure you want to delete ${props.p?.game}-${props.p?.name}?`,
header: 'Delete profile',
message: t('profile.reallyDelete', {
profile: `${props.p?.game}-${props.p?.name}`,
}),
header: t('profile.delete'),
accept: deleteProfile,
});
};
const dataExists = ref(false);
general.dataDir.then((dataDir) =>
path
.join(dataDir, `profile-${props.p!.game}-${props.p!.name}`)
.then(async (p) => {
dataExists.value = await invoke('file_exists', { path: p });
})
);
</script>
<template>
@ -125,20 +140,43 @@ const promptDeleteProfile = async () => {
/>
<Button
rounded
icon="pi pi-folder"
icon="pi pi-cog"
severity="help"
aria-label="open-directory"
aria-label="open-config-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="
path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
.then(async (path) => {
if (await invoke('file_exists', { path })) {
async () =>
path
.join(
await general.configDir,
`profile-${p!.game}-${p!.name}`
)
.then(async (path) => {
await invoke('open_file', { path });
}
})
})
"
/>
<Button
v-if="dataExists"
rounded
icon="pi pi-folder"
severity="help"
aria-label="open-data-directory"
size="small"
class="self-center"
style="width: 2rem; height: 2rem"
@click="
async () =>
path
.join(
await general.dataDir,
`profile-${p!.game}-${p!.name}`
)
.then(async (path) => {
await invoke('open_file', { path });
})
"
/>
</div>

View File

@ -5,10 +5,15 @@ import ContextMenu from 'primevue/contextmenu';
import { useConfirm } from 'primevue/useconfirm';
import { listen } from '@tauri-apps/api/event';
import { getCurrentWindow } from '@tauri-apps/api/window';
import Onboarding from './Onboarding.vue';
import { invoke } from '../invoke';
import { usePrfStore } from '../stores';
import { useClientStore, usePrfStore } from '../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const client = useClientStore();
const confirmDialog = useConfirm();
type StartStatus = 'ready' | 'preparing' | 'running';
@ -22,22 +27,22 @@ const startline = async (force: boolean, refresh: boolean) => {
if (start_check.length > 0) {
const message = start_check.map((o) => {
if ('MissingRemotePackage' in o) {
return `Package missing: ${o.MissingRemotePackage}`;
return `${t('start.error.package')}: ${o.MissingRemotePackage}`;
} else if ('MissingLocalPackage' in o) {
return `Package missing: ${o.MissingLocalPackage}`;
return `${t('start.error.package')}: ${o.MissingLocalPackage}`;
} else if ('MissingDependency' in o) {
return `Dependency missing: ${(o.MissingDependency as string[]).join(' ')}`;
return `${t('start.error.dependency')}: ${(o.MissingDependency as string[]).join(' ')}`;
} else if ('MissingTool' in o) {
return `Tool missing: ${o.MissingTool}`;
return `${t('start.error.tool')}: ${o.MissingTool}`;
} else {
return 'Unknown error';
return t('start.error.unknown');
}
});
confirmDialog.require({
message: message.join('\n'),
header: 'Start check failed',
acceptLabel: 'Run anyway',
rejectLabel: 'Cancel',
header: t('start.failed'),
acceptLabel: t('start.accept'),
rejectLabel: t('cancel'),
accept: () => {
startline(true, refresh);
},
@ -60,16 +65,16 @@ const kill = async () => {
const disabledTooltip = computed(() => {
if (prf.current?.data.sgt.target.length === 0) {
return 'The game path must be specified';
return t('start.tooltip.game');
}
if (prf.current?.data.sgt.amfs.length === 0) {
return 'The amfs path must be specified';
return t('start.tooltip.amfs');
}
if (
prf.current?.data.sgt.hook === null ||
prf.current?.data.sgt.hook === undefined
) {
return 'A segatools hook package is necessary';
return t('start.tooltip.segatools');
}
return null;
});
@ -94,32 +99,104 @@ const createShortcut = async () => {
}
};
const menuItems = [
{
label: 'Refresh and start',
icon: 'pi pi-sync',
command: async () => await startline(false, true),
},
{
label: 'Start unchecked',
icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false),
},
{
label: 'Create desktop shortcut',
icon: 'pi pi-link',
command: createShortcut,
},
];
const hasShortcut = ref(false);
(async () => {
hasShortcut.value = (
(await invoke('list_platform_capabilities')) as string[]
).includes('shortcut');
})();
const menuItems = computed(() => {
let base = [
{
label: t('start.button.unchecked'),
icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false),
},
];
let baseTail = [
{
label: t('start.button.help'),
icon: 'pi pi-question-circle',
command: () => {
onboardingFirstTime.value = false;
onboardingVisible.value = true;
},
},
];
if (prf.current === null) {
return [];
}
if (hasShortcut.value === true) {
base = [
...base,
{
label: t('start.button.shortcut'),
icon: 'pi pi-link',
command: createShortcut,
},
];
}
if (prf.current.meta.game === 'chunithm') {
return [...base, ...baseTail];
}
if (prf.current.meta.game === 'ongeki') {
return [
{
label: t('start.button.refresh'),
icon: 'pi pi-sync',
command: async () => await startline(false, true),
},
...base,
{
label: t('start.button.cache'),
icon: 'pi pi-trash',
command: async () => await invoke('clear_cache'),
},
...baseTail,
];
}
});
const menu = ref();
const showContextMenu = (event: Event) => {
event.preventDefault();
menu.value.show(event);
};
const onboardingVisible = ref(false);
const onboardingFirstTime = ref(false);
const tryStart = () => {
const game = prf.current?.meta.game;
if (game !== undefined) {
if (client.onboarded.includes(game)) {
startline(false, false);
} else {
onboardingVisible.value = true;
onboardingFirstTime.value = true;
client.setOnboarded(game);
}
}
};
</script>
<template>
<Onboarding
:visible="onboardingVisible"
:first-time="onboardingFirstTime"
:on-finish="
() => {
onboardingVisible = false;
if (onboardingFirstTime === true) {
startline(false, false);
}
}
"
/>
<ContextMenu ref="menu" :model="menuItems" />
<Button
v-if="startStatus === 'ready'"
@ -130,14 +207,14 @@ const showContextMenu = (event: Event) => {
aria-label="start"
size="small"
class="m-2.5"
@click="startline(false, false)"
@click="tryStart"
@contextmenu="showContextMenu"
/>
<Button
v-else-if="startStatus === 'preparing'"
disabled
icon="pi pi-spin pi-spinner"
label="START"
:label="t('start.button.start')"
aria-label="start"
size="small"
class="m-2.5"
@ -146,7 +223,7 @@ const showContextMenu = (event: Event) => {
v-else
:disabled="false"
icon="pi pi-ban"
label="STOP"
:label="t('start.button.stop')"
aria-label="stop"
size="small"
class="m-2.5"

View File

@ -20,17 +20,15 @@ const install = async () => {
});
} catch (err) {
if (props.pkg !== undefined) {
props.pkg.js.busy = false;
props.pkg.js.downloading = false;
}
}
//if (rv === 'Deferred') { /* download progress */ }
};
</script>
<template>
<Button
v-if="needsUpdate(pkg) && !pkg?.js.busy"
v-if="needsUpdate(pkg) && !pkg?.js.downloading"
rounded
icon="pi pi-download"
severity="success"

View File

@ -12,6 +12,9 @@ import { invoke } from '../../invoke';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { hasFeature, pkgKey } from '../../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const pkgs = usePkgStore();
const prf = usePrfStore();
@ -60,14 +63,18 @@ load();
<template>
<OptionCategory title="Aime">
<OptionRow
title="Aime type"
tooltip="Additional Aime plugins can be downloaded from the package store."
:title="t('cfg.aime.type')"
:tooltip="
t('cfg.segatools.installTooltip', {
thing: t('cfg.aime.modules'),
})
"
>
<Select
v-model="prf.current!.data.sgt.aime"
:options="[
{ title: 'hardware', value: 'Disabled' },
{ title: 'segatools built-in emulation', value: 'BuiltIn' },
{ title: t('cfg.hardware'), value: 'Disabled' },
{ title: t('cfg.segatools.builtIn'), value: 'BuiltIn' },
...pkgs.byFeature(Feature.Aime).map((p) => {
return {
title: pkgKey(p),
@ -83,8 +90,8 @@ load();
></Select>
</OptionRow>
<OptionRow
title="Aime code"
tooltip="Only applicable with the segatools built-in emulation or with compatible third-party packages"
:title="t('cfg.aime.code')"
:tooltip="t('cfg.aime.codeTooltip')"
v-if="prf.current!.data.sgt.aime !== 'Disabled'"
>
<InputText
@ -97,7 +104,7 @@ load();
/>
</OptionRow>
<div v-if="prf.current!.data.sgt.aime?.hasOwnProperty('AMNet')">
<OptionRow title="Server name">
<OptionRow :title="t('cfg.aime.serverName')">
<InputText
class="shrink"
size="small"
@ -106,7 +113,7 @@ load();
v-model="prf.current!.data.sgt.amnet.name"
/>
</OptionRow>
<OptionRow title="Server address">
<OptionRow :title="t('cfg.network.address')">
<InputText
class="shrink"
size="small"
@ -116,22 +123,21 @@ load();
/>
</OptionRow>
<OptionRow
title="Use AiMeDB for physical cards"
tooltip="Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab."
:title="t('cfg.aime.aimedb')"
:tooltip="t('cfg.aime.aimedbTooltip')"
>
<ToggleSwitch v-model="prf.current!.data.sgt.amnet.physical" />
</OptionRow>
</div>
<OptionRow
title="Aime serial port"
tooltip="Ports can be checked in Devices and Printers or at googlechromelabs.github.io/serial-terminal
For AIC Pico, the AIME port should be selected."
:title="t('cfg.aime.serialPort')"
:tooltip="t('cfg.aime.serialPortTooltip')"
v-if="prf.current!.data.sgt.aime === 'Disabled'"
>
<Select
v-model="prf.current!.data.sgt.aime_port"
:options="[
{ title: 'default', value: null },
{ title: t('default'), value: null },
...Object.entries(coms ?? {}).map(([title, value]) => {
return {
title,
@ -139,7 +145,7 @@ load();
};
}),
]"
placeholder="default"
:placeholder="t('default')"
option-label="title"
option-value="value"
></Select>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import { Ref, computed, onMounted, ref } from 'vue';
import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
@ -8,11 +8,14 @@ import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { invoke } from '../../invoke';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const capabilities: Ref<string[]> = ref([]);
const displayList: Ref<{ title: string; value: string }[]> = ref([
{
title: 'Primary',
title: t('cfg.display.primary'),
value: 'default',
},
]);
@ -25,7 +28,7 @@ const extraDisplayOptionsDisabled = computed(() => {
const loadDisplays = () => {
const newList = [
{
title: 'Primary',
title: t('cfg.display.primary'),
value: 'default',
},
];
@ -62,19 +65,23 @@ const loadDisplays = () => {
.catch(() => {});
};
loadDisplays();
onMounted(() => {
loadDisplays();
});
const game = prf.current!.meta.game;
const isVertical = game === 'ongeki';
const adjustableRez = game === 'ongeki';
const canSkipPrimarySwitch = game === 'ongeki';
const game = computed(() => prf.current!.meta.game);
const isVertical = computed(() => prf.current!.meta.game === 'ongeki');
const adjustableRez = computed(() => prf.current!.meta.game === 'ongeki');
const canSkipPrimarySwitch = computed(
() => prf.current!.meta.game === 'ongeki'
);
</script>
<template>
<OptionCategory title="Display">
<OptionCategory :title="t('cfg.display.title')">
<OptionRow
v-if="capabilities.includes('display')"
title="Target display"
:title="t('cfg.display.target')"
>
<Select
v-model="prf.current!.data.display.target"
@ -87,7 +94,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
</OptionRow>
<OptionRow
class="number-input"
title="Game resolution"
:title="t('cfg.display.resolution')"
v-if="adjustableRez"
>
<InputNumber
@ -108,13 +115,13 @@ const canSkipPrimarySwitch = game === 'ongeki';
v-model="prf.current!.data.display.rez[1]"
/>
</OptionRow>
<OptionRow title="Display mode">
<OptionRow :title="t('cfg.display.mode')">
<SelectButton
v-model="prf.current!.data.display.mode"
:options="[
{ title: 'Window', value: 'Window' },
{ title: 'Borderless window', value: 'Borderless' },
{ title: 'Fullscreen', value: 'Fullscreen' },
{ title: t('cfg.display.window'), value: 'Window' },
{ title: t('cfg.display.borderless'), value: 'Borderless' },
{ title: t('cfg.display.fullscreen'), value: 'Fullscreen' },
]"
:allow-empty="false"
option-label="title"
@ -122,7 +129,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Display rotation"
:title="t('cfg.display.rotation')"
v-if="capabilities.includes('display')"
>
<SelectButton
@ -130,12 +137,18 @@ const canSkipPrimarySwitch = game === 'ongeki';
:options="
isVertical
? [
{ title: 'Portrait', value: 90 },
{ title: 'Portrait (flipped)', value: 270 },
{ title: t('cfg.display.portrait'), value: 90 },
{
title: `${t('cfg.display.portrait')} (${t('cfg.display.flipped')})`,
value: 270,
},
]
: [
{ title: 'Landscape', value: 0 },
{ title: 'Landscape (flipped)', value: 180 },
{ title: t('cfg.display.landscape'), value: 0 },
{
title: `${t('cfg.display.landscape')} (${t('cfg.display.flipped')})`,
value: 180,
},
]
"
:allow-empty="true"
@ -147,7 +160,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
<OptionRow
v-if="capabilities.includes('display')"
class="number-input"
title="Refresh Rate"
:title="t('cfg.display.refreshRate')"
>
<InputNumber
v-if="game === 'ongeki'"
@ -173,9 +186,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Borderless fullscreen"
:title="t('cfg.display.borderlessFullscreen')"
v-if="capabilities.includes('display')"
tooltip="Match display resolution with the game."
:tooltip="t('cfg.display.borderlessFullscreenTooltip')"
>
<ToggleSwitch
:disabled="
@ -186,28 +199,31 @@ const canSkipPrimarySwitch = game === 'ongeki';
/>
</OptionRow>
<OptionRow
title="Skip switching primary display"
:title="t('cfg.display.dontSwitchPrimary')"
v-if="
capabilities.includes('display') &&
prf.current?.data.display.target !== 'default' &&
(prf.current!.data.display.dont_switch_primary ||
displayList.length > 2) &&
canSkipPrimarySwitch
!capabilities.includes('display') ||
(prf.current?.data.display.target !== 'default' &&
(prf.current!.data.display.dont_switch_primary ||
displayList.length > 2) &&
canSkipPrimarySwitch)
"
dangerous-tooltip="Only enable this option if switching the primary display causes issues. The monitors must have a matching refresh rate."
:dangerous-tooltip="t('cfg.display.dontSwitchPrimaryTooltip')"
>
<ToggleSwitch
:disabled="extraDisplayOptionsDisabled"
:disabled="
extraDisplayOptionsDisabled &&
capabilities.includes('display')
"
v-model="prf.current!.data.display.dont_switch_primary"
/>
</OptionRow>
<OptionRow
title="Display index"
:title="t('cfg.display.index')"
class="number-input"
v-if="
capabilities.includes('display') &&
prf.current?.data.display.target !== 'default' &&
prf.current!.data.display.dont_switch_primary
!capabilities.includes('display') ||
(prf.current?.data.display.target !== 'default' &&
prf.current!.data.display.dont_switch_primary)
"
>
<InputNumber
@ -216,8 +232,12 @@ const canSkipPrimarySwitch = game === 'ongeki';
:min="game === 'chunithm' ? 0 : 1"
:max="32"
:use-grouping="false"
placeholder="1"
v-model="prf.current!.data.display.monitor_index_override"
:disabled="extraDisplayOptionsDisabled"
:disabled="
extraDisplayOptionsDisabled &&
capabilities.includes('display')
"
:allow-empty="true"
/>
</OptionRow>

View File

@ -5,27 +5,27 @@ import KeyboardKey from '../KeyboardKey.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Keyboard">
<OptionRow
title="Enable"
tooltip="Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)"
>
<OptionCategory :title="t('cfg.keyboard.title')">
<OptionRow :title="t('enable')" :tooltip="t('cfg.keyboard.tooltip')">
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
</OptionRow>
<OptionRow
title="Lever mode"
:title="t('cfg.keyboard.leverMode')"
v-if="prf.current!.data.keyboard!.game === 'Ongeki'"
>
<SelectButton
v-model="prf.current!.data.keyboard!.data.use_mouse"
:options="[
{ title: 'XInput', value: false },
{ title: 'Mouse', value: true },
{ title: t('cfg.keyboard.mouse'), value: true },
]"
:allow-empty="false"
option-label="title"
@ -95,7 +95,7 @@ const prf = usePrfStore();
</div>
</div>
<div v-if="prf.current?.meta.game === 'chunithm'">
<div class="absolute left-1/2 top-1/5">
<div class="absolute left-9/17 top-1/12">
<div
class="flex flex-row flex-nowrap gap-2 self-center w-full"
>
@ -108,6 +108,7 @@ const prf = usePrfStore();
button="ir"
:index="idx - 1"
:tooltip="`ir${idx}`"
tall
small
color="rgba(0, 255, 0, 0.2)"
/>
@ -124,7 +125,7 @@ const prf = usePrfStore();
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 16 - i)"
.map((_, i) => 32 - 2 * i - 1)"
>
<KeyboardKey
button="cell"
@ -142,7 +143,7 @@ const prf = usePrfStore();
<div
v-for="idx in Array(16)
.fill(0)
.map((_, i) => 32 - i)"
.map((_, i) => 32 - 2 * i)"
>
<KeyboardKey
button="cell"

View File

@ -4,18 +4,24 @@ import FileEditor from '../FileEditor.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Misc">
<OptionRow title="OpenSSL bug workaround for Intel ≥10th gen">
<OptionCategory :title="t('cfg.misc.title')">
<OptionRow
:title="t('cfg.misc.intel')"
:dangerous-tooltip="t('cfg.misc.intelTooltip')"
>
<ToggleSwitch v-model="prf.current!.data.sgt.intel" />
</OptionRow>
<OptionRow
title="More segatools options"
tooltip="Advanced options not covered by STARTLINER"
:title="t('cfg.misc.other')"
:tooltip="t('cfg.misc.otherTooltip')"
>
<!-- <Button icon="pi pi-refresh" size="small" /> -->
<FileEditor filename="segatools-base.ini" />

View File

@ -6,18 +6,21 @@ import FilePicker from '../FilePicker.vue';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { usePrfStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
</script>
<template>
<OptionCategory title="Network">
<OptionRow title="Network type">
<OptionCategory :title="t('cfg.network.title')">
<OptionRow :title="t('cfg.network.type')">
<SelectButton
v-model="prf.current!.data.network.network_type"
:options="[
{ title: 'Remote', value: 'Remote' },
{ title: 'Local (ARTEMiS)', value: 'Artemis' },
{ title: t('cfg.network.remote'), value: 'Remote' },
{ title: t('cfg.network.localArtemis'), value: 'Artemis' },
]"
:allow-empty="false"
option-label="title"
@ -25,8 +28,8 @@ const prf = usePrfStore();
/>
</OptionRow>
<OptionRow
v-if="prf.current!.data.network.network_type == 'Artemis'"
title="ARTEMiS path"
v-if="prf.current!.data.network.network_type === 'Artemis'"
:title="t('cfg.network.artemisPath')"
>
<FilePicker
:directory="false"
@ -47,7 +50,7 @@ const prf = usePrfStore();
</OptionRow> -->
<OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Server address"
:title="t('cfg.network.address')"
>
<InputText
class="shrink"
@ -58,7 +61,7 @@ const prf = usePrfStore();
/> </OptionRow
><OptionRow
v-if="prf.current!.data.network.network_type == 'Remote'"
title="Keychip"
:title="t('cfg.network.keychip')"
>
<InputText
class="shrink"
@ -67,7 +70,7 @@ const prf = usePrfStore();
placeholder="A123-01234567890"
v-model="prf.current!.data.network.keychip"
/> </OptionRow
><OptionRow title="Subnet">
><OptionRow :title="t('cfg.network.subnet')">
<InputText
class="shrink"
size="small"
@ -76,7 +79,7 @@ const prf = usePrfStore();
v-model="prf.current!.data.network.subnet"
/>
</OptionRow>
<OptionRow title="Address suffix">
<OptionRow :title="t('cfg.network.addrSuffix')">
<InputNumber
class="shrink"
size="small"

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Ref, computed, ref } from 'vue';
import Select from 'primevue/select';
import { useConfirm } from 'primevue/useconfirm';
import { emit } from '@tauri-apps/api/event';
@ -11,11 +11,22 @@ import { invoke } from '../../invoke';
import { usePkgStore, usePrfStore } from '../../stores';
import { Feature } from '../../types';
import { pkgKey } from '../../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore();
const pkgs = usePkgStore();
const confirmDialog = useConfirm();
const capabilities: Ref<string[]> = ref([]);
invoke('list_platform_capabilities').then(async (v: unknown) => {
if (Array.isArray(v)) {
capabilities.value.push(...v);
}
});
const names = computed(() => {
switch (prf.current?.meta.game) {
case 'ongeki': {
@ -32,8 +43,6 @@ const names = computed(() => {
io: 'chuniio',
};
}
case undefined:
throw new Error('Option tab without a profile');
}
});
@ -54,16 +63,16 @@ const checkSegatoolsIni = async (target: string) => {
</script>
<template>
<OptionCategory title="General">
<OptionCategory :title="t('cfg.segatools.general')">
<OptionRow
:title="names.exe"
tooltip="STARTLINER expects unpacked executables put into otherwise clean data."
:title="names?.exe"
:tooltip="t('cfg.segatools.targetTooltip')"
>
<FilePicker
:directory="false"
:promptname="names.exe"
:promptname="names?.exe"
extension="exe"
:value="prf.current!.data.sgt.target"
:value="prf.current?.data.sgt.target"
:callback="
(value: string) => (
(prf.current!.data.sgt.target = value),
@ -77,7 +86,7 @@ const checkSegatoolsIni = async (target: string) => {
<FilePicker
:directory="true"
placeholder="amfs"
:value="prf.current!.data.sgt.amfs"
:value="prf.current?.data.sgt.amfs"
:callback="
(value: string) => (prf.current!.data.sgt.amfs = value)
"
@ -103,8 +112,12 @@ const checkSegatoolsIni = async (target: string) => {
></FilePicker>
</OptionRow>
<OptionRow
:title="names.hook"
tooltip="Hooks can be downloaded from the package store."
:title="names?.hook"
:tooltip="
t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.hooks'),
})
"
>
<Select
v-model="prf.current!.data.sgt.hook"
@ -125,15 +138,18 @@ const checkSegatoolsIni = async (target: string) => {
></Select>
</OptionRow>
<OptionRow
:title="names.io"
tooltip="IO plugins can be downloaded from the package store."
:title="names?.io"
:tooltip="`${t('cfg.segatools.ioModulesDesc')}
${t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.ioModules'),
})}`"
>
<Select
v-model="prf.current!.data.sgt.io2"
:options="[
{ title: 'native io4', value: 'hardware' },
{ title: t('cfg.segatools.io4'), value: 'hardware' },
{
title: 'segatools built-in (keyboard)',
title: t('cfg.segatools.ioBuiltIn'),
value: 'segatools_built_in',
},
...pkgs
@ -153,5 +169,29 @@ const checkSegatoolsIni = async (target: string) => {
option-value="value"
></Select>
</OptionRow>
<OptionRow
v-if="capabilities.includes('wine')"
:title="t('cfg.wine.runtime')"
>
<FilePicker
:directory="false"
:value="prf.current!.data.wine.runtime"
:callback="
(value: string) => (prf.current!.data.wine.runtime = value)
"
></FilePicker>
</OptionRow>
<OptionRow
v-if="capabilities.includes('wine')"
:title="t('cfg.wine.prefix')"
>
<FilePicker
:directory="true"
:value="prf.current!.data.wine.prefix"
:callback="
(value: string) => (prf.current!.data.wine.prefix = value)
"
></FilePicker>
</OptionRow>
</OptionCategory>
</template>

View File

@ -1,92 +1,43 @@
<script setup lang="ts">
import { computed } from 'vue';
import SelectButton from 'primevue/selectbutton';
import ToggleSwitch from 'primevue/toggleswitch';
import OptionCategory from '../OptionCategory.vue';
import OptionRow from '../OptionRow.vue';
import { useClientStore } from '../../stores';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const client = useClientStore();
const offlineModel = computed({
get() {
return client.offlineMode;
},
async set(value: boolean) {
await client.setOfflineMode(value);
},
});
const updatesModel = computed({
get() {
return client.enableAutoupdates;
},
async set(value: boolean) {
await client.setAutoupdates(value);
},
});
const verboseModel = computed({
get() {
return client.verbose;
},
async set(value: boolean) {
await client.setVerbose(value);
},
});
const themeModel = computed({
get() {
return client.theme;
},
async set(value: 'light' | 'dark' | 'system') {
await client.setTheme(value);
},
});
</script>
<template>
<OptionCategory title="STARTLINER">
<OptionRow title="UI scaling">
<SelectButton
v-model="client.scaleModel"
:options="[
{ title: 'S', value: 's' },
{ title: 'M', value: 'm' },
{ title: 'L', value: 'l' },
{ title: 'XL', value: 'xl' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
<OptionRow
:title="t('cfg.startliner.offlineMode')"
:tooltip="`${t('cfg.startliner.offlineModeTooltip')} ${t('cfg.afterRestart')}`"
>
<ToggleSwitch
:model-value="client.offlineMode"
@update:model-value="
async (v) => await client.setOfflineMode(v)
"
/>
</OptionRow>
<OptionRow
title="Offline mode"
tooltip="Disables the package store. Applies after a restart."
>
<ToggleSwitch v-model="offlineModel" />
</OptionRow>
<OptionRow title="Enable automatic updates">
<ToggleSwitch v-model="updatesModel" />
<OptionRow :title="t('cfg.startliner.autoUpdate')">
<ToggleSwitch
:model-value="client.enableAutoupdates"
@update:model-value="
async (v) => await client.setAutoupdates(v)
"
></ToggleSwitch>
</OptionRow>
<OptionRow
title="Enable detailed logs"
tooltip="Applies after a restart."
:title="t('cfg.startliner.verbose')"
:tooltip="t('cfg.afterRestart')"
>
<ToggleSwitch v-model="verboseModel" />
</OptionRow>
<OptionRow title="Light theme">
<SelectButton
v-model="themeModel"
:options="[
{ title: 'System', value: 'system' },
{ title: 'Light', value: 'light' },
{ title: 'Dark', value: 'dark' },
]"
:allow-empty="false"
option-label="title"
option-value="value"
<ToggleSwitch
:model-value="client.verbose"
@update:model-value="async (v) => await client.setVerbose(v)"
/>
</OptionRow>
</OptionCategory>

27
src/i18n.ts Normal file
View File

@ -0,0 +1,27 @@
import en from './i18n/en';
import { createI18n } from 'vue-i18n';
export type Locale = 'en' | 'ja' | 'pl';
const loadLocaleMessages = async (locale: Locale) => {
return (await import(`./i18n/${locale}.ts`)).default;
};
const i18n = createI18n({
legacy: false,
locale: 'en',
fallbackLocale: 'en',
warnHtmlInMessage: false,
warnHtmlMessage: false,
messages: { en, ja: {}, pl: {} },
});
const setLocale = async (locale: Locale) => {
if (locale !== 'en') {
const messages = await loadLocaleMessages(locale);
i18n.global.setLocaleMessage(locale, messages);
}
i18n.global.locale.value = locale;
};
export { i18n, setLocale };

248
src/i18n/en.ts Normal file
View File

@ -0,0 +1,248 @@
export default {
ok: 'OK',
cancel: 'Cancel',
enable: 'Enable',
disable: 'Disable',
default: 'Default',
search: 'Search',
next: 'Next',
skip: 'Skip',
close: 'Close',
by: 'by {namespace}',
start: {
failed: 'Start check failed',
accept: 'Run anyway',
error: {
package: 'Package missing',
dependency: 'Dependency missing',
tool: 'Tool missing',
unknown: 'Unknown error',
},
tooltip: {
game: 'The game path must be specified',
amfs: 'The amfs path must be specified',
segatools: 'A segatools hook package is necessary',
},
button: {
start: 'START',
stop: 'STOP',
unchecked: 'Skip checks and start',
shortcut: 'Create desktop shortcut',
help: 'Help',
refresh: 'Reapply mods and start',
cache: 'Clear mod cache',
},
},
game: {
ongeki: 'O.N.G.E.K.I.',
chunithm: 'CHUNITHM',
},
profile: {
welcome: 'Welcome to STARTLINER! Start by creating a profile.',
create: '{game} profile',
delete: 'Delete profile',
reallyDelete: 'Are you sure you want to delete {profile}?',
template: 'STARTLINER template',
importTemplate: 'Import template',
exportTemplate: 'Export template',
export: 'Export',
},
creator: {
header: 'Package creator',
basic: 'Basic information',
name: 'Name',
description: 'Description',
website: 'Website',
type: 'Package type',
rainy: 'Standard',
segatools: 'Segatools',
native: 'Native',
games: 'Games',
packageFormat: 'Package format spec',
},
store: {
installRecommended: 'Install recommended packages',
installed: 'Show installed',
deprecated: 'Show deprecated',
nsfw: 'Show NSFW',
incompatible: 'This package is currently incompatible with STARTLINER.',
includeCategories: 'Include categories',
excludeCategories: 'Exclude categories',
},
pkglist: {
missing: 'Missing',
local: 'Local packages',
namespace: 'By namespace',
type: 'By type',
category: 'By category',
standard: 'Standard mods',
native: 'Native mods',
segatools: 'segatools',
unsupported: 'Unsupported',
exclusions: 'Exclusions:',
},
patch: {
loading: 'Loading...',
noneFound:
"No compatible patches found. Make sure you're using unpacked and unpatched files.",
forceLoad: 'Force load',
// Example patch name override
// 'standard-no-encryption': 'No encryption',
// 'standard-no-encryption-tooltip': 'Will also disable TLS',
// It is also possible to add a tooltip where there normally is none
// 'standard-maximum-tracks-tooltip': 'The number of tracks per credit',
// For more info check https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Translation-%26-Localization
},
cfg: {
afterRestart: 'Applied after a restart',
hardware: 'Hardware',
segatools: {
general: 'General',
builtIn: 'Segatools built-in emulation',
targetTooltip:
'STARTLINER expects unpacked executables put into otherwise clean data.',
hooks: 'Hooks',
ioModules: 'IO modules',
ioModulesDesc: 'This should match your desired input method.',
ioBuiltIn: 'segatools built-in (keyboard)',
io4: 'Native IO4',
installTooltip: '{thing} can be downloaded from the package store.',
},
display: {
title: 'Display',
resolution: 'Game resolution',
primary: 'Primary',
target: 'Target display',
mode: 'Mode',
rotation: 'Rotation',
refreshRate: 'Refresh rate',
borderlessFullscreen: 'Borderless fullscreen',
borderlessFullscreenTooltip:
'Match display resolution with the game.',
dontSwitchPrimary: 'Skip switching primary display',
dontSwitchPrimaryTooltip:
'Only enable this option if switching the primary display causes issues. The monitors must have a matching refresh rate.',
index: 'Display index',
portrait: 'Portrait',
landscape: 'Landscape',
flipped: 'flipped',
window: 'Window',
borderless: 'Borderless window',
fullscreen: 'Fullscreen',
},
network: {
title: 'Network',
type: 'Network type',
remote: 'Remote',
localArtemis: 'Local (ARTEMiS)',
artemisPath: 'ARTEMiS path',
address: 'Server address',
keychip: 'Keychip',
subnet: 'Subnet',
addrSuffix: 'Address suffix',
},
aime: {
type: 'Aime type',
modules: 'Aime modules',
code: 'Aime code',
codeTooltip:
'Only applicable with the segatools built-in emulation or with compatible third-party packages',
aimedb: 'Use AiMeDB for physical cards',
aimedbTooltip:
'Whether physical cards should use AiMeDB to retrieve access codes. If the game is using a hosted network, enable this option to load the same account data/profile as you would get on a physical cab.',
serialPort: 'Aime serial port',
serialPortTooltip: `Ports can be checked in Devices and Printers or at googlechromelabs.github.io/serial-terminal
For AIC Pico, the AIME port should be selected.`,
serverName: 'Server name',
},
misc: {
title: 'Miscellaneous',
intel: 'OpenSSL bug workaround for Intel ≥10th gen',
intelTooltip: 'It is recommended to patch amdaemon instead.',
other: 'Other segatools options',
otherTooltip:
'Advanced or situational options not covered by STARTLINER',
},
extensions: {
title: 'Extensions',
bepInExConsole: 'BepInEx console',
audioMode: 'Audio mode',
audioTooltip:
'Exclusive 2-channel mode requires 7EVENDAYSHOLIDAYS-ExclusiveAudio',
audioShared: 'Shared',
audio6Ch: 'Exclusive 6-channel',
audio2Ch: 'Exclusive 2-channel',
sampleRate: 'Sample rate',
blacklist: 'Song ID blacklist',
blacklistTooltip:
'Scores on charts within this ID range will not be saved nor uploaded',
bonusTracks: 'Unlock bonus tracks',
bonusTracksTooltip:
'Disabling this option can help declutter the song list',
saekawa: 'Saekawa configuration file',
inohara: 'Inohara configuration file',
},
keyboard: {
title: 'Keyboard',
tooltip:
'Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)',
leverMode: 'Lever mode',
mouse: 'Mouse',
irTooltip:
'When playing on an actual keyboard, only bind ir1; leave the rest unbound',
},
wine: {
prefix: 'Wine prefix',
runtime: 'Wine runtime',
},
startliner: {
offlineMode: 'Offline mode',
offlineModeTooltip: 'Disables the package store.',
autoUpdate: 'Automatic updates',
verbose: 'Detailed logs',
},
},
onboarding: {
or: 'or',
backButton: 'a button on the back of the controller',
standard: `
You might get stuck on the following screen:
{bigblack}Aグループの基準機から設定を取得{endbig}
In which case, you should go to the test menu, and in game settings {black}ゲーム設定{end} switch from "follow the standard machine" {black}基準機に従う{end} to "standard machine" {black}基準機{end}.
The test menu can be accessed with %TESTMENU%.
`,
'ongeki-system-processing': `
You might get stuck on this screen for several minutes. _This is normal_. The game just takes a long time to load data.
If you install <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, subsequent launches will be much faster.
`,
'ongeki-lever': `
You also have to calibrate the lever, or you may get the error 3301.
Go to lever settings ({black}レバー設定{end}), move the lever to both edges, then press "end" ({black}終了{end}) and "save" ({black}保存する{end}).
`,
'chunithm-server': `
If you're stuck on this screen, restart the game.
If the problem persists, {link}check your network configuration{endlink}
`,
finale: `
You can access this page any time by right-clicking the START button.
Additional resources:
- {segaguide}SEGAguide{endlink}
- {twotorial}two-torial{endlink}
## Have fun
`,
},
};

14
src/i18n/ja.ts Normal file
View File

@ -0,0 +1,14 @@
export default {
game: {
ongeki: 'オンゲキ',
chunithm: 'チュウニズム',
},
profile: {
create: '{game}のプロフィル',
},
cfg: {
aime: {
type: 'Aimeタイプ',
},
},
};

288
src/i18n/pl.ts Normal file
View File

@ -0,0 +1,288 @@
export default {
ok: 'OK',
cancel: 'Anuluj',
enable: 'Włącz',
disable: 'Wyłącz',
default: 'Domyślne',
search: 'Wyszukaj',
next: 'Dalej',
skip: 'Pomiń',
close: 'Zamknij',
by: 'od {namespace}',
start: {
failed: 'Uruchomienie nie powiodło się',
accept: 'Uruchom mimo to',
error: {
package: 'Brakujący pakiet',
dependency: 'Brakująca dependencja',
tool: 'Brakujące narzędzie',
unknown: 'Nieznany błąd',
},
tooltip: {
game: 'Należy najpierw wskazać lokalizację gry',
amfs: 'Należy najpierw wskazać lokalizację amfs',
segatools: 'Należy dodać hook segatools',
},
button: {
start: 'START',
stop: 'STOP',
unchecked: 'Uruchom bez sprawdzania',
shortcut: 'Utwórz skrót',
help: 'Pomoc',
refresh: 'Uruchom po re-aplikacji modów',
cache: 'Wyczyść mod cache',
},
},
game: {
ongeki: 'O.N.G.E.K.I.',
chunithm: 'CHUNITHM',
},
profile: {
welcome: 'Witaj w STARTLINERZE! Zacznij od utworzenia profilu',
create: 'Profil {game}',
delete: 'Usuń profil',
reallyDelete: 'Czy na pewno chcesz usunąć {profile}?',
template: 'Szablon',
importTemplate: 'Importuj szablon',
exportTemplate: 'Eksportuj szablon',
export: 'Eksportuj',
},
creator: {
header: 'Kreator pakietów',
basic: 'Podstawowe informacje',
name: 'Nazwa',
description: 'Opis',
website: 'Strona internetowa',
type: 'Typ',
rainy: 'Standardowy',
segatools: 'Segatools',
native: 'Natywny',
games: 'Gry',
packageFormat: 'Specyfikacja formatu',
},
store: {
installRecommended: 'Dodaj zalecane pakiety',
installed: 'Pokaż zainstalowane',
deprecated: 'Pokaż przestarzałe',
nsfw: 'Pokaż mityczny O.N.G.E.K.I. Sex Mod dlaczego ta opcja w ogóle tu jest',
incompatible:
'Ten pakiet jest obecnie niekompatybilny ze STARTLINEREM.',
includeCategories: 'Włącz kategorie',
excludeCategories: 'Wyłącz kategorie',
},
pkglist: {
missing: 'Niedostępne',
local: 'Lokalne pakiety',
namespace: 'Po przestrzeni nazw',
type: 'Po typie',
category: 'Po kategorii',
standard: 'Standardowe mody',
native: 'Natywne mody',
segatools: 'segatools',
unsupported: 'Niewspierane',
exclusions: 'Czarna lista:',
},
patch: {
loading: 'Wczytuję...',
noneFound:
'Brak kompatybilnych łatek. Upewnij się, że używasz czystych odpakowanych plików.',
forceLoad: 'Wymuś załadowanie',
'standard-shared-audio':
'Wymuś współdzielony tryb dźwięku; częstotliwość w systemie musi wynosić 48kHz',
'standard-shared-audio-tooltip':
'Poprawia kompatybilność, ale może zwiększyć opóźnienie',
'standard-2ch': 'Wymuś stereo',
'standard-2ch-tooltip': 'Może powodować bass overload',
'standard-song-timer': 'Wyłącz timer wyboru utworu',
'standard-map-timer': 'Timer wyboru mapy',
'standard-map-timer-tooltip':
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
'standard-ticket-timer': 'Timer wyboru biletu',
'standard-ticket-timer-tooltip':
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
'standard-course-timer': 'Timer wyboru dana',
'standard-course-timer-tooltip':
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
'standard-unlimited-tracks': 'Nieograniczona maksymalna liczba utworów',
'standard-unlimited-tracks-tooltip':
'Konieczne do grania więcej niż 7 utworów na kredyt',
'standard-maximum-tracks': 'Maksymalna liczba utworów',
'standard-no-encryption': 'Wyłącz szyfrowanie',
'standard-no-encryption-tooltip': 'Wyłączy również TLS',
'standard-no-tls': 'Wyłącz TLS',
'standard-no-tls-tooltip': 'Obejście problemów z serwerem tytułowym',
'standard-head-to-head': 'Napraw head to head',
'standard-head-to-head-tooltip':
'Naprawia nieskończoną synchronizację podczas próby połączenia w trybie head to head',
'standard-bypass-1080p': 'Obejdź sprawdzenie 1080p',
'standard-bypass-120hz': 'Obejdź sprawdzenie 120Hz',
'standard-force-free-play-text': 'Wymuś tekst kredytu FREE PLAY',
'standard-force-free-play-text-tooltip':
'Zastępuje liczbę kredytów tekstem FREE PLAY',
'standard-custom-free-play-length': 'Długość tekstu FREE PLAY',
'standard-custom-free-play-length-tooltip':
'Zmienia długość tekstu wyświetlanego, gdy włączony jest wymuszony tekst kredytu FREE PLAY',
'standard-custom-free-play-text': 'Customowy tekst FREE PLAY',
'standard-custom-free-play-text-tooltip': 'Zastąp tekst FREE PLAY',
'standard-localhost':
'Zezwól na serwer pod adresem 127.0.0.1/localhost',
'standard-credit-freeze': 'Zamroź kredyty',
'standard-credit-freeze-tooltip':
'Zapobiega używaniu kredytów. Co najmniej jeden kredyt musi być dostępny, aby rozpocząć grę lub zakupić bilety premium.',
'standard-openssl-fix': 'Napraw błąd OpenSSL SHA',
'standard-openssl-fix-tooltip':
'Naprawia crash na procesorach Intel 10. generacji i nowszych',
},
cfg: {
afterRestart: 'Wymaga restartu.',
hardware: 'Prawdziwy czytnik',
segatools: {
general: 'Ogólne',
builtIn: 'Wbudowany emulator',
targetTooltip:
'STARTLINER oczekuje czystych danych, pomijając odpakowane exe.',
hooks: 'Hooki',
ioModules: 'Moduły IO',
ioModulesDesc:
'Powinien odpowiadać twojej preferowanej metodzie wejścia.',
ioBuiltIn: 'Wbudowany emulator (klawiatura)',
io4: 'Natywne IO4',
installTooltip: '{thing} można pobrać z pobierajki pakietów.',
},
display: {
title: 'Ekran',
resolution: 'Rozdzielczość',
primary: 'Główny',
target: 'Docelowy wyświetlacz',
mode: 'Tryb',
rotation: 'Obrót',
refreshRate: 'Częstotliwość odświeżania',
borderlessFullscreen: 'Bezramkowy tryb pełnoekranowy',
borderlessFullscreenTooltip:
'Dopasuj rozdzielczość wyświetlacza do gry.',
dontSwitchPrimary: 'Pomiń przełączanie głównego wyświetlacza',
dontSwitchPrimaryTooltip:
'Włącz tę opcję tylko wtedy, gdy przełączanie głównego wyświetlacza powoduje problemy. Monitory muszą mieć dopasowaną częstotliwość odświeżania.',
index: 'Indeks wyświetlacza',
portrait: 'Pion',
landscape: 'Poziom',
flipped: 'Odwrócony',
window: 'Okno',
borderless: 'Okno bez ramki',
fullscreen: 'Pełny ekran',
},
network: {
title: 'Sieć',
type: 'Typ sieci',
remote: 'Zdalny',
localArtemis: 'Lokalny (ARTEMiS)',
artemisPath: 'Lokalizacja ARTEMiSa',
address: 'Adres serwera',
keychip: 'Keychip',
subnet: 'Podsieć',
addrSuffix: 'Sufiks adresu',
},
aime: {
type: 'Typ Aime',
modules: 'Moduły Aime',
code: 'Kod Aime',
codeTooltip:
'Dotyczy tylko wbudowanej emulacji lub zgodnych pakietów',
aimedb: 'Użyj AiMeDB dla kart fizycznych',
aimedbTooltip:
'Decyduje czy karty fizyczne powinny używać AiMeDB do pobierania kodów dostępu. Jeśli łączysz się z hostowaną siecią, włącz tę opcję, aby załadować te same dane konta, jakie uzyskałxbyś na fizycznym cabie.',
serialPort: 'Port szeregowy Aime',
serialPortTooltip: `Porty można sprawdzić w Urządzeniach i drukarkach lub na googlechromelabs.github.io/serial-terminal
Dla AIC Pico powinien być wybrany port AIME.`,
serverName: 'Nazwa serwera',
},
misc: {
title: 'Różne',
intel: 'Obejście buga OpenSSL dla procesorów Intel ≥10 generacji',
intelTooltip: 'Zaleca się zamiast tego załatać amdaemon.',
other: 'Inne opcje segatools',
otherTooltip:
'Zaawansowane lub sytuacyjne opcje, które nie są objęte przez STARTLINERA',
},
extensions: {
title: 'Rozszerzenia',
bepInExConsole: 'Konsola BepInExa',
audioMode: 'Tryb audio',
audioTooltip:
'Tryb ekskluzywny 2-kanałowy wymaga 7EVENDAYSHOLIDAYS-ExclusiveAudio',
audioShared: 'Współdzielony',
audio6Ch: 'Ekskluzywny 6-kanałowy',
audio2Ch: 'Ekskluzywny 2-kanałowy',
sampleRate: 'Częstotliwość',
blacklist: 'Czarna lista utworów',
blacklistTooltip:
'Utwory w tym zakresie ID nie będą zapisywane ani przesyłane',
bonusTracks: 'Odblokuj Bonusowe Utwory',
bonusTracksTooltip:
'Wyłączenie tej opcji może pomóc w uporządkowaniu listy utworów',
saekawa: 'Plik konfiguracyjny Saekawy',
inohara: 'Plik konfiguracyjny Inohary',
},
keyboard: {
title: 'Klawiatura',
tooltip:
'Dotyczy tylko wtedy, gdy moduł IO jest ustawiony na wbudowaną emulację lub zgodny moduł (np. mu3io.NET)',
leverMode: 'Tryb wajchy',
mouse: 'Mysz',
irTooltip:
'Jeśli grasz na klawiaturze, ustaw tylko ir1; pozostałe zostaw wyłączone',
},
wine: {
prefix: 'Wine prefix',
runtime: 'Lokalizacja Wine',
},
startliner: {
offlineMode: 'Tryb offline',
offlineModeTooltip: 'Wyłącza pobierajkę pakietów.',
autoUpdate: 'Automatyczne aktualizacje',
verbose: 'Szczegółowe logi',
},
},
onboarding: {
or: 'lub',
backButton: 'przycisku z tyłu',
standard: `
Możesz utknąć na następującym ekranie:
{bigblack}Aグループの基準機から設定を取得{endbig}
Wówczas musisz przejść do menu testowego i w ustawieniach gry {black}ゲーム設定{end} przełączyć z "podążaj za standardem" {black}基準機に従う{end} na "standard" {black}基準機{end}.
Do menu testowego możesz dostać się za pomocą %TESTMENU%.
`,
'ongeki-system-processing': `
Możesz utknąć na tym ekranie przez kilka(naście) minut. _To jest normalne_. Gra po prostu potrzebuje dużo czasu na załadowanie danych.
Jeśli zainstalujesz <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, kolejne uruchomienia będą znacznie szybsze.
`,
'ongeki-lever': `
Musisz również skalibrować wajchę; w przeciwnym razie możesz otrzymać błąd 3301.
Przejdź do ustawień wajchy ({black}レバー設定{end}), przesuń wajchę do obu krawędzi, a następnie naciśnij "koniec" ({black}終了{end}) i "zapisz" ({black}保存する{end}).
`,
'chunithm-server': `
Jeśli utkniesz na tym ekranie, zrestartuj grę.
Jeśli problem będzie się powtarzał, {link}sprawdź swoją konfigurację sieciową{endlink}.
`,
finale: `
Możesz uzyskać dostęp do tej strony w każdej chwili, klikając prawym przyciskiem myszy przycisk START.
Dodatkowe zasoby:
- {segaguide}SEGAguide{endlink}
- {twotorial}two-torial{endlink}
## Miłej zabawy
`,
},
};

119
src/keyboard.ts Normal file
View File

@ -0,0 +1,119 @@
const KEY_MAP: { [key: number]: string } = {
1: 'M1',
2: 'M2',
4: 'M3',
5: 'M4',
6: 'M5',
8: 'Backspace',
9: 'Tab',
12: 'Clear',
13: 'Enter',
19: 'Pause',
20: 'CapsLock',
27: 'Escape',
32: 'Space',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'ArrowLeft',
38: 'ArrowUp',
39: 'ArrowRight',
40: 'ArrowDown',
45: 'Insert',
46: 'Delete',
48: 'Digit0',
49: 'Digit1',
50: 'Digit2',
51: 'Digit3',
52: 'Digit4',
53: 'Digit5',
54: 'Digit6',
55: 'Digit7',
56: 'Digit8',
57: 'Digit9',
65: 'KeyA',
66: 'KeyB',
67: 'KeyC',
68: 'KeyD',
69: 'KeyE',
70: 'KeyF',
71: 'KeyG',
72: 'KeyH',
73: 'KeyI',
74: 'KeyJ',
75: 'KeyK',
76: 'KeyL',
77: 'KeyM',
78: 'KeyN',
79: 'KeyO',
80: 'KeyP',
81: 'KeyQ',
82: 'KeyR',
83: 'KeyS',
84: 'KeyT',
85: 'KeyU',
86: 'KeyV',
87: 'KeyW',
88: 'KeyX',
89: 'KeyY',
90: 'KeyZ',
91: 'MetaLeft',
92: 'MetaRight',
93: 'ContextMenu',
96: 'Numpad0',
97: 'Numpad1',
98: 'Numpad2',
99: 'Numpad3',
100: 'Numpad4',
101: 'Numpad5',
102: 'Numpad6',
103: 'Numpad7',
104: 'Numpad8',
105: 'Numpad9',
106: 'NumpadMultiply',
107: 'NumpadAdd',
109: 'NumpadSubtract',
110: 'NumpadDecimal',
111: 'NumpadDivide',
112: 'F1',
113: 'F2',
114: 'F3',
115: 'F4',
116: 'F5',
117: 'F6',
118: 'F7',
119: 'F8',
120: 'F9',
121: 'F10',
122: 'F11',
123: 'F12',
144: 'NumLock',
145: 'ScrollLock',
160: 'ShiftLeft',
161: 'ShiftRight',
162: 'ControlLeft',
163: 'ControlRight',
164: 'AltLeft',
165: 'AltRight',
186: 'Semicolon',
187: 'Equal',
188: 'Comma',
189: 'Minus',
190: 'Period',
191: 'Slash',
192: 'Backquote',
219: 'BracketLeft',
220: 'Backslash',
221: 'BracketRight',
222: 'Quote',
};
export const fromKeycode = (keyCode: number): string | null => {
return KEY_MAP[keyCode] ?? null;
};
export const toKeycode = (key: string): number | null => {
const res = Object.entries(KEY_MAP).find(([_, v]) => v === key)?.[0];
return res ? parseInt(res) : null;
};

View File

@ -6,14 +6,15 @@ import PrimeVue from 'primevue/config';
import ConfirmationService from 'primevue/confirmationservice';
import Tooltip from 'primevue/tooltip';
import App from './components/App.vue';
import { i18n } from './i18n';
import { changePrimaryColor } from './util';
const pinia = createPinia();
const app = createApp(App);
const Preset = definePreset(Theme, {});
app.use(pinia);
app.use(i18n);
app.use(PrimeVue, {
theme: {
preset: Preset,

View File

@ -4,6 +4,7 @@ import { listen } from '@tauri-apps/api/event';
import * as path from '@tauri-apps/api/path';
import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
import { Locale, setLocale as actualSetLocale } from './i18n';
import { invoke, invoke_nopopup } from './invoke';
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
import {
@ -31,23 +32,24 @@ export const useGeneralStore = defineStore('general', () => {
},
});
const configDir = computed(() => {
const loadDirs = async () => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
const d = (await invoke('list_directories')) as Dirs;
dirs.value = d;
}
return dirs.value.config_dir;
};
const configDir = computed(async () => {
await loadDirs();
return dirs.value!.config_dir;
});
const dataDir = computed(() => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
}
return dirs.value.data_dir;
const dataDir = computed(async () => {
await loadDirs();
return dirs.value!.data_dir;
});
const cacheDir = computed(() => {
if (dirs.value === null) {
throw new Error('Invalid directory access');
}
return dirs.value.cache_dir;
const cacheDir = computed(async () => {
await loadDirs();
return dirs.value!.cache_dir;
});
return {
@ -119,13 +121,13 @@ export const usePkgStore = defineStore('pkg', {
listen<InstallStatus>('install-start', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.busy = true;
this.pkg[key].js.downloading = true;
});
listen<InstallStatus>('install-end', async (ev) => {
const key = ev.payload.pkg;
await this.reload(key);
this.pkg[key].js.busy = false;
this.pkg[key].js.downloading = false;
});
},
@ -152,17 +154,22 @@ export const usePkgStore = defineStore('pkg', {
async reloadWith(key: string, pkg: Package) {
if (this.pkg[key] === undefined) {
this.pkg[key] = { js: { busy: false } } as Package;
this.pkg[key] = { js: { downloading: false } } as Package;
} else {
this.pkg[key].loc = null;
this.pkg[key].rmt = null;
}
Object.assign(this.pkg[key], pkg);
if (!pkg.js) {
pkg.js = { downloading: false };
}
if (pkg.rmt !== null) {
pkg.rmt.categories.forEach((c) =>
this.availableCategories.add(c)
);
pkg.js.downloading = false;
}
},
@ -193,9 +200,8 @@ export const usePkgStore = defineStore('pkg', {
force: true,
});
} catch (err) {
console.error(err);
if (pkg !== undefined) {
pkg.js.busy = false;
pkg.js.downloading = false;
}
}
},
@ -316,8 +322,8 @@ export const usePrfStore = defineStore('prf', () => {
const generalStore = useGeneralStore();
const configDir = computed(async () => {
return await path.join(
generalStore.configDir,
return path.join(
await generalStore.configDir,
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
);
});
@ -353,6 +359,10 @@ export const usePrfStore = defineStore('prf', () => {
};
});
export enum ClientData {
Onboarded,
}
export const useClientStore = defineStore('client', () => {
type ScaleType = 's' | 'm' | 'l' | 'xl';
const scaleFactor: Ref<ScaleType> = ref('s');
@ -362,16 +372,26 @@ export const useClientStore = defineStore('client', () => {
const enableAutoupdates = ref(true);
const verbose = ref(false);
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
const onboarded: Ref<Game[]> = ref([]);
const locale: Ref<Locale> = ref('en');
const currentTab: Ref<string> = ref('users');
const pkgListMode: Ref<'namespace' | 'type' | 'category'> =
ref('namespace');
const hiddenCategories: Ref<string[]> = ref([]);
const scaleValue = (value: ScaleType) =>
const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
const scaleValue = computed(() => {
return _scaleValue(scaleFactor.value);
});
const setScaleFactor = async (value: ScaleType) => {
scaleFactor.value = value;
const window = getCurrentWindow();
const w = Math.floor(scaleValue(value) * 900);
const h = Math.floor(scaleValue(value) * 480);
const w = Math.floor(_scaleValue(value) * 900);
const h = Math.floor(_scaleValue(value) * 600);
let size = await window.innerSize();
@ -397,7 +417,7 @@ export const useClientStore = defineStore('client', () => {
const input = JSON.parse(
await readTextFile(
await path.join(
generalStore.configDir,
await generalStore.configDir,
'client-options.json'
)
)
@ -416,6 +436,27 @@ export const useClientStore = defineStore('client', () => {
if (input.theme) {
theme.value = input.theme;
}
if (input.onboarded) {
onboarded.value = input.onboarded;
}
if (input.locale) {
locale.value = input.locale;
}
if (input.currentTab) {
currentTab.value = input.currentTab;
}
if (input.pkgListMode) {
pkgListMode.value = input.pkgListMode;
}
if (input.hiddenCategories) {
hiddenCategories.value = input.hiddenCategories;
}
await setLocale(locale.value);
await setTheme(theme.value);
} catch (e) {
console.error(`Error reading client options: ${e}`);
@ -440,7 +481,10 @@ export const useClientStore = defineStore('client', () => {
const size = await w.innerSize();
await writeTextFile(
await path.join(generalStore.configDir, 'client-options.json'),
await path.join(
await generalStore.configDir,
'client-options.json'
),
JSON.stringify({
scaleFactor: scaleFactor.value,
windowSize: {
@ -448,6 +492,11 @@ export const useClientStore = defineStore('client', () => {
h: Math.floor(size.height),
},
theme: theme.value,
onboarded: onboarded.value,
locale: locale.value,
currentTab: currentTab.value,
pkgListMode: pkgListMode.value,
hiddenCategories: hiddenCategories.value,
})
);
};
@ -495,6 +544,17 @@ export const useClientStore = defineStore('client', () => {
await save();
};
const setOnboarded = async (game: Game) => {
onboarded.value = [...onboarded.value, game];
await save();
};
const setLocale = async (loc: Locale) => {
locale.value = loc;
await save();
await actualSetLocale(loc);
};
getCurrentWindow().onResized(async ({ payload }) => {
// For whatever reason this is 0 when minimized
if (payload.width > 0) {
@ -508,8 +568,15 @@ export const useClientStore = defineStore('client', () => {
enableAutoupdates,
verbose,
theme,
onboarded,
locale,
timeout,
scaleModel,
currentTab,
pkgListMode,
hiddenCategories,
_scaleValue,
scaleValue,
load,
save,
queueSave,
@ -517,5 +584,7 @@ export const useClientStore = defineStore('client', () => {
setAutoupdates,
setVerbose,
setTheme,
setOnboarded,
setLocale,
};
});

View File

@ -19,7 +19,7 @@ export interface Package {
icon: string;
} | null;
js: {
busy: boolean;
downloading: boolean;
};
}
@ -56,6 +56,7 @@ export interface ProfileData {
display: DisplayConfig;
network: NetworkConfig;
bepinex: BepInExConfig;
wine: WineConfig;
mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined;
patches: {
@ -105,9 +106,17 @@ export interface BepInExConfig {
console: boolean;
}
export interface WineConfig {
runtime: string;
prefix: string;
}
export interface Mu3IniConfig {
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
// blacklist?: [number, number];
sample_rate: number;
blacklist?: [number, number];
gp: number;
enable_bonus_tracks: boolean;
}
export interface OngekiButtons {
@ -164,7 +173,7 @@ export interface Patch {
id: string;
name: string;
tooltip: string;
type: undefined | 'number';
type: undefined | 'number' | 'hex';
default: number;
min: number;
max: number;

View File

@ -3,11 +3,7 @@ import { Feature, Game, Package } from './types';
export const changePrimaryColor = (game: Game | null) => {
const color =
game === 'ongeki'
? 'pink'
: game === 'chunithm'
? 'yellow'
: 'bluegray';
game === 'ongeki' ? 'pink' : game === 'chunithm' ? 'yellow' : 'purple';
updatePrimaryPalette({
50: `{${color}.50}`,
@ -63,3 +59,12 @@ export const messageSplit = (message: any) => {
export const shouldPreferDark = () => {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
};
export const prettyPrint = (game: Game) => {
switch (game) {
case 'ongeki':
return 'O.N.G.E.K.I.';
case 'chunithm':
return 'CHUNITHM';
}
};

View File

@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'vite';
// @ts-expect-error process is a nodejs global
const host = process.env.TAURI_DEV_HOST;
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
ignored: ['**/rust/**'],
},
},
build: {
chunkSizeWarningLimit: 1024,
},
}));