Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
e87b661f08 | |||
5d2d407659 | |||
795e889bd0 | |||
7071f19877 | |||
a72ec25088 | |||
5893536daa | |||
e9550e8eee | |||
658a69a1e2 | |||
f3ee0d0068 | |||
43f885cffc | |||
d0ce3cddc7 | |||
9cbdf2a9c8 | |||
54a6476010 | |||
e4dc0b1f55 | |||
e6c21ef04a | |||
d3145bfc4e | |||
c7ddeb53e6 | |||
b82fcc942f | |||
ac18c34895 |
32
CHANGELOG.md
@ -1,3 +1,35 @@
|
|||||||
|
## 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
|
||||||
|
|
||||||
|
## 0.8.1
|
||||||
|
|
||||||
|
- Hotfixed the program failing to launch if the data dir hadn't already been created
|
||||||
|
|
||||||
|
## 0.8.0
|
||||||
|
|
||||||
|
- Added support for ChuniIO
|
||||||
|
- CHUNITHM support is now complete
|
||||||
|
- Added a context menu option to create a desktop shortcut
|
||||||
|
- Added a confirmation prompt before deleting a profile
|
||||||
|
- Removed Slow
|
||||||
|
|
||||||
|
## 0.7.1
|
||||||
|
|
||||||
|
- Hotfixed amdaemon crashing at launch
|
||||||
|
- Greyed out packages currently incompatible with STARTLINER
|
||||||
|
|
||||||
## 0.7.0
|
## 0.7.0
|
||||||
|
|
||||||
- Hopefully fixed issues with the download button
|
- Hopefully fixed issues with the download button
|
||||||
|
47
README.md
@ -1,6 +1,7 @@
|
|||||||
# STARTLINER
|
# 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).
|
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).
|
||||||
|
|
||||||
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome.
|
||||||
|
|
||||||
@ -8,12 +9,9 @@ Made with Rust (Tauri) and Vue. Technically multiplatform. Contributions welcome
|
|||||||
|
|
||||||
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
|
- [Clean](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details) data modding
|
||||||
- Segatools configuration
|
- Segatools configuration
|
||||||
- Display configuration with automatic rollback
|
- Monitor configuration with automatic rollback
|
||||||
- Support for multiple configurations pointing at the same data
|
- Support for multiple configurations pointing at the same data
|
||||||
|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
Download a prebuilt binary from [Releases](https://gitea.tendokyu.moe/akanyan/STARTLINER/releases) or build it yourself:
|
||||||
@ -23,37 +21,22 @@ bun install
|
|||||||
bun run tauri build
|
bun run tauri build
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least). STARTLINER expects clean data with unpacked binaries. Anything else you may have in the game directory (segatools, BepInEx, etc.) can be present, but will not be used.
|
Create a profile, then click on things in the configuration tab (game path, `amfs` and network at the least).
|
||||||
|
|
||||||
Once a profile has been set up, it is possible to bypass the GUI:
|
STARTLINER expects clean data with unpacked binaries. Anything else you may have in the game directory
|
||||||
|
(segatools, BepInEx, etc.) can be present, but will not be used.
|
||||||
```sh
|
|
||||||
startliner --start --game ongeki --profile <name>
|
|
||||||
```
|
|
||||||
|
|
||||||
To create a desktop shortcut: `Copy -> Paste Shortcut -> Properties`, and then append `--start --game ongeki --profile <name>` to `Target`.
|
|
||||||
|
|
||||||
## Package format
|
## Package format
|
||||||
|
|
||||||
- [Package format requirements](https://rainy.patafour.zip/package/create/docs/)
|
Refer to [the wiki](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format).
|
||||||
- A subset of the simple BlueSteel Rainycolor format is currently supported. [Full reference (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou/wiki/Create-Module#user-content-rainycolor-simple)
|
|
||||||
|
|
||||||
```
|
|
||||||
├───app
|
|
||||||
│ └───BepInEx
|
|
||||||
│ └───*
|
|
||||||
├───option
|
|
||||||
│ └───Axyz
|
|
||||||
│ └───*
|
|
||||||
├───icon.png
|
|
||||||
├───README.md
|
|
||||||
└───manifest.json
|
|
||||||
```
|
|
||||||
|
|
||||||
More file overrides may be supported in the future.
|
|
||||||
|
|
||||||
Arbitrary scripts are not supported by design and that will probably never change.
|
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
- [BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
|
[BlueSteel launcher (CW: vore)](https://yozora.bluesteel.737.jp.net/HarmonyPublic/SOS-Kongou)
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
1
TODO.md
@ -4,6 +4,5 @@
|
|||||||
|
|
||||||
### Long-term
|
### Long-term
|
||||||
|
|
||||||
- Progress bars and other GUI sugar
|
|
||||||
- artemis as a special package
|
- artemis as a special package
|
||||||
- Other arcade games (if there is demand)
|
- Other arcade games (if there is demand)
|
||||||
|
3
public/help-chunithm-server.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
If you're stuck on this screen, restart the game.
|
||||||
|
|
||||||
|
If the problem persists, <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">check your network configuration</a>
|
BIN
public/help-chunithm-server.png
Normal file
After Width: | Height: | Size: 73 KiB |
BIN
public/help-finale-chunithm.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/help-finale-ongeki.png
Normal file
After Width: | Height: | Size: 12 KiB |
8
public/help-finale.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
You can access this page any time by right-clicking the START button.
|
||||||
|
|
||||||
|
Additional resources:
|
||||||
|
|
||||||
|
- <a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">SEGAguide</a>
|
||||||
|
- <a href="https://two-torial.xyz/" target="_blank">two-torial</a>
|
||||||
|
|
||||||
|
## Have fun
|
3
public/help-ongeki-lever.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
You also have to calibrate the lever, or you may get the error 3301.
|
||||||
|
|
||||||
|
Go to lever settings (<span class="bg-black text-white">レバー設定</span>), move the lever to both edges, then press "end" (<span class="bg-black text-white">終了</span>) and "save" (<span class="bg-black text-white">保存する</span>).
|
BIN
public/help-ongeki-lever.png
Normal file
After Width: | Height: | Size: 90 KiB |
3
public/help-ongeki-system-processing.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
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.
|
BIN
public/help-ongeki-system-processing.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
public/help-standard-chunithm.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
public/help-standard-ongeki.png
Normal file
After Width: | Height: | Size: 129 KiB |
7
public/help-standard.md
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
You might get stuck on the following screen:
|
||||||
|
|
||||||
|
<div class="p-2 mt-1 mb-1 bg-black text-white">Aグループの基準機から設定を取得</div>
|
||||||
|
|
||||||
|
In which case, you should go to the test menu, and in game settings <span class="bg-black text-white">ゲーム設定</span> switch from "follow the standard machine" <span class="bg-black text-white">基準機に従う</span> to "standard machine" <span class="bg-black text-white">基準機</span>.
|
||||||
|
|
||||||
|
The test menu can be accessed with %TESTMENU%.
|
BIN
res/cfg.png
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 43 KiB |
BIN
res/cfg2.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
res/icon-chunithm.ico
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
res/icon-ongeki.ico
Normal file
After Width: | Height: | Size: 87 KiB |
BIN
res/list.png
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 70 KiB |
BIN
res/store.png
Normal file
After Width: | Height: | Size: 63 KiB |
2
rust/Cargo.lock
generated
@ -803,7 +803,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -53,5 +53,5 @@ tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
|
|||||||
tauri-plugin-updater = "2"
|
tauri-plugin-updater = "2"
|
||||||
|
|
||||||
[target.'cfg(target_os = "windows")'.dependencies]
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
winsafe = { version = "0.0.23", features = ["user"] }
|
winsafe = { version = "0.0.23", features = ["user", "ole", "shell"] }
|
||||||
displayz = "^0.2.0"
|
displayz = "^0.2.0"
|
||||||
|
BIN
rust/icons/icon.ico
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
rust/icons/icon.png
Normal file
After Width: | Height: | Size: 53 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 103 KiB |
Before Width: | Height: | Size: 23 KiB |
@ -133,6 +133,8 @@ impl AppData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn init_logger(cfg: &GlobalConfig) {
|
fn init_logger(cfg: &GlobalConfig) {
|
||||||
|
_ = std::fs::create_dir_all(util::data_dir());
|
||||||
|
|
||||||
let mut fern_builder;
|
let mut fern_builder;
|
||||||
let colors = ColoredLevelConfig::new()
|
let colors = ColoredLevelConfig::new()
|
||||||
.debug(Color::Green)
|
.debug(Color::Green)
|
||||||
|
@ -397,6 +397,13 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
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())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
||||||
log::debug!("invoke: list_platform_capabilities");
|
log::debug!("invoke: list_platform_capabilities");
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
use std::{collections::HashSet, path::PathBuf};
|
use std::{collections::HashSet, path::PathBuf};
|
||||||
use futures::Stream;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tauri::{AppHandle, Emitter};
|
use tauri::{AppHandle, Emitter};
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
@ -15,7 +14,7 @@ pub struct DownloadHandler {
|
|||||||
#[derive(Serialize, Deserialize, Clone)]
|
#[derive(Serialize, Deserialize, Clone)]
|
||||||
pub struct DownloadTick {
|
pub struct DownloadTick {
|
||||||
pkg_key: PkgKey,
|
pkg_key: PkgKey,
|
||||||
ratio: f32
|
ratio: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DownloadHandler {
|
impl DownloadHandler {
|
||||||
@ -50,14 +49,15 @@ impl DownloadHandler {
|
|||||||
|
|
||||||
let mut cache_file_w = File::create(&zip_path_part).await?;
|
let mut cache_file_w = File::create(&zip_path_part).await?;
|
||||||
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
|
let mut byte_stream = reqwest::get(&rmt.download_url).await?.bytes_stream();
|
||||||
let first_hint = byte_stream.size_hint().0 as f32;
|
let mut total_bytes = 0;
|
||||||
|
|
||||||
log::info!("downloading: {}", rmt.download_url);
|
log::info!("downloading: {}", rmt.download_url);
|
||||||
while let Some(item) = byte_stream.next().await {
|
while let Some(item) = byte_stream.next().await {
|
||||||
let i = item?;
|
let i = item?;
|
||||||
app.emit("download-tick", DownloadTick {
|
total_bytes += i.len();
|
||||||
|
_ = app.emit("download-progress", DownloadTick {
|
||||||
pkg_key: pkg_key.clone(),
|
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?;
|
cache_file_w.write_all(&mut i.as_ref()).await?;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ use appdata::{AppData, ToggleAction};
|
|||||||
use model::misc::Game;
|
use model::misc::Game;
|
||||||
use pkg::PkgKey;
|
use pkg::PkgKey;
|
||||||
use pkg_store::Payload;
|
use pkg_store::Payload;
|
||||||
use tauri::{AppHandle, Listener, Manager, RunEvent};
|
use tauri::{AppHandle, Emitter, Listener, Manager, RunEvent};
|
||||||
use tauri_plugin_deep_link::DeepLinkExt;
|
use tauri_plugin_deep_link::DeepLinkExt;
|
||||||
use tauri_plugin_cli::CliExt;
|
use tauri_plugin_cli::CliExt;
|
||||||
use tokio::{fs, sync::Mutex, try_join};
|
use tokio::{fs, sync::Mutex, try_join};
|
||||||
@ -66,13 +66,8 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
log::debug!("{:?} {:?} {:?}", start_arg, game_arg, name_arg);
|
||||||
if start_arg.occurrences > 0 {
|
if start_arg.occurrences > 0 {
|
||||||
start_immediately = true;
|
start_immediately = true;
|
||||||
app_data.state.remain_open = false;
|
|
||||||
} else {
|
} else {
|
||||||
tauri::WebviewWindowBuilder::new(app, "main", tauri::WebviewUrl::App("index.html".into()))
|
open_window(apph.clone())?;
|
||||||
.title("STARTLINER")
|
|
||||||
.inner_size(900f64, 480f64)
|
|
||||||
.min_inner_size(900f64, 480f64)
|
|
||||||
.build()?;
|
|
||||||
start_immediately = false;
|
start_immediately = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -107,14 +102,15 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.listen("download-end", closure!(clone apph, |ev| {
|
app.listen("download-end", closure!(clone apph, |ev| {
|
||||||
log::debug!("download-end triggered: {}", ev.payload());
|
|
||||||
let raw = ev.payload();
|
let raw = ev.payload();
|
||||||
|
log::debug!("download-end triggered: {}", raw);
|
||||||
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
let key = PkgKey(raw[1..raw.len()-1].to_owned());
|
||||||
let apph = apph.clone();
|
let apph = apph.clone();
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
let mutex = apph.state::<Mutex<AppData>>();
|
||||||
let mut appd = mutex.lock().await;
|
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);
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -131,19 +127,21 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
app.listen("install-end-prelude", closure!(clone apph, |ev| {
|
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());
|
let payload = serde_json::from_str::<Payload>(ev.payload());
|
||||||
|
log::debug!("install-end-prelude triggered: {:?}", payload);
|
||||||
let apph = apph.clone();
|
let apph = apph.clone();
|
||||||
if let Ok(payload) = payload {
|
if let Ok(payload) = payload {
|
||||||
tauri::async_runtime::spawn(async move {
|
tauri::async_runtime::spawn(async move {
|
||||||
let mutex = apph.state::<Mutex<AppData>>();
|
let mutex = apph.state::<Mutex<AppData>>();
|
||||||
let mut appd = mutex.lock().await;
|
let mut appd = mutex.lock().await;
|
||||||
|
let res = appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf);
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"install-end-prelude toggle {:?}",
|
"install-end-prelude toggle {:?}",
|
||||||
appd.toggle_package(payload.pkg.clone(), ToggleAction::EnableSelf)
|
res
|
||||||
);
|
);
|
||||||
use tauri::Emitter;
|
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 {
|
} else {
|
||||||
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
|
log::error!("install-end-prelude: invalid payload: {}", ev.payload());
|
||||||
@ -157,13 +155,20 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
{
|
{
|
||||||
let mut appd = mtx.lock().await;
|
let mut appd = mtx.lock().await;
|
||||||
if let Err(e) = appd.pkgs.reload_all().await {
|
if let Err(e) = appd.pkgs.reload_all().await {
|
||||||
log::error!("Unable to reload packages: {}", e);
|
log::error!("unable to reload packages: {}", e);
|
||||||
apph.exit(1);
|
apph.exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(e) = cmd::startline(apph.clone(), false).await {
|
if let Err(e) = cmd::startline(apph.clone(), false).await {
|
||||||
log::error!("Unable to launch: {}", e);
|
log::error!("unable to launch: {}", e);
|
||||||
apph.exit(1);
|
_ = open_window(apph.clone());
|
||||||
|
// stupid but effective
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(3));
|
||||||
|
_ = apph.emit("launch-error", e.to_string());
|
||||||
|
} else {
|
||||||
|
let mut appd = mtx.lock().await;
|
||||||
|
appd.state.remain_open = false;
|
||||||
|
log::info!("started quietly");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
@ -199,6 +204,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::sync_current_profile,
|
cmd::sync_current_profile,
|
||||||
cmd::save_current_profile,
|
cmd::save_current_profile,
|
||||||
cmd::load_segatools_ini,
|
cmd::load_segatools_ini,
|
||||||
|
cmd::create_shortcut,
|
||||||
|
|
||||||
cmd::get_global_config,
|
cmd::get_global_config,
|
||||||
cmd::set_global_config,
|
cmd::set_global_config,
|
||||||
@ -229,7 +235,8 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
let mutex = app.state::<Mutex<AppData>>();
|
let mutex = app.state::<Mutex<AppData>>();
|
||||||
let appd = mutex.lock().await;
|
let appd = mutex.lock().await;
|
||||||
if let Some(p) = &appd.profile {
|
if let Some(p) = &appd.profile {
|
||||||
log::debug!("save: {:?}", p.save());
|
let res = p.save();
|
||||||
|
log::debug!("save: {:?}", res);
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -322,3 +329,14 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
|||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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, 600f64)
|
||||||
|
.min_inner_size(900f64, 600f64)
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -20,6 +20,13 @@ impl Game {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn print(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Game::Ongeki => "O.N.G.E.K.I.",
|
||||||
|
Game::Chunithm => "CHUNITHM"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn hook_exe(&self) -> &'static str {
|
pub fn hook_exe(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Game::Ongeki => "mu3hook.dll",
|
Game::Ongeki => "mu3hook.dll",
|
||||||
@ -88,9 +95,10 @@ pub enum StartCheckError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize, Clone)]
|
#[derive(Default, Serialize, Deserialize, Clone)]
|
||||||
#[serde(default)]
|
|
||||||
pub struct ConfigHook {
|
pub struct ConfigHook {
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub allnet_auth: Option<ConfigHookAuth>,
|
pub allnet_auth: Option<ConfigHookAuth>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub aime: Option<ConfigHookAime>,
|
pub aime: Option<ConfigHookAime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -13,6 +13,14 @@ pub enum Aime {
|
|||||||
Other(PkgKey),
|
Other(PkgKey),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Default, PartialEq, Debug)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum IOSelection {
|
||||||
|
Hardware,
|
||||||
|
#[default] SegatoolsBuiltIn,
|
||||||
|
Custom(PkgKey)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct AMNet {
|
pub struct AMNet {
|
||||||
@ -32,7 +40,9 @@ impl Default for AMNet {
|
|||||||
pub struct Segatools {
|
pub struct Segatools {
|
||||||
pub target: PathBuf,
|
pub target: PathBuf,
|
||||||
pub hook: Option<PkgKey>,
|
pub hook: Option<PkgKey>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub io: Option<PkgKey>,
|
pub io: Option<PkgKey>,
|
||||||
|
pub io2: IOSelection,
|
||||||
pub aime: Aime,
|
pub aime: Aime,
|
||||||
pub amfs: PathBuf,
|
pub amfs: PathBuf,
|
||||||
pub option: PathBuf,
|
pub option: PathBuf,
|
||||||
@ -51,6 +61,7 @@ impl Segatools {
|
|||||||
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
|
Game::Chunithm => Some(PkgKey("segatools-chusanhook".to_owned()))
|
||||||
},
|
},
|
||||||
io: None,
|
io: None,
|
||||||
|
io2: IOSelection::SegatoolsBuiltIn,
|
||||||
amfs: PathBuf::default(),
|
amfs: PathBuf::default(),
|
||||||
option: PathBuf::default(),
|
option: PathBuf::default(),
|
||||||
appdata: PathBuf::from("appdata"),
|
appdata: PathBuf::from("appdata"),
|
||||||
|
@ -22,4 +22,5 @@ pub struct V1Version {
|
|||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||||
pub download_url: String,
|
pub download_url: String,
|
||||||
|
pub file_size: i64,
|
||||||
}
|
}
|
@ -6,14 +6,14 @@ use tauri::{AppHandle, Listener};
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct DisplayInfo {
|
pub struct DisplayInfo {
|
||||||
pub primary: String,
|
pub primary: Option<String>,
|
||||||
pub set: Option<DisplaySet>,
|
pub set: Option<DisplaySet>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DisplayInfo {
|
impl Default for DisplayInfo {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DisplayInfo {
|
DisplayInfo {
|
||||||
primary: "default".to_owned(),
|
primary: None,
|
||||||
set: query_displays().ok(),
|
set: query_displays().ok(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -60,7 +60,7 @@ impl Display {
|
|||||||
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
.ok_or_else(|| anyhow!("Unable to query display settings"))?;
|
||||||
|
|
||||||
let res = DisplayInfo {
|
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()),
|
set: Some(display_set.clone()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -132,12 +132,14 @@ impl Display {
|
|||||||
let display_set = info.set.as_ref()
|
let display_set = info.set.as_ref()
|
||||||
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
.ok_or_else(|| anyhow!("Unable to clean up displays: no display set"))?;
|
||||||
|
|
||||||
|
if let Some(info_primary) = &info.primary {
|
||||||
let primary = display_set
|
let primary = display_set
|
||||||
.displays()
|
.displays()
|
||||||
.find(|display| display.name() == info.primary)
|
.find(|display| display.name() == info_primary)
|
||||||
.ok_or_else(|| anyhow!("Display {} not found", info.primary))?;
|
.ok_or_else(|| anyhow!("Display {} not found", info_primary))?;
|
||||||
|
|
||||||
primary.set_primary()?;
|
primary.set_primary()?;
|
||||||
|
}
|
||||||
|
|
||||||
display_set.apply()?;
|
display_set.apply()?;
|
||||||
displayz::refresh()?;
|
displayz::refresh()?;
|
||||||
|
@ -75,6 +75,12 @@ impl Keyboard {
|
|||||||
|
|
||||||
// This is assumed to run in sync after the segatools module
|
// This is assumed to run in sync after the segatools module
|
||||||
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
|
pub fn line_up(&self, ini: &mut Ini) -> Result<()> {
|
||||||
|
if let Some(enable) = ini.section(Some("io4")).and_then(|s| s.get("enable")) {
|
||||||
|
// io4 was disabled by the Segatools module -> abort
|
||||||
|
if enable == "0" {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
match self {
|
match self {
|
||||||
Keyboard::Ongeki(kb) => {
|
Keyboard::Ongeki(kb) => {
|
||||||
if kb.enabled {
|
if kb.enabled {
|
||||||
@ -95,7 +101,19 @@ impl Keyboard {
|
|||||||
.set("mouse", if kb.use_mouse { "1" } else { "0" });
|
.set("mouse", if kb.use_mouse { "1" } else { "0" });
|
||||||
} else {
|
} else {
|
||||||
ini.with_section(Some("io4"))
|
ini.with_section(Some("io4"))
|
||||||
.set("enable", "0");
|
.set("test", "0")
|
||||||
|
.set("service", "0")
|
||||||
|
.set("coin", "0")
|
||||||
|
.set("left1", "0")
|
||||||
|
.set("left2", "0")
|
||||||
|
.set("left3", "0")
|
||||||
|
.set("right1", "0")
|
||||||
|
.set("right2", "0")
|
||||||
|
.set("right3", "0")
|
||||||
|
.set("leftSide", "0")
|
||||||
|
.set("rightSide", "0")
|
||||||
|
.set("leftMenu", "0")
|
||||||
|
.set("rightMenu", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Keyboard::Chunithm(kb) => {
|
Keyboard::Chunithm(kb) => {
|
||||||
@ -109,14 +127,21 @@ impl Keyboard {
|
|||||||
ini.with_section(Some("io3"))
|
ini.with_section(Some("io3"))
|
||||||
.set("test", kb.test.to_string())
|
.set("test", kb.test.to_string())
|
||||||
.set("service", kb.svc.to_string())
|
.set("service", kb.svc.to_string())
|
||||||
.set("coin", kb.coin.to_string())
|
.set("coin", kb.coin.to_string());
|
||||||
.set("ir", "0");
|
|
||||||
} else {
|
} else {
|
||||||
ini.with_section(Some("io4"))
|
for (i, _) in kb.cell.iter().enumerate() {
|
||||||
.set("enable", "0");
|
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), "0");
|
||||||
ini.with_section(Some("slider"))
|
|
||||||
.set("enable", "0");
|
|
||||||
}
|
}
|
||||||
|
for (i, _) in kb.ir.iter().enumerate() {
|
||||||
|
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), "0");
|
||||||
|
}
|
||||||
|
ini.with_section(Some("io3"))
|
||||||
|
.set("test", "0")
|
||||||
|
.set("service", "0")
|
||||||
|
.set("coin", "0");
|
||||||
|
}
|
||||||
|
ini.with_section(Some("io3"))
|
||||||
|
.set("ir", "0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::path::{PathBuf, Path};
|
use std::path::{PathBuf, Path};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use ini::Ini;
|
use ini::Ini;
|
||||||
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
|
use crate::{model::{misc::{ConfigHook, ConfigHookAime, ConfigHookAimeUnit, ConfigHookAuth, Game}, profile::{Aime, IOSelection, Segatools}}, profiles::ProfilePaths, util::{self, PathStr}};
|
||||||
use crate::pkg_store::PackageStore;
|
use crate::pkg_store::PackageStore;
|
||||||
|
|
||||||
impl Segatools {
|
impl Segatools {
|
||||||
@ -21,8 +21,8 @@ impl Segatools {
|
|||||||
if let Some(key) = &self.hook {
|
if let Some(key) = &self.hook {
|
||||||
remove_if_nonpresent!(self.hook, key, None, store);
|
remove_if_nonpresent!(self.hook, key, None, store);
|
||||||
}
|
}
|
||||||
if let Some(key) = &self.io {
|
if let IOSelection::Custom(key) = &self.io2 {
|
||||||
remove_if_nonpresent!(self.io, key, None, store);
|
remove_if_nonpresent!(self.io2, key, IOSelection::default(), store);
|
||||||
}
|
}
|
||||||
match &self.aime {
|
match &self.aime {
|
||||||
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
Aime::AMNet(key) => remove_if_nonpresent!(self.aime, key, Aime::BuiltIn, store),
|
||||||
@ -134,6 +134,9 @@ impl Segatools {
|
|||||||
if self.amnet.name.len() > 0 {
|
if self.amnet.name.len() > 0 {
|
||||||
aimeio.set("serverName", &self.amnet.name);
|
aimeio.set("serverName", &self.amnet.name);
|
||||||
}
|
}
|
||||||
|
} else if let Aime::Other(key) = &self.aime {
|
||||||
|
ini_out.with_section(Some("aimeio"))
|
||||||
|
.set("path", util::pkg_dir().join(key.to_string()).join("segatools").join("aimeio.dll").stringify()?);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ini_out.with_section(Some("aime"))
|
ini_out.with_section(Some("aime"))
|
||||||
@ -141,7 +144,7 @@ impl Segatools {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if game == Game::Ongeki {
|
if game == Game::Ongeki {
|
||||||
if let Some(io) = &self.io {
|
if let IOSelection::Custom(io) = &self.io2 {
|
||||||
ini_out.with_section(Some("mu3io"))
|
ini_out.with_section(Some("mu3io"))
|
||||||
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||||
} else {
|
} else {
|
||||||
@ -149,6 +152,44 @@ impl Segatools {
|
|||||||
.set("path", "");
|
.set("path", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
match game {
|
||||||
|
Game::Ongeki => {
|
||||||
|
match &self.io2 {
|
||||||
|
IOSelection::Custom(io) => {
|
||||||
|
ini_out.with_section(Some("mu3io"))
|
||||||
|
.set("path", util::pkg_dir().join(io.to_string()).join("segatools").join("mu3io.dll").stringify()?);
|
||||||
|
}
|
||||||
|
IOSelection::SegatoolsBuiltIn => {
|
||||||
|
ini_out.with_section(Some("mu3io"))
|
||||||
|
.set("path", "");
|
||||||
|
}
|
||||||
|
IOSelection::Hardware => {
|
||||||
|
ini_out.with_section(Some("io4"))
|
||||||
|
.set("enable", "0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Game::Chunithm => {
|
||||||
|
match &self.io2 {
|
||||||
|
IOSelection::Custom(io) => {
|
||||||
|
ini_out.with_section(Some("chuniio"))
|
||||||
|
.set("path32", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio32.dll").stringify()?)
|
||||||
|
.set("path64", util::pkg_dir().join(io.to_string()).join("segatools").join("chuniio64.dll").stringify()?);
|
||||||
|
}
|
||||||
|
IOSelection::SegatoolsBuiltIn => {
|
||||||
|
ini_out.with_section(Some("chuniio"))
|
||||||
|
.set("path32", "")
|
||||||
|
.set("path64", "");
|
||||||
|
}
|
||||||
|
IOSelection::Hardware => {
|
||||||
|
ini_out.with_section(Some("io4"))
|
||||||
|
.set("enable", "0");
|
||||||
|
ini_out.with_section(Some("slider"))
|
||||||
|
.set("enable", "0");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
log::debug!("option dir: {:?} -> {:?}", opt_dir_in, opt_dir_out);
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ pub struct Remote {
|
|||||||
pub nsfw: bool,
|
pub nsfw: bool,
|
||||||
pub categories: Vec<String>,
|
pub categories: Vec<String>,
|
||||||
pub dependencies: BTreeSet<PkgKey>,
|
pub dependencies: BTreeSet<PkgKey>,
|
||||||
|
pub file_size: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PkgKey {
|
impl PkgKey {
|
||||||
@ -112,7 +113,8 @@ impl Package {
|
|||||||
nsfw: p.has_nsfw_content,
|
nsfw: p.has_nsfw_content,
|
||||||
version: v.version_number,
|
version: v.version_number,
|
||||||
categories: p.categories,
|
categories: p.categories,
|
||||||
dependencies: Self::sanitize_deps(v.dependencies)
|
dependencies: Self::sanitize_deps(v.dependencies),
|
||||||
|
file_size: v.file_size
|
||||||
}),
|
}),
|
||||||
source: PackageSource::Rainy,
|
source: PackageSource::Rainy,
|
||||||
})
|
})
|
||||||
@ -128,7 +130,7 @@ impl Package {
|
|||||||
.unwrap()
|
.unwrap()
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
let status = Self::parse_status(&mft);
|
let status = Self::parse_status(&mft, &dir);
|
||||||
let dependencies = Self::sanitize_deps(mft.dependencies);
|
let dependencies = Self::sanitize_deps(mft.dependencies);
|
||||||
|
|
||||||
Ok(Package {
|
Ok(Package {
|
||||||
@ -221,9 +223,15 @@ impl Package {
|
|||||||
res
|
res
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_status(mft: &PackageManifest) -> Status {
|
fn parse_status(mft: &PackageManifest, dir: impl AsRef<Path>) -> Status {
|
||||||
if mft.installers.len() == 0 {
|
if mft.installers.len() == 0 {
|
||||||
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None }); //Unchecked
|
if dir.as_ref().join("post_load.ps1").exists() {
|
||||||
|
return Status::Unsupported;
|
||||||
|
}
|
||||||
|
if dir.as_ref().join("app").join("data").exists() {
|
||||||
|
return Status::Unsupported;
|
||||||
|
}
|
||||||
|
return Status::OK(make_bitflags!(Feature::Mod), DLLs { game: None, amd: None });
|
||||||
} else {
|
} else {
|
||||||
let mut flags = BitFlags::default();
|
let mut flags = BitFlags::default();
|
||||||
let mut game_dll = None;
|
let mut game_dll = None;
|
||||||
@ -233,20 +241,14 @@ impl Package {
|
|||||||
if id == "rainycolor" {
|
if id == "rainycolor" {
|
||||||
flags |= Feature::Mod;
|
flags |= Feature::Mod;
|
||||||
} else if id == "segatools" {
|
} else if id == "segatools" {
|
||||||
// Multiple features in the same dll (yubideck etc.) should be supported at some point
|
|
||||||
if let Some(serde_json::Value::String(module)) = installer.get("module") {
|
if let Some(serde_json::Value::String(module)) = installer.get("module") {
|
||||||
if module == "mu3hook" {
|
flags |= Self::parse_segatools_module(&module);
|
||||||
flags |= Feature::Mu3Hook;
|
}
|
||||||
} else if module == "chusanhook" {
|
if let Some(serde_json::Value::Array(arr)) = installer.get("module") {
|
||||||
flags |= Feature::ChusanHook;
|
for elem in arr {
|
||||||
} else if module == "amnet" {
|
if let serde_json::Value::String(module) = elem {
|
||||||
flags |= Feature::AMNet | Feature::Aime;
|
flags |= Self::parse_segatools_module(module);
|
||||||
} else if module == "aimeio" {
|
}
|
||||||
flags |= Feature::Aime;
|
|
||||||
} else if module == "mu3io" {
|
|
||||||
flags |= Feature::Mu3IO;
|
|
||||||
} else if module == "chuniio" {
|
|
||||||
flags |= Feature::ChuniIO;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if id == "native_mod" {
|
} else if id == "native_mod" {
|
||||||
@ -274,4 +276,16 @@ impl Package {
|
|||||||
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
|
Status::OK(flags, DLLs { game: game_dll, amd: amd_dll })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn parse_segatools_module(module: &str) -> BitFlags<Feature, u16> {
|
||||||
|
match module {
|
||||||
|
"mu3hook" => make_bitflags!(Feature::Mu3Hook),
|
||||||
|
"chusanhook" => make_bitflags!(Feature::ChusanHook),
|
||||||
|
"amnet" => make_bitflags!(Feature::{AMNet | Aime}),
|
||||||
|
"aimeio" => make_bitflags!(Feature::Aime),
|
||||||
|
"mu3io" => make_bitflags!(Feature::Mu3IO),
|
||||||
|
"chuniio" => make_bitflags!(Feature::ChuniIO),
|
||||||
|
_ => BitFlags::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -21,7 +21,7 @@ pub struct PackageStore {
|
|||||||
offline: bool,
|
offline: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize)]
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
pub struct Payload {
|
pub struct Payload {
|
||||||
pub pkg: PkgKey
|
pub pkg: PkgKey
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||||
use crate::{model::{misc::Game, patch::{PatchFileVec, PatchSelection}, profile::{Aime, ChunithmKeyboard, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
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 tauri::Emitter;
|
use tauri::Emitter;
|
||||||
use std::process::Stdio;
|
use std::process::Stdio;
|
||||||
use crate::model::profile::BepInEx;
|
use crate::model::profile::BepInEx;
|
||||||
@ -59,9 +59,15 @@ impl Profile {
|
|||||||
log::debug!("{:?}", data);
|
log::debug!("{:?}", data);
|
||||||
|
|
||||||
// Backwards compat
|
// Backwards compat
|
||||||
if game == Game::Ongeki && data.keyboard.is_none() {
|
if game == Game::Ongeki {
|
||||||
|
if data.keyboard.is_none() {
|
||||||
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
|
data.keyboard = Some(Keyboard::Ongeki(OngekiKeyboard::default()));
|
||||||
}
|
}
|
||||||
|
if let Some(io) = data.sgt.io {
|
||||||
|
data.sgt.io2 = IOSelection::Custom(io);
|
||||||
|
data.sgt.io = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
if game == Game::Chunithm {
|
if game == Game::Chunithm {
|
||||||
if data.keyboard.is_none() {
|
if data.keyboard.is_none() {
|
||||||
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
|
data.keyboard = Some(Keyboard::Chunithm(ChunithmKeyboard::default()));
|
||||||
@ -112,7 +118,7 @@ impl Profile {
|
|||||||
if let Some(hook) = &self.data.sgt.hook {
|
if let Some(hook) = &self.data.sgt.hook {
|
||||||
res.push(hook.clone());
|
res.push(hook.clone());
|
||||||
}
|
}
|
||||||
if let Some(io) = &self.data.sgt.io {
|
if let IOSelection::Custom(io) = &self.data.sgt.io2 {
|
||||||
res.push(io.clone());
|
res.push(io.clone());
|
||||||
}
|
}
|
||||||
if let Aime::AMNet(aime) = &self.data.sgt.aime {
|
if let Aime::AMNet(aime) = &self.data.sgt.aime {
|
||||||
|
@ -174,3 +174,43 @@ pub async fn remove_dir_all(path: impl AsRef<Path>) -> Result<()> {
|
|||||||
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
|
Err(anyhow!("invalid remove_dir_all target: not in a data directory"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
pub fn create_shortcut(
|
||||||
|
apph: AppHandle,
|
||||||
|
meta: &crate::profiles::ProfileMeta
|
||||||
|
) -> Result<()> {
|
||||||
|
use winsafe::{co, prelude::{ole_IPersistFile, ole_IUnknown, shell_IShellLink}, CoCreateInstance, CoInitializeEx, IPersistFile};
|
||||||
|
let _com_guard = CoInitializeEx(
|
||||||
|
co::COINIT::APARTMENTTHREADED
|
||||||
|
| co::COINIT::DISABLE_OLE1DDE,
|
||||||
|
)?;
|
||||||
|
let obj = CoCreateInstance::<winsafe::IShellLink>(
|
||||||
|
&co::CLSID::ShellLink,
|
||||||
|
None,
|
||||||
|
co::CLSCTX::INPROC_SERVER,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let target_dir = apph.path().cache_dir()?.join(NAME);
|
||||||
|
let target_path = target_dir.join("startliner.exe");
|
||||||
|
let lnk_path = apph.path().desktop_dir()?.join(format!("{} {}.lnk", &meta.game.print(), &meta.name));
|
||||||
|
|
||||||
|
obj.SetPath(target_path.to_str().ok_or_else(|| anyhow!("Illegal target path"))?)?;
|
||||||
|
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"))?,
|
||||||
|
0
|
||||||
|
)?;
|
||||||
|
|
||||||
|
match meta.game {
|
||||||
|
Game::Ongeki => std::fs::write(target_dir.join("icon-ongeki.ico"), include_bytes!("../../res/icon-ongeki.ico")),
|
||||||
|
Game::Chunithm => std::fs::write(target_dir.join("icon-chunithm.ico"), include_bytes!("../../res/icon-chunithm.ico"))
|
||||||
|
}?;
|
||||||
|
|
||||||
|
let file = obj.QueryInterface::<IPersistFile>()?;
|
||||||
|
|
||||||
|
file.Save(Some(lnk_path.to_str().ok_or_else(|| anyhow!("Illegal shortcut path"))?), true)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "STARTLINER",
|
"productName": "STARTLINER",
|
||||||
"version": "0.7.0",
|
"version": "0.11.0",
|
||||||
"identifier": "zip.patafour.startliner",
|
"identifier": "zip.patafour.startliner",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
@ -66,7 +66,7 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"icon": ["icons/slow.png", "icons/slow.ico"],
|
"icon": ["icons/icon.png", "icons/icon.ico"],
|
||||||
"createUpdaterArtifacts": true
|
"createUpdaterArtifacts": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,7 +28,9 @@ import {
|
|||||||
usePrfStore,
|
usePrfStore,
|
||||||
} from '../stores';
|
} from '../stores';
|
||||||
import { Dirs } from '../types';
|
import { Dirs } from '../types';
|
||||||
import { messageSplit } from '../util';
|
import { messageSplit, shouldPreferDark } from '../util';
|
||||||
|
|
||||||
|
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
|
||||||
|
|
||||||
const pkg = usePkgStore();
|
const pkg = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
@ -80,6 +82,66 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
|||||||
errorMessage.value = event.payload.message;
|
errorMessage.value = event.payload.message;
|
||||||
errorHeader.value = event.payload.header;
|
errorHeader.value = event.payload.header;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
listen<string>('launch-error', (event) => {
|
||||||
|
errorVisible.value = true;
|
||||||
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -93,6 +155,16 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
|||||||
? 'main-scale-l'
|
? 'main-scale-l'
|
||||||
: 'main-scale-xl'
|
: '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>
|
<ConfirmDialog>
|
||||||
<template #message="{ message }">
|
<template #message="{ message }">
|
||||||
@ -122,7 +194,7 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
|||||||
{{ errorMessage }}
|
{{ errorMessage }}
|
||||||
<Button
|
<Button
|
||||||
class="m-auto"
|
class="m-auto"
|
||||||
label="A sad state of affairs"
|
label="OK"
|
||||||
@click="errorVisible = false"
|
@click="errorVisible = false"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -154,7 +226,10 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
|||||||
><div class="pi pi-box"></div
|
><div class="pi pi-box"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab
|
<Tab
|
||||||
v-if="prf.current?.meta.game === 'chunithm'"
|
v-if="
|
||||||
|
prf.current?.meta.game === 'chunithm' &&
|
||||||
|
prf.current.data.sgt.target.length > 0
|
||||||
|
"
|
||||||
value="patches"
|
value="patches"
|
||||||
><div class="pi pi-ticket"></div
|
><div class="pi pi-ticket"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
@ -255,6 +330,20 @@ listen<{ message: string; header: string }>('invoke-error', (event) => {
|
|||||||
<div v-else>
|
<div v-else>
|
||||||
Patches require <code>mempatcher</code> to be installed
|
Patches require <code>mempatcher</code> to be installed
|
||||||
and enabled.
|
and enabled.
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
label="Add mempatcher"
|
||||||
|
icon="pi pi-plus"
|
||||||
|
class="mt-3"
|
||||||
|
@click="
|
||||||
|
() =>
|
||||||
|
pkg.installFromKey(
|
||||||
|
'mempatcher-mempatcher'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel value="info">
|
<TabPanel value="info">
|
||||||
@ -328,4 +417,23 @@ body {
|
|||||||
.p-progressbar-label {
|
.p-progressbar-label {
|
||||||
transition-duration: 0s !important;
|
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>
|
</style>
|
||||||
|
@ -65,7 +65,7 @@ const filePick = async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
|
<Button v-if="!exists" icon="pi pi-plus" size="small" @click="filePick" />
|
||||||
<div v-else>
|
<div class="primitive-base" v-else>
|
||||||
<Button
|
<Button
|
||||||
v-if="exists"
|
v-if="exists"
|
||||||
icon="pi pi-pen-to-square"
|
icon="pi pi-pen-to-square"
|
||||||
@ -102,12 +102,20 @@ const filePick = async () => {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: 10vh;
|
top: 50%;
|
||||||
left: 10vw;
|
left: 50%;
|
||||||
height: 80vh;
|
height: 500px;
|
||||||
width: 80vw;
|
width: 800px;
|
||||||
|
margin-left: -400px;
|
||||||
|
margin-top: -250px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 20px;
|
||||||
background-color: #151515;
|
background-color: #151515;
|
||||||
color: #ddd;
|
color: #ddd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.primitive-base ::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -16,7 +16,7 @@ invoke('get_changelog').then((s) => (changelog.value = s as string));
|
|||||||
O.N.G.E.K.I. and CHUNITHM.
|
O.N.G.E.K.I. and CHUNITHM.
|
||||||
<h1>Changelog</h1>
|
<h1>Changelog</h1>
|
||||||
<ScrollPanel style="height: 200px">
|
<ScrollPanel style="height: 200px">
|
||||||
<div class="changelog">
|
<div class="markdown">
|
||||||
<vue-markdown-it
|
<vue-markdown-it
|
||||||
:source="changelog"
|
:source="changelog"
|
||||||
:options="{ typographer: true, breaks: true }"
|
:options="{ typographer: true, breaks: true }"
|
||||||
@ -44,13 +44,20 @@ h1 {
|
|||||||
font-size: 1.7rem;
|
font-size: 1.7rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.changelog h2 {
|
.markdown h3 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown h2 {
|
||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
}
|
}
|
||||||
.changelog ul {
|
.markdown ul {
|
||||||
list-style-type: circle;
|
list-style-type: circle;
|
||||||
}
|
}
|
||||||
.changelog li {
|
.markdown li {
|
||||||
margin-left: 40px;
|
margin-left: 40px;
|
||||||
}
|
}
|
||||||
|
.markdown a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
@ -11,20 +12,26 @@ const props = defineProps({
|
|||||||
pkg: Object as () => Package,
|
pkg: Object as () => Package,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const deleting = ref(false);
|
||||||
|
|
||||||
const remove = async () => {
|
const remove = async () => {
|
||||||
if (props.pkg === undefined) {
|
if (props.pkg === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleting.value = true;
|
||||||
|
|
||||||
await invoke('delete_package', {
|
await invoke('delete_package', {
|
||||||
key: pkgKey(props.pkg),
|
key: pkgKey(props.pkg),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
deleting.value = false;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
v-if="pkg?.loc && !pkg?.js.busy"
|
v-if="pkg?.loc && !pkg?.js.downloading"
|
||||||
rounded
|
rounded
|
||||||
icon="pi pi-trash"
|
icon="pi pi-trash"
|
||||||
severity="danger"
|
severity="danger"
|
||||||
@ -32,7 +39,7 @@ const remove = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="self-center ml-4"
|
class="self-center ml-4"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
:loading="pkg?.js.busy"
|
:loading="deleting"
|
||||||
v-on:click="remove()"
|
v-on:click="remove()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -45,7 +52,7 @@ const remove = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="self-center ml-4"
|
class="self-center ml-4"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
:loading="pkg?.js.busy"
|
:loading="pkg?.js.downloading"
|
||||||
v-on:click="async () => await pkgs.install(pkg)"
|
v-on:click="async () => await pkgs.install(pkg)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref } from 'vue';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import { fromKeycode, toKeycode } from '../keyboard';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
import { OngekiButtons } from '../types';
|
import { OngekiButtons } from '../types';
|
||||||
|
|
||||||
@ -15,9 +16,51 @@ const handleKey = (
|
|||||||
) => {
|
) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const keycode = toKeycode(event.code);
|
let keycode = toKeycode(event.code);
|
||||||
|
|
||||||
if (keycode !== null && button !== undefined) {
|
if (keycode !== null && button !== undefined) {
|
||||||
const data = prf.current!.data.keyboard!.data as any;
|
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 (index !== undefined) {
|
if (index !== undefined) {
|
||||||
data[button][index] = keycode;
|
data[button][index] = keycode;
|
||||||
} else {
|
} else {
|
||||||
@ -75,7 +118,7 @@ const handleMouse = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getKey = (key: keyof OngekiButtons, index?: number) =>
|
const getKey = (key: keyof OngekiButtons, index?: number): any =>
|
||||||
computed(() => {
|
computed(() => {
|
||||||
const data = prf.current!.data.keyboard?.data as any;
|
const data = prf.current!.data.keyboard?.data as any;
|
||||||
const keycode =
|
const keycode =
|
||||||
@ -85,147 +128,45 @@ const getKey = (key: keyof OngekiButtons, index?: number) =>
|
|||||||
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '–';
|
return keycode && fromKeycode(keycode) ? fromKeycode(keycode) : '–';
|
||||||
});
|
});
|
||||||
|
|
||||||
const KEY_MAP: { [key: number]: string } = {
|
const props = defineProps({
|
||||||
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({
|
|
||||||
small: Boolean,
|
small: Boolean,
|
||||||
verySmall: Boolean,
|
|
||||||
tall: Boolean,
|
tall: Boolean,
|
||||||
tooltip: String,
|
tooltip: String,
|
||||||
button: String,
|
button: String,
|
||||||
color: String,
|
color: String,
|
||||||
index: Number,
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<InputText
|
<InputText
|
||||||
:style="{
|
:style="{
|
||||||
width: small ? '3em' : '5em',
|
width: small ? '2.8rem' : '5rem',
|
||||||
height: small ? '3em' : tall ? '10em' : '5em',
|
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||||
fontSize: small ? '0.9em' : '1em',
|
fontSize,
|
||||||
backgroundColor: color,
|
backgroundColor: color,
|
||||||
}"
|
}"
|
||||||
unstyled
|
unstyled
|
||||||
class="text-center buttoninputtext"
|
class="text-center buttoninputtext"
|
||||||
v-tooltip="tooltip"
|
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||||
@contextmenu.prevent="() => {}"
|
@contextmenu.prevent="() => {}"
|
||||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||||
@mousedown="
|
@mousedown="
|
||||||
@ -233,7 +174,7 @@ defineProps({
|
|||||||
handleMouse(button as keyof OngekiButtons, ev, index)
|
handleMouse(button as keyof OngekiButtons, ev, index)
|
||||||
"
|
"
|
||||||
@focusout="() => (hasClickedM1Once = false)"
|
@focusout="() => (hasClickedM1Once = false)"
|
||||||
:model-value="getKey(button as keyof OngekiButtons, index) as any"
|
:model-value="modelValue"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -241,5 +182,7 @@ defineProps({
|
|||||||
.buttoninputtext {
|
.buttoninputtext {
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
border: 1px solid rgba(200, 200, 200, 0.3);
|
border: 1px solid rgba(200, 200, 200, 0.3);
|
||||||
|
overflow: scroll !important;
|
||||||
|
text-align: center !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -15,7 +15,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
const pkgs = usePkgStore();
|
const pkgs = usePkgStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
const groupCallIndex = ref(0);
|
|
||||||
const empty = ref(false);
|
const empty = ref(false);
|
||||||
const gameSublist: Ref<string[]> = ref([]);
|
const gameSublist: Ref<string[]> = ref([]);
|
||||||
|
|
||||||
@ -46,10 +45,7 @@ const group = computed(() => {
|
|||||||
({ namespace }) => namespace
|
({ namespace }) => namespace
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (groupCallIndex.value > 0) {
|
|
||||||
empty.value = Object.keys(res).length === 0;
|
empty.value = Object.keys(res).length === 0;
|
||||||
}
|
|
||||||
groupCallIndex.value += 1;
|
|
||||||
return res;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -26,19 +26,32 @@ const model = computed({
|
|||||||
await prf.togglePkg(props.pkg, value);
|
await prf.togglePkg(props.pkg, value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const unsupported = computed(() => props.pkg!.loc!.status === 'Unsupported');
|
||||||
|
|
||||||
|
if (unsupported.value === true && model.value === true) {
|
||||||
|
prf.togglePkg(props.pkg, false);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||||
<UpdateButton :pkg="pkg" />
|
<UpdateButton :pkg="pkg" />
|
||||||
|
<span
|
||||||
|
v-tooltip="
|
||||||
|
unsupported &&
|
||||||
|
'This package is currently incompatible with STARTLINER.'
|
||||||
|
"
|
||||||
|
>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
v-if="hasFeature(pkg, Feature.Mod)"
|
v-if="hasFeature(pkg, Feature.Mod) || unsupported === true"
|
||||||
class="scale-[1.33] shrink-0"
|
class="scale-[1.33] shrink-0"
|
||||||
inputId="switch"
|
inputId="switch"
|
||||||
:disabled="pkg!.loc!.status === 'Unsupported'"
|
:disabled="unsupported === true"
|
||||||
v-model="model"
|
v-model="model"
|
||||||
/>
|
/>
|
||||||
|
</span>
|
||||||
<InstallButton :pkg="pkg" />
|
<InstallButton :pkg="pkg" />
|
||||||
<Button
|
<Button
|
||||||
rounded
|
rounded
|
||||||
|
@ -38,7 +38,7 @@ const iconSrc = computed(() => {
|
|||||||
<label class="m-3 align-middle text grow z-5 h-50px">
|
<label class="m-3 align-middle text grow z-5 h-50px">
|
||||||
<div>
|
<div>
|
||||||
<span class="text-lg">
|
<span class="text-lg">
|
||||||
{{ pkg?.name ?? 'Untitled' }}
|
{{ pkg?.name.replaceAll('_', ' ') ?? 'Untitled' }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="pkg?.rmt?.deprecated"
|
v-if="pkg?.rmt?.deprecated"
|
||||||
|
155
src/components/Onboarding.vue
Normal file
@ -0,0 +1,155 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ComputedRef, computed, onMounted } 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';
|
||||||
|
|
||||||
|
const prf = usePrfStore();
|
||||||
|
const client = useClientStore();
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
visible: Boolean,
|
||||||
|
firstTime: Boolean,
|
||||||
|
onFinish: Function,
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Datum {
|
||||||
|
text: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const game = computed(() => prf.current?.meta.game);
|
||||||
|
|
||||||
|
const processText = (s: string) => {
|
||||||
|
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} or a button on the back of the controller`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return s.replace('%TESTMENU%', 'a button on the back of the controller');
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadPage = async (title: string) => {
|
||||||
|
return {
|
||||||
|
text: await (await fetch(`/help-${title}.md`)).text(),
|
||||||
|
image: `help-${title}.png`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
let systemProcessing: Datum;
|
||||||
|
let standardOngeki: Datum;
|
||||||
|
let standardChunithm: Datum;
|
||||||
|
let lever: Datum;
|
||||||
|
let server: Datum;
|
||||||
|
let finaleOngeki: Datum;
|
||||||
|
let finaleChunithm: Datum;
|
||||||
|
|
||||||
|
const data: ComputedRef<Datum[]> = computed(() => {
|
||||||
|
const res = [];
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
[standardOngeki, systemProcessing, lever, server, finaleOngeki] =
|
||||||
|
await Promise.all([
|
||||||
|
loadPage('standard'),
|
||||||
|
loadPage('ongeki-system-processing'),
|
||||||
|
loadPage('ongeki-lever'),
|
||||||
|
loadPage('chunithm-server'),
|
||||||
|
loadPage('finale'),
|
||||||
|
]);
|
||||||
|
standardOngeki = {
|
||||||
|
...standardOngeki,
|
||||||
|
image: '/help-standard-ongeki.png',
|
||||||
|
};
|
||||||
|
standardChunithm = {
|
||||||
|
...standardOngeki,
|
||||||
|
image: '/help-standard-chunithm.png',
|
||||||
|
};
|
||||||
|
finaleOngeki = {
|
||||||
|
...finaleOngeki,
|
||||||
|
image: '/help-finale-ongeki.png',
|
||||||
|
};
|
||||||
|
finaleChunithm = {
|
||||||
|
...finaleOngeki,
|
||||||
|
image: '/help-finale-chunithm.png',
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</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 }"
|
||||||
|
>
|
||||||
|
<Carousel :value="data" :num-visible="1" :num-scroll="1">
|
||||||
|
<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
|
||||||
|
class="m-auto"
|
||||||
|
label="OK"
|
||||||
|
@click="() => onFinish && onFinish()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css">
|
||||||
|
.p-dialog ::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-container {
|
||||||
|
height: 9.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
@ -3,7 +3,7 @@ import InputNumber from 'primevue/inputnumber';
|
|||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import OptionRow from './OptionRow.vue';
|
import OptionRow from './OptionRow.vue';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
import { Patch } from '@/types';
|
import { Patch } from '../types';
|
||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
import InputText from 'primevue/inputtext';
|
import InputText from 'primevue/inputtext';
|
||||||
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import * as path from '@tauri-apps/api/path';
|
import * as path from '@tauri-apps/api/path';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { useGeneralStore, usePrfStore } from '../stores';
|
import { useGeneralStore, usePrfStore } from '../stores';
|
||||||
@ -9,6 +10,8 @@ import { ProfileMeta } from '../types';
|
|||||||
|
|
||||||
const general = useGeneralStore();
|
const general = useGeneralStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
const confirmDialog = useConfirm();
|
||||||
|
|
||||||
const isEditing = ref(false);
|
const isEditing = ref(false);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -54,6 +57,14 @@ const deleteProfile = async () => {
|
|||||||
await prf.reloadList();
|
await prf.reloadList();
|
||||||
await prf.reload();
|
await prf.reload();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const promptDeleteProfile = async () => {
|
||||||
|
confirmDialog.require({
|
||||||
|
message: `Are you sure you want to delete ${props.p?.game}-${props.p?.name}?`,
|
||||||
|
header: 'Delete profile',
|
||||||
|
accept: deleteProfile,
|
||||||
|
});
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -90,7 +101,7 @@ const deleteProfile = async () => {
|
|||||||
size="small"
|
size="small"
|
||||||
class="self-center ml-2"
|
class="self-center ml-2"
|
||||||
style="width: 2rem; height: 2rem"
|
style="width: 2rem; height: 2rem"
|
||||||
@click="deleteProfile"
|
@click="promptDeleteProfile"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
rounded
|
rounded
|
||||||
|
@ -5,10 +5,12 @@ import ContextMenu from 'primevue/contextmenu';
|
|||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from 'primevue/useconfirm';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||||
|
import Onboarding from './Onboarding.vue';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePrfStore } from '../stores';
|
import { useClientStore, usePrfStore } from '../stores';
|
||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
const client = useClientStore();
|
||||||
const confirmDialog = useConfirm();
|
const confirmDialog = useConfirm();
|
||||||
|
|
||||||
type StartStatus = 'ready' | 'preparing' | 'running';
|
type StartStatus = 'ready' | 'preparing' | 'running';
|
||||||
@ -85,10 +87,20 @@ listen('launch-end', () => {
|
|||||||
getCurrentWindow().setFocus();
|
getCurrentWindow().setFocus();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createShortcut = async () => {
|
||||||
|
const current = prf.current;
|
||||||
|
if (current !== null) {
|
||||||
|
await invoke('create_shortcut', {
|
||||||
|
profileMeta: current.meta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const menuItems = [
|
||||||
{
|
{
|
||||||
label: 'Refresh and start',
|
label: 'Refresh and start',
|
||||||
icon: 'pi pi-sync',
|
icon: 'pi pi-sync',
|
||||||
|
tooltip: 'test',
|
||||||
command: async () => await startline(false, true),
|
command: async () => await startline(false, true),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -96,6 +108,19 @@ const menuItems = [
|
|||||||
icon: 'pi pi-exclamation-circle',
|
icon: 'pi pi-exclamation-circle',
|
||||||
command: async () => await startline(true, false),
|
command: async () => await startline(true, false),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Create desktop shortcut',
|
||||||
|
icon: 'pi pi-link',
|
||||||
|
command: createShortcut,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Help',
|
||||||
|
icon: 'pi pi-question-circle',
|
||||||
|
command: () => {
|
||||||
|
onboardingFirstTime.value = false;
|
||||||
|
onboardingVisible.value = true;
|
||||||
|
},
|
||||||
|
},
|
||||||
];
|
];
|
||||||
const menu = ref();
|
const menu = ref();
|
||||||
|
|
||||||
@ -103,9 +128,38 @@ const showContextMenu = (event: Event) => {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
menu.value.show(event);
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Onboarding
|
||||||
|
:visible="onboardingVisible"
|
||||||
|
:first-time="onboardingFirstTime"
|
||||||
|
:on-finish="
|
||||||
|
() => {
|
||||||
|
onboardingVisible = false;
|
||||||
|
if (onboardingFirstTime === true) {
|
||||||
|
startline(false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
<ContextMenu ref="menu" :model="menuItems" />
|
<ContextMenu ref="menu" :model="menuItems" />
|
||||||
<Button
|
<Button
|
||||||
v-if="startStatus === 'ready'"
|
v-if="startStatus === 'ready'"
|
||||||
@ -116,7 +170,7 @@ const showContextMenu = (event: Event) => {
|
|||||||
aria-label="start"
|
aria-label="start"
|
||||||
size="small"
|
size="small"
|
||||||
class="m-2.5"
|
class="m-2.5"
|
||||||
@click="startline(false, false)"
|
@click="tryStart"
|
||||||
@contextmenu="showContextMenu"
|
@contextmenu="showContextMenu"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
@ -20,17 +20,15 @@ const install = async () => {
|
|||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (props.pkg !== undefined) {
|
if (props.pkg !== undefined) {
|
||||||
props.pkg.js.busy = false;
|
props.pkg.js.downloading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (rv === 'Deferred') { /* download progress */ }
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button
|
<Button
|
||||||
v-if="needsUpdate(pkg) && !pkg?.js.busy"
|
v-if="needsUpdate(pkg) && !pkg?.js.downloading"
|
||||||
rounded
|
rounded
|
||||||
icon="pi pi-download"
|
icon="pi pi-download"
|
||||||
severity="success"
|
severity="success"
|
||||||
|
@ -84,6 +84,7 @@ load();
|
|||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow
|
<OptionRow
|
||||||
title="Aime code"
|
title="Aime code"
|
||||||
|
tooltip="Only applicable with the segatools built-in emulation or with compatible third-party packages"
|
||||||
v-if="prf.current!.data.sgt.aime !== 'Disabled'"
|
v-if="prf.current!.data.sgt.aime !== 'Disabled'"
|
||||||
>
|
>
|
||||||
<InputText
|
<InputText
|
||||||
|
@ -11,7 +11,10 @@ const prf = usePrfStore();
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<OptionCategory title="Keyboard">
|
<OptionCategory title="Keyboard">
|
||||||
<OptionRow title="Enable">
|
<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)"
|
||||||
|
>
|
||||||
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
|
<ToggleSwitch v-model="prf.current!.data.keyboard!.data.enabled" />
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow
|
<OptionRow
|
||||||
@ -30,6 +33,7 @@ const prf = usePrfStore();
|
|||||||
/>
|
/>
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
<div
|
<div
|
||||||
|
v-if="prf.current!.data.keyboard!.data.enabled"
|
||||||
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
|
:style="`position: relative; height: ${prf.current!.data.keyboard!.game === 'Ongeki' ? 400 : 250}px`"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
@ -120,7 +124,7 @@ const prf = usePrfStore();
|
|||||||
<div
|
<div
|
||||||
v-for="idx in Array(16)
|
v-for="idx in Array(16)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => 16 - i)"
|
.map((_, i) => 32 - 2 * i - 1)"
|
||||||
>
|
>
|
||||||
<KeyboardKey
|
<KeyboardKey
|
||||||
button="cell"
|
button="cell"
|
||||||
@ -138,7 +142,7 @@ const prf = usePrfStore();
|
|||||||
<div
|
<div
|
||||||
v-for="idx in Array(16)
|
v-for="idx in Array(16)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
.map((_, i) => 32 - i)"
|
.map((_, i) => 32 - 2 * i)"
|
||||||
>
|
>
|
||||||
<KeyboardKey
|
<KeyboardKey
|
||||||
button="cell"
|
button="cell"
|
||||||
|
@ -126,16 +126,27 @@ const checkSegatoolsIni = async (target: string) => {
|
|||||||
</OptionRow>
|
</OptionRow>
|
||||||
<OptionRow
|
<OptionRow
|
||||||
:title="names.io"
|
:title="names.io"
|
||||||
v-if="prf.current?.meta.game === 'ongeki'"
|
|
||||||
tooltip="IO plugins can be downloaded from the package store."
|
tooltip="IO plugins can be downloaded from the package store."
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
v-model="prf.current!.data.sgt.io"
|
v-model="prf.current!.data.sgt.io2"
|
||||||
placeholder="segatools built-in"
|
|
||||||
:options="[
|
:options="[
|
||||||
{ title: 'segatools built-in', value: null },
|
{ title: 'native io4', value: 'hardware' },
|
||||||
...pkgs.byFeature(Feature.Mu3IO).map((p) => {
|
{
|
||||||
return { title: pkgKey(p), value: pkgKey(p) };
|
title: 'segatools built-in (keyboard)',
|
||||||
|
value: 'segatools_built_in',
|
||||||
|
},
|
||||||
|
...pkgs
|
||||||
|
.byFeature(
|
||||||
|
prf.current?.meta.game === 'ongeki'
|
||||||
|
? Feature.Mu3IO
|
||||||
|
: Feature.ChuniIO
|
||||||
|
)
|
||||||
|
.map((p) => {
|
||||||
|
return {
|
||||||
|
title: pkgKey(p),
|
||||||
|
value: { custom: pkgKey(p) },
|
||||||
|
};
|
||||||
}),
|
}),
|
||||||
]"
|
]"
|
||||||
option-label="title"
|
option-label="title"
|
||||||
|
@ -34,6 +34,15 @@ const verboseModel = computed({
|
|||||||
await client.setVerbose(value);
|
await client.setVerbose(value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const themeModel = computed({
|
||||||
|
get() {
|
||||||
|
return client.theme;
|
||||||
|
},
|
||||||
|
async set(value: 'light' | 'dark' | 'system') {
|
||||||
|
await client.setTheme(value);
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -67,5 +76,18 @@ const verboseModel = computed({
|
|||||||
>
|
>
|
||||||
<ToggleSwitch v-model="verboseModel" />
|
<ToggleSwitch v-model="verboseModel" />
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
|
<OptionRow title="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"
|
||||||
|
/>
|
||||||
|
</OptionRow>
|
||||||
</OptionCategory>
|
</OptionCategory>
|
||||||
</template>
|
</template>
|
||||||
|
119
src/keyboard.ts
Normal 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;
|
||||||
|
};
|
@ -17,6 +17,9 @@ app.use(pinia);
|
|||||||
app.use(PrimeVue, {
|
app.use(PrimeVue, {
|
||||||
theme: {
|
theme: {
|
||||||
preset: Preset,
|
preset: Preset,
|
||||||
|
options: {
|
||||||
|
darkModeSelector: '.use-dark-mode',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
app.use(ConfirmationService);
|
app.use(ConfirmationService);
|
||||||
|
@ -6,7 +6,12 @@ import { PhysicalSize, getCurrentWindow } from '@tauri-apps/api/window';
|
|||||||
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
import { readTextFile, writeTextFile } from '@tauri-apps/plugin-fs';
|
||||||
import { invoke, invoke_nopopup } from './invoke';
|
import { invoke, invoke_nopopup } from './invoke';
|
||||||
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
|
import { Dirs, Feature, Game, Package, Profile, ProfileMeta } from './types';
|
||||||
import { changePrimaryColor, hasFeature, pkgKey } from './util';
|
import {
|
||||||
|
changePrimaryColor,
|
||||||
|
hasFeature,
|
||||||
|
pkgKey,
|
||||||
|
shouldPreferDark,
|
||||||
|
} from './util';
|
||||||
|
|
||||||
type InstallStatus = {
|
type InstallStatus = {
|
||||||
pkg: string;
|
pkg: string;
|
||||||
@ -114,13 +119,13 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
listen<InstallStatus>('install-start', async (ev) => {
|
listen<InstallStatus>('install-start', async (ev) => {
|
||||||
const key = ev.payload.pkg;
|
const key = ev.payload.pkg;
|
||||||
await this.reload(key);
|
await this.reload(key);
|
||||||
this.pkg[key].js.busy = true;
|
this.pkg[key].js.downloading = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
listen<InstallStatus>('install-end', async (ev) => {
|
listen<InstallStatus>('install-end', async (ev) => {
|
||||||
const key = ev.payload.pkg;
|
const key = ev.payload.pkg;
|
||||||
await this.reload(key);
|
await this.reload(key);
|
||||||
this.pkg[key].js.busy = false;
|
this.pkg[key].js.downloading = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -147,17 +152,22 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
|
|
||||||
async reloadWith(key: string, pkg: Package) {
|
async reloadWith(key: string, pkg: Package) {
|
||||||
if (this.pkg[key] === undefined) {
|
if (this.pkg[key] === undefined) {
|
||||||
this.pkg[key] = { js: { busy: false } } as Package;
|
this.pkg[key] = { js: { downloading: false } } as Package;
|
||||||
} else {
|
} else {
|
||||||
this.pkg[key].loc = null;
|
this.pkg[key].loc = null;
|
||||||
this.pkg[key].rmt = null;
|
this.pkg[key].rmt = null;
|
||||||
}
|
}
|
||||||
Object.assign(this.pkg[key], pkg);
|
Object.assign(this.pkg[key], pkg);
|
||||||
|
|
||||||
|
if (!pkg.js) {
|
||||||
|
pkg.js = { downloading: false };
|
||||||
|
}
|
||||||
|
|
||||||
if (pkg.rmt !== null) {
|
if (pkg.rmt !== null) {
|
||||||
pkg.rmt.categories.forEach((c) =>
|
pkg.rmt.categories.forEach((c) =>
|
||||||
this.availableCategories.add(c)
|
this.availableCategories.add(c)
|
||||||
);
|
);
|
||||||
|
pkg.js.downloading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -188,9 +198,8 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
|
||||||
if (pkg !== undefined) {
|
if (pkg !== undefined) {
|
||||||
pkg.js.busy = false;
|
pkg.js.downloading = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -329,7 +338,7 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
if (timeout !== null) {
|
if (timeout !== null) {
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
}
|
}
|
||||||
timeout = setTimeout(() => invoke('save_current_profile'), 2000);
|
timeout = setTimeout(() => invoke('save_current_profile'), 600);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -348,6 +357,10 @@ export const usePrfStore = defineStore('prf', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export enum ClientData {
|
||||||
|
Onboarded,
|
||||||
|
}
|
||||||
|
|
||||||
export const useClientStore = defineStore('client', () => {
|
export const useClientStore = defineStore('client', () => {
|
||||||
type ScaleType = 's' | 'm' | 'l' | 'xl';
|
type ScaleType = 's' | 'm' | 'l' | 'xl';
|
||||||
const scaleFactor: Ref<ScaleType> = ref('s');
|
const scaleFactor: Ref<ScaleType> = ref('s');
|
||||||
@ -356,16 +369,22 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
const offlineMode = ref(false);
|
const offlineMode = ref(false);
|
||||||
const enableAutoupdates = ref(true);
|
const enableAutoupdates = ref(true);
|
||||||
const verbose = ref(false);
|
const verbose = ref(false);
|
||||||
|
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
|
||||||
|
const onboarded: Ref<Game[]> = ref([]);
|
||||||
|
|
||||||
const scaleValue = (value: ScaleType) =>
|
const _scaleValue = (value: ScaleType) =>
|
||||||
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
||||||
|
|
||||||
|
const scaleValue = computed(() => {
|
||||||
|
return _scaleValue(scaleFactor.value);
|
||||||
|
});
|
||||||
|
|
||||||
const setScaleFactor = async (value: ScaleType) => {
|
const setScaleFactor = async (value: ScaleType) => {
|
||||||
scaleFactor.value = value;
|
scaleFactor.value = value;
|
||||||
|
|
||||||
const window = getCurrentWindow();
|
const window = getCurrentWindow();
|
||||||
const w = Math.floor(scaleValue(value) * 900);
|
const w = Math.floor(_scaleValue(value) * 900);
|
||||||
const h = Math.floor(scaleValue(value) * 480);
|
const h = Math.floor(_scaleValue(value) * 600);
|
||||||
|
|
||||||
let size = await window.innerSize();
|
let size = await window.innerSize();
|
||||||
|
|
||||||
@ -406,6 +425,15 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
if (input.scaleFactor) {
|
if (input.scaleFactor) {
|
||||||
await setScaleFactor(input.scaleFactor);
|
await setScaleFactor(input.scaleFactor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (input.theme) {
|
||||||
|
theme.value = input.theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.onboarded) {
|
||||||
|
onboarded.value = input.onboarded;
|
||||||
|
}
|
||||||
|
await setTheme(theme.value);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Error reading client options: ${e}`);
|
console.error(`Error reading client options: ${e}`);
|
||||||
}
|
}
|
||||||
@ -436,6 +464,8 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
w: Math.floor(size.width),
|
w: Math.floor(size.width),
|
||||||
h: Math.floor(size.height),
|
h: Math.floor(size.height),
|
||||||
},
|
},
|
||||||
|
theme: theme.value,
|
||||||
|
onboarded: onboarded.value,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -468,6 +498,26 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
await invoke('set_global_config', { field: 'verbose', value });
|
await invoke('set_global_config', { field: 'verbose', value });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setTheme = async (value: 'light' | 'dark' | 'system') => {
|
||||||
|
if (value === 'dark') {
|
||||||
|
document.documentElement.classList.add('use-dark-mode');
|
||||||
|
} else if (value === 'light') {
|
||||||
|
document.documentElement.classList.remove('use-dark-mode');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.toggle(
|
||||||
|
'use-dark-mode',
|
||||||
|
shouldPreferDark()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
theme.value = value;
|
||||||
|
await save();
|
||||||
|
};
|
||||||
|
|
||||||
|
const setOnboarded = async (game: Game) => {
|
||||||
|
onboarded.value = [...onboarded.value, game];
|
||||||
|
await save();
|
||||||
|
};
|
||||||
|
|
||||||
getCurrentWindow().onResized(async ({ payload }) => {
|
getCurrentWindow().onResized(async ({ payload }) => {
|
||||||
// For whatever reason this is 0 when minimized
|
// For whatever reason this is 0 when minimized
|
||||||
if (payload.width > 0) {
|
if (payload.width > 0) {
|
||||||
@ -480,13 +530,19 @@ export const useClientStore = defineStore('client', () => {
|
|||||||
offlineMode,
|
offlineMode,
|
||||||
enableAutoupdates,
|
enableAutoupdates,
|
||||||
verbose,
|
verbose,
|
||||||
|
theme,
|
||||||
|
onboarded,
|
||||||
timeout,
|
timeout,
|
||||||
scaleModel,
|
scaleModel,
|
||||||
|
_scaleValue,
|
||||||
|
scaleValue,
|
||||||
load,
|
load,
|
||||||
save,
|
save,
|
||||||
queueSave,
|
queueSave,
|
||||||
setOfflineMode,
|
setOfflineMode,
|
||||||
setAutoupdates,
|
setAutoupdates,
|
||||||
setVerbose,
|
setVerbose,
|
||||||
|
setTheme,
|
||||||
|
setOnboarded,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -19,7 +19,7 @@ export interface Package {
|
|||||||
icon: string;
|
icon: string;
|
||||||
} | null;
|
} | null;
|
||||||
js: {
|
js: {
|
||||||
busy: boolean;
|
downloading: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ export interface ProfileData {
|
|||||||
export interface SegatoolsConfig {
|
export interface SegatoolsConfig {
|
||||||
target: string;
|
target: string;
|
||||||
hook: string | null;
|
hook: string | null;
|
||||||
io: string | null;
|
io2: 'segatools_built_in' | 'hardware' | { custom: string };
|
||||||
amfs: string;
|
amfs: string;
|
||||||
option: string;
|
option: string;
|
||||||
appdata: string;
|
appdata: string;
|
||||||
|
13
src/util.ts
@ -59,3 +59,16 @@ export const hasFeature = (pkg: Package | undefined, feature: Feature) => {
|
|||||||
export const messageSplit = (message: any) => {
|
export const messageSplit = (message: any) => {
|
||||||
return message.message?.split('\n');
|
return message.message?.split('\n');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { defineConfig } from 'vite';
|
|
||||||
import vue from '@vitejs/plugin-vue';
|
import vue from '@vitejs/plugin-vue';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
|
||||||
// @ts-expect-error process is a nodejs global
|
// @ts-expect-error process is a nodejs global
|
||||||
const host = process.env.TAURI_DEV_HOST;
|
const host = process.env.TAURI_DEV_HOST;
|
||||||
@ -30,4 +30,7 @@ export default defineConfig(async () => ({
|
|||||||
ignored: ['**/rust/**'],
|
ignored: ['**/rust/**'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
chunkSizeWarningLimit: 1024,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|