Compare commits
14 Commits
Author | SHA1 | Date | |
---|---|---|---|
edef5cc6dc | |||
2dad0de4f1 | |||
14a65eb5bb | |||
0add9200a6 | |||
ee49da3665 | |||
f478ad9216 | |||
c59dbcc35c | |||
91d38b58c4 | |||
240f60b283 | |||
6a32ad65a5 | |||
6cc7a537b6 | |||
bf4c06ee2d | |||
f26d83f291 | |||
8b2c1a04ee |
34
CHANGELOG.md
34
CHANGELOG.md
@ -1,3 +1,37 @@
|
||||
## 0.18.3
|
||||
|
||||
- Updated Rainycolor's domain・真
|
||||
|
||||
## 0.18.2
|
||||
|
||||
- Updated Rainycolor's domain
|
||||
|
||||
## 0.18.1
|
||||
|
||||
- Keys can now be unbinded with Esc
|
||||
- Fixed CHUNITHM IR behavior on actual keyboards
|
||||
|
||||
## 0.18.0
|
||||
|
||||
- Added new grouping options to the package list
|
||||
|
||||
## 0.17.0
|
||||
|
||||
- Added a package creation prompt
|
||||
- Added a default package icon
|
||||
|
||||
## 0.16.0
|
||||
|
||||
- Fixed the clear cache button not working
|
||||
- Fixed Linux builds
|
||||
- Moved the store tab to the left
|
||||
- "Reapply mods and start" renamed from "Refresh and start" to better convey the meaning
|
||||
- "Reapply mods and start" is no longer necessary when enabling packages from the `local` namespace
|
||||
- Various internationalization additions
|
||||
- STARTLINER now remembers the recently open tab and re-opens it on the next session
|
||||
- Added "Beta" to the title as STARTLINER is approaching feature-completeness
|
||||
- Added full Polish localization :smciota:
|
||||
|
||||
## 0.15.0
|
||||
|
||||
- Added internationalization
|
||||
|
@ -9,7 +9,7 @@ This is a program that seeks to streamline game data configuration, currently su
|
||||
|
||||
STARTLINER is four things:
|
||||
|
||||
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainy.patafour.zip),
|
||||
- a mod installer and updater, powered by [Rainycolor Watercolor](https://rainycolor.org),
|
||||
- a configuration GUI for segatools,
|
||||
- a glorified `start.bat` clicker, with automatic monitor setup and rollback,
|
||||
- [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).
|
||||
|
@ -1,3 +0,0 @@
|
||||
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>
|
@ -1,8 +0,0 @@
|
||||
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
|
@ -1,3 +0,0 @@
|
||||
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>).
|
@ -1,3 +0,0 @@
|
||||
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.
|
@ -1,7 +0,0 @@
|
||||
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
public/no-icon.png
Normal file
BIN
public/no-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
103
rust/src/cmd.rs
103
rust/src/cmd.rs
@ -1,11 +1,12 @@
|
||||
use ini::Ini;
|
||||
use log;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
use std::path::PathBuf;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::fs;
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use crate::model::config::GlobalConfigField;
|
||||
use crate::model::local::PackageManifest;
|
||||
use crate::model::misc::Game;
|
||||
use crate::model::patch::Patch;
|
||||
use crate::modules::package::prepare_dlls;
|
||||
@ -75,6 +76,7 @@ pub async fn startline(app: AppHandle, refresh: bool) -> Result<(), String> {
|
||||
&p.data.sgt.target.parent().unwrap().join("amdaemon.exe")
|
||||
).map_err(|e| e.to_string())?;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
let info = p.prepare_display()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let lineup_res = p.line_up(hash, refresh, patches_enabled).await
|
||||
@ -165,6 +167,65 @@ pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key:
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_package(
|
||||
name: String,
|
||||
description: String,
|
||||
website: String,
|
||||
r#type: String,
|
||||
games: Vec<Game>
|
||||
) -> Result<(), String> {
|
||||
log::debug!("invoke: create_package");
|
||||
|
||||
let dir = util::pkg_dir_of("local", &name);
|
||||
|
||||
if dir.exists() {
|
||||
return Err("Package already exists".to_owned());
|
||||
}
|
||||
|
||||
let mut installers = Vec::new();
|
||||
|
||||
if r#type == "segatools" {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
"identifier".to_owned(),
|
||||
serde_json::Value::String("segatools".to_owned())
|
||||
);
|
||||
installers.push(map);
|
||||
} else if r#type == "native" {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert(
|
||||
"identifier".to_owned(),
|
||||
serde_json::Value::String("native_mod".to_owned())
|
||||
);
|
||||
map.insert(
|
||||
"dll-game".to_owned(),
|
||||
serde_json::Value::String("some.dll".to_owned())
|
||||
);
|
||||
map.insert(
|
||||
"dll-amdaemon".to_owned(),
|
||||
serde_json::Value::String("another.dll".to_owned())
|
||||
);
|
||||
installers.push(map);
|
||||
}
|
||||
|
||||
let manifest = PackageManifest {
|
||||
name,
|
||||
version_number: "1.0.0".to_owned(),
|
||||
description,
|
||||
website_url: website,
|
||||
dependencies: BTreeSet::new(),
|
||||
installers,
|
||||
games: Some(games)
|
||||
};
|
||||
|
||||
std::fs::create_dir(&dir).map_err(|e| e.to_string())?;
|
||||
let json = serde_json::to_string_pretty(&manifest).map_err(|e| e.to_string())?;
|
||||
std::fs::write(dir.join("manifest.json"), json).map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: reload_all_packages");
|
||||
@ -190,12 +251,16 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
|
||||
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> {
|
||||
log::debug!("invoke: get_game_packages {game}");
|
||||
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Option<Game>) -> Result<Vec<PkgKey>, ()> {
|
||||
log::debug!("invoke: get_game_packages {game:?}");
|
||||
|
||||
let appd = state.lock().await;
|
||||
|
||||
Ok(appd.pkgs.get_game_list(game))
|
||||
if let Some(game) = game {
|
||||
Ok(appd.pkgs.get_game_list(game))
|
||||
} else {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -404,10 +469,14 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
|
||||
pub async fn create_shortcut(_app: AppHandle, profile_meta: ProfileMeta) -> Result<(), String> {
|
||||
log::debug!("invoke: create_shortcut({:?})", profile_meta);
|
||||
|
||||
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
|
||||
#[cfg(target_os = "windows")]
|
||||
return util::create_shortcut(_app, &profile_meta).map_err(|e| e.to_string());
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return Err("unsupported".to_owned());
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@ -437,12 +506,32 @@ pub async fn import_profile(path: PathBuf) -> Result<(), String> {
|
||||
Profile::import(path).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn clear_cache(state: State<'_, Mutex<AppData>>) -> Result<(), String> {
|
||||
log::debug!("invoke: clear_cache");
|
||||
|
||||
let appd = state.lock().await;
|
||||
if let Some(p) = &appd.profile {
|
||||
let dir = p.data_dir().join("mu3-mods-cache");
|
||||
let path = dir.join("data_cache.bin");
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
let path = dir.join("data_fumen_analysis_cache.bin");
|
||||
if path.exists() {
|
||||
std::fs::remove_file(path).map_err(|e| e.to_string())?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn list_platform_capabilities() -> Result<Vec<String>, ()> {
|
||||
log::debug!("invoke: list_platform_capabilities");
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
return Ok(vec!["display".to_owned()]);
|
||||
return Ok(vec!["display".to_owned(), "shortcut".to_owned(), "chunithm".to_owned()]);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
return Ok(vec!["wine".to_owned()]);
|
||||
|
@ -193,6 +193,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::install_package,
|
||||
cmd::delete_package,
|
||||
cmd::toggle_package,
|
||||
cmd::create_package,
|
||||
|
||||
cmd::list_profiles,
|
||||
cmd::init_profile,
|
||||
@ -207,6 +208,7 @@ pub async fn run(_args: Vec<String>) {
|
||||
cmd::create_shortcut,
|
||||
cmd::export_profile,
|
||||
cmd::import_profile,
|
||||
cmd::clear_cache,
|
||||
|
||||
cmd::get_global_config,
|
||||
cmd::set_global_config,
|
||||
@ -335,7 +337,7 @@ async fn update(app: tauri::AppHandle) -> tauri_plugin_updater::Result<()> {
|
||||
fn open_window(apph: AppHandle) -> anyhow::Result<()> {
|
||||
let config = apph.config().clone();
|
||||
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into()))
|
||||
.title(format!("STARTLINER {}", config.version.unwrap_or_default()))
|
||||
.title(format!("STARTLINER {} Beta", config.version.unwrap_or_default()))
|
||||
.inner_size(900f64, 600f64)
|
||||
.min_inner_size(900f64, 600f64)
|
||||
.build()?;
|
||||
|
@ -6,10 +6,11 @@ use super::misc::Game;
|
||||
|
||||
// manifest.json
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct PackageManifest {
|
||||
pub name: String,
|
||||
pub version_number: String,
|
||||
pub website_url: String,
|
||||
pub description: String,
|
||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||
|
||||
|
@ -108,7 +108,10 @@ impl Display {
|
||||
Game::Ongeki => 60,
|
||||
},
|
||||
borderless_fullscreen: true,
|
||||
#[cfg(target_os = "windows")]
|
||||
dont_switch_primary: false,
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
dont_switch_primary: true,
|
||||
monitor_index_override: None,
|
||||
}
|
||||
}
|
||||
@ -141,7 +144,7 @@ pub struct BepInEx {
|
||||
pub console: bool,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone)]
|
||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct Wine {
|
||||
pub runtime: PathBuf,
|
||||
|
8
rust/src/modules/display_linux.rs
Normal file
8
rust/src/modules/display_linux.rs
Normal file
@ -0,0 +1,8 @@
|
||||
use ini::Ini;
|
||||
use crate::model::{misc::Game, profile::Display};
|
||||
|
||||
impl Display {
|
||||
pub fn line_up(&self, _game: Game, _ini: &mut Ini) {
|
||||
// nop
|
||||
}
|
||||
}
|
@ -117,12 +117,16 @@ impl Keyboard {
|
||||
}
|
||||
}
|
||||
Keyboard::Chunithm(kb) => {
|
||||
let mut enabled_ir = false;
|
||||
if kb.enabled {
|
||||
for (i, cell) in kb.cell.iter().enumerate() {
|
||||
ini.with_section(Some("slider")).set(format!("cell{}", i + 1), cell.to_string());
|
||||
}
|
||||
for (i, ir) in kb.ir.iter().enumerate() {
|
||||
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), ir.to_string());
|
||||
ini.with_section(Some("ir")).set(format!("ir{}", i + 1), (*ir).to_string());
|
||||
if i > 0 && *ir != 0 {
|
||||
enabled_ir = true;
|
||||
}
|
||||
}
|
||||
ini.with_section(Some("io3"))
|
||||
.set("test", kb.test.to_string())
|
||||
@ -140,8 +144,13 @@ impl Keyboard {
|
||||
.set("service", "0")
|
||||
.set("coin", "0");
|
||||
}
|
||||
ini.with_section(Some("io3"))
|
||||
.set("ir", "0");
|
||||
if enabled_ir {
|
||||
ini.with_section(Some("io3"))
|
||||
.set("ir", "0");
|
||||
} else {
|
||||
ini.with_section(Some("io3"))
|
||||
.set("ir", kb.ir[0].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,4 +7,7 @@ pub mod keyboard;
|
||||
pub mod mempatcher;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod display_windows;
|
||||
pub mod display_windows;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub mod display_linux;
|
@ -7,12 +7,20 @@ use crate::pkg_store::PackageStore;
|
||||
use crate::util;
|
||||
use crate::profiles::types::ProfilePaths;
|
||||
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, redo_bepinex: bool) -> Result<()> {
|
||||
pub async fn prepare_packages<'a>(p: &'a impl ProfilePaths, pkgs: &BTreeSet<PkgKey>, mut redo_bepinex: bool) -> Result<()> {
|
||||
log::debug!("begin prepare packages");
|
||||
|
||||
let pfx_dir = p.data_dir();
|
||||
let opt_dir = pfx_dir.join("option");
|
||||
|
||||
for m in pkgs {
|
||||
let (namespace, _) = m.split()?;
|
||||
if namespace == "local" {
|
||||
log::info!("package with the 'local' namespace enabled -- force refreshing");
|
||||
redo_bepinex = true;
|
||||
}
|
||||
}
|
||||
|
||||
if redo_bepinex {
|
||||
if pfx_dir.join("BepInEx").exists() {
|
||||
util::remove_dir_all(pfx_dir.join("BepInEx")).await?;
|
||||
|
@ -109,7 +109,7 @@ impl Package {
|
||||
loc: None,
|
||||
rmt: Some(Remote {
|
||||
package_url: p.package_url,
|
||||
download_url: v.download_url,
|
||||
download_url: v.download_url.replace("https://rainy.patafour.zip/", "https://www.rainycolor.org/"),
|
||||
icon: v.icon,
|
||||
deprecated: p.is_deprecated,
|
||||
nsfw: p.has_nsfw_content,
|
||||
|
@ -132,7 +132,7 @@ impl PackageStore {
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
let response = reqwest::get(format!("https://rainy.patafour.zip/c/{game}/api/v1/package/")).await?;
|
||||
let response = reqwest::get(format!("https://www.rainycolor.org/c/{game}/api/v1/package/")).await?;
|
||||
|
||||
let reader = response
|
||||
.bytes_stream()
|
||||
|
@ -1,6 +1,6 @@
|
||||
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
|
||||
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
|
||||
use crate::{model::{misc::Game, patch::{PatchList, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::{display_windows::DisplayInfo, package::prepare_packages}, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use crate::{model::{misc::Game, patch::{PatchList, PatchSelection}, profile::{Aime, ChunithmKeyboard, IOSelection, Keyboard, Mu3Ini, OngekiKeyboard, ProfileModule}}, modules::package::prepare_packages, pkg::PkgKey, pkg_store::PackageStore, util};
|
||||
use tauri::Emitter;
|
||||
use std::process::Stdio;
|
||||
use crate::model::profile::BepInEx;
|
||||
@ -10,9 +10,23 @@ use std::fs::File;
|
||||
use tokio::process::Command;
|
||||
use tokio::task::JoinSet;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use crate::modules::display_windows::DisplayInfo;
|
||||
|
||||
pub mod template;
|
||||
pub mod types;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
pub trait RawArg {
|
||||
fn raw_arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Command;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
impl RawArg for Command {
|
||||
fn raw_arg<S: AsRef<std::ffi::OsStr>>(&mut self, arg: S) -> &mut Command {
|
||||
return self.arg::<S>(arg);
|
||||
}
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
pub fn new(mut meta: ProfileMeta) -> Result<Self> {
|
||||
meta.name = fixed_name(&meta, true);
|
||||
@ -176,6 +190,7 @@ impl Profile {
|
||||
self.data.patches = source.patches;
|
||||
}
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
|
||||
let info = match &self.data.display {
|
||||
None => None,
|
||||
@ -252,8 +267,8 @@ impl Profile {
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
game_builder = Command::new(&self.wine.runtime);
|
||||
amd_builder = Command::new(&self.wine.runtime);
|
||||
game_builder = Command::new(&self.data.wine.runtime);
|
||||
amd_builder = Command::new(&self.data.wine.runtime);
|
||||
|
||||
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
|
||||
amd_builder.arg("cmd.exe");
|
||||
@ -349,8 +364,8 @@ impl Profile {
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
amd_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
game_builder.env("WINEPREFIX", &self.wine.prefix);
|
||||
amd_builder.env("WINEPREFIX", &self.data.wine.prefix);
|
||||
game_builder.env("WINEPREFIX", &self.data.wine.prefix);
|
||||
}
|
||||
|
||||
let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?;
|
||||
|
@ -152,6 +152,7 @@ impl PathStr for PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn bool_to_01(val: bool) -> &'static str {
|
||||
return if val { "1" } else { "0" }
|
||||
}
|
||||
|
@ -64,19 +64,4 @@ controllerLedOutputOpeNITHM=0
|
||||
; [60]-[62]: right side partition LEDs
|
||||
;
|
||||
; Board 2 is the slider and has 31 LEDs:
|
||||
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
|
||||
|
||||
|
||||
; -----------------------------------------------------------------------------
|
||||
; Custom IO settings
|
||||
; -----------------------------------------------------------------------------
|
||||
|
||||
[chuniio]
|
||||
; Uncomment this if you have custom chuniio implementation comprised of a single 32bit DLL.
|
||||
; (will use chu2to3 engine internally)
|
||||
;path=
|
||||
|
||||
; Uncomment both of these if you have custom chuniio implementation comprised of two DLLs.
|
||||
; x86 chuniio to path32, x64 to path64. Both are necessary.
|
||||
;path32=
|
||||
;path64=
|
||||
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers
|
@ -63,9 +63,9 @@
|
||||
},
|
||||
{
|
||||
// Ongeki
|
||||
filename: "amdaemon.exe",
|
||||
filename: "amdaemon.exe",
|
||||
version: "46d47eab",
|
||||
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
|
||||
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
|
||||
patches: [
|
||||
{
|
||||
id: 'standard-localhost',
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.15.0",
|
||||
"version": "0.18.3",
|
||||
"identifier": "zip.patafour.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
|
@ -20,15 +20,16 @@ import OptionList from './OptionList.vue';
|
||||
import PatchList from './PatchList.vue';
|
||||
import ProfileList from './ProfileList.vue';
|
||||
import StartButton from './StartButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import {
|
||||
useClientStore,
|
||||
useGeneralStore,
|
||||
usePkgStore,
|
||||
usePrfStore,
|
||||
} from '../stores';
|
||||
import { Dirs } from '../types';
|
||||
import { messageSplit, shouldPreferDark } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
|
||||
|
||||
@ -37,10 +38,10 @@ const prf = usePrfStore();
|
||||
const general = useGeneralStore();
|
||||
const client = useClientStore();
|
||||
|
||||
client.load();
|
||||
|
||||
pkg.setupListeners();
|
||||
|
||||
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
|
||||
ref('users');
|
||||
const pkgSearchTerm = ref('');
|
||||
|
||||
const isProfileDisabled = computed(() => prf.current === null);
|
||||
@ -56,17 +57,11 @@ listen<undefined>('update-end', (_) => {
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
invoke('list_directories').then((d) => {
|
||||
general.dirs = d as Dirs;
|
||||
client.load();
|
||||
});
|
||||
|
||||
const fetch_promise = pkg.fetch(true);
|
||||
|
||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||
|
||||
if (prf.current !== null) {
|
||||
currentTab.value = 'loc';
|
||||
await pkg.reloadAll();
|
||||
}
|
||||
|
||||
@ -211,10 +206,11 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
|
||||
<Tabs
|
||||
lazy
|
||||
:value="currentTab"
|
||||
:value="client.currentTab"
|
||||
v-on:update:value="
|
||||
(value) => {
|
||||
currentTab = value as any;
|
||||
client.currentTab = value as string;
|
||||
client.save();
|
||||
}
|
||||
"
|
||||
class="h-screen"
|
||||
@ -222,6 +218,13 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
<div class="fixed w-full flex z-100">
|
||||
<TabList class="grow" :show-navigators="false">
|
||||
<Tab value="users"><div class="pi pi-home"></div></Tab>
|
||||
<Tab
|
||||
:disabled="
|
||||
isProfileDisabled || pkg.networkStatus !== 'online'
|
||||
"
|
||||
value="rmt"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
<Tab :disabled="isProfileDisabled" value="loc"
|
||||
><div class="pi pi-box"></div
|
||||
></Tab>
|
||||
@ -230,12 +233,7 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
value="patches"
|
||||
><div class="pi pi-ticket"></div
|
||||
></Tab>
|
||||
<Tab
|
||||
v-if="pkg.networkStatus === 'online'"
|
||||
:disabled="isProfileDisabled"
|
||||
value="rmt"
|
||||
><div class="pi pi-download"></div
|
||||
></Tab>
|
||||
|
||||
<Tab :disabled="isProfileDisabled" value="cfg"
|
||||
><div class="pi pi-cog"></div
|
||||
></Tab>
|
||||
@ -248,17 +246,21 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
<div class="flex gap-4">
|
||||
<div
|
||||
class="flex"
|
||||
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)"
|
||||
v-if="
|
||||
['loc', 'rmt', 'cfg'].includes(
|
||||
client.currentTab
|
||||
)
|
||||
"
|
||||
>
|
||||
<InputIcon class="self-center mr-2">
|
||||
<i class="pi pi-search" />
|
||||
</InputIcon>
|
||||
<InputText
|
||||
v-if="currentTab === 'cfg'"
|
||||
v-if="client.currentTab === 'cfg'"
|
||||
style="min-width: 0; width: 25dvw"
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
:placeholder="t('search')"
|
||||
v-model="general.cfgSearchTerm"
|
||||
/>
|
||||
<InputText
|
||||
@ -266,7 +268,7 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
style="min-width: 0; width: 25dvw"
|
||||
class="self-center"
|
||||
size="small"
|
||||
placeholder="Search"
|
||||
:placeholder="t('search')"
|
||||
v-model="pkgSearchTerm"
|
||||
/>
|
||||
</div>
|
||||
@ -304,19 +306,19 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
</TabList>
|
||||
</div>
|
||||
<TabPanels class="w-full grow mt-[3rem]">
|
||||
<TabPanel value="loc">
|
||||
<TabPanel value="loc" v-if="!isProfileDisabled">
|
||||
<ModList :search="pkgSearchTerm" />
|
||||
</TabPanel>
|
||||
<TabPanel value="rmt">
|
||||
<TabPanel value="rmt" v-if="!isProfileDisabled">
|
||||
<ModStore :search="pkgSearchTerm" />
|
||||
</TabPanel>
|
||||
<TabPanel value="cfg">
|
||||
<TabPanel value="cfg" v-if="!isProfileDisabled">
|
||||
<OptionList />
|
||||
</TabPanel>
|
||||
<TabPanel value="users">
|
||||
<ProfileList />
|
||||
</TabPanel>
|
||||
<TabPanel value="patches">
|
||||
<TabPanel value="patches" v-if="!isProfileDisabled">
|
||||
<PatchList
|
||||
v-if="
|
||||
pkg.hasLocal('mempatcher-mempatcher') &&
|
||||
@ -347,7 +349,12 @@ listen<DownloadingStatus>('download-progress', (event) => {
|
||||
<InfoPage />
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
<div v-if="currentTab === 'users' || currentTab === 'info'">
|
||||
<div
|
||||
v-if="
|
||||
client.currentTab === 'users' ||
|
||||
client.currentTab === 'info'
|
||||
"
|
||||
>
|
||||
<img
|
||||
v-if="prf.current?.meta.game === 'ongeki'"
|
||||
src="/sticker-ongeki.svg"
|
||||
|
@ -4,6 +4,9 @@ import InputText from 'primevue/inputtext';
|
||||
import { fromKeycode, toKeycode } from '../keyboard';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { OngekiButtons } from '../types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
@ -61,6 +64,10 @@ const handleKey = (
|
||||
}
|
||||
}
|
||||
|
||||
if (event.code === 'Escape') {
|
||||
keycode = 0;
|
||||
}
|
||||
|
||||
if (index !== undefined) {
|
||||
data[button][index] = keycode;
|
||||
} else {
|
||||
@ -160,13 +167,24 @@ const fontSize = computed(() => {
|
||||
<InputText
|
||||
:style="{
|
||||
width: small ? '2.8rem' : '5rem',
|
||||
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||
height:
|
||||
small && tall
|
||||
? '5rem'
|
||||
: small
|
||||
? '2.8rem'
|
||||
: tall
|
||||
? '10rem'
|
||||
: '5rem',
|
||||
fontSize,
|
||||
backgroundColor: color,
|
||||
}"
|
||||
unstyled
|
||||
class="text-center buttoninputtext"
|
||||
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||
v-tooltip="
|
||||
tooltip
|
||||
? `${tooltip}: ${modelValue} ${tooltip.startsWith('ir') ? `\n${t('cfg.keyboard.irTooltip')}` : ''}`
|
||||
: undefined
|
||||
"
|
||||
@contextmenu.prevent="() => {}"
|
||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||
@mousedown="
|
||||
|
@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import ModListEntry from './ModListEntry.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { Package } from '../types';
|
||||
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Feature, Game, Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@ -17,48 +23,290 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const client = useClientStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(false);
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
const loadPackages = () => {
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game ?? null,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
});
|
||||
};
|
||||
|
||||
loadPackages();
|
||||
|
||||
const allCategories = computed(() => {
|
||||
const res = new Set<string>();
|
||||
for (const pkg of pkgs.allLocal) {
|
||||
for (const cat of pkg.rmt?.categories ?? []) {
|
||||
res.add(cat);
|
||||
}
|
||||
}
|
||||
return [...res.values()].sort((a, b) => a.localeCompare(b));
|
||||
});
|
||||
|
||||
const group = computed(() => {
|
||||
const res = Object.assign(
|
||||
{},
|
||||
Object.groupBy(
|
||||
pkgs.allLocal
|
||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
p.name
|
||||
.toLowerCase()
|
||||
.includes(props.search.toLowerCase()) ||
|
||||
p.namespace
|
||||
.toLowerCase()
|
||||
.includes(props.search.toLowerCase())
|
||||
)
|
||||
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||
({ namespace }) => namespace
|
||||
)
|
||||
);
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
const local = computed(() => {
|
||||
return pkgs.allLocal
|
||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||
.filter((p) => p.namespace === 'local');
|
||||
});
|
||||
|
||||
const groupedList = computed(() => {
|
||||
const searchedPkgs = pkgs.allLocal
|
||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||
.filter((p) => p.namespace !== 'local')
|
||||
.filter(
|
||||
(p) =>
|
||||
props.search === undefined ||
|
||||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
|
||||
p.namespace.toLowerCase().includes(props.search.toLowerCase())
|
||||
);
|
||||
|
||||
let grouped;
|
||||
if (client.pkgListMode === 'namespace') {
|
||||
grouped = Object.groupBy(searchedPkgs, ({ namespace }) => namespace);
|
||||
} else if (client.pkgListMode === 'type') {
|
||||
grouped = {
|
||||
standard: [] as Package[],
|
||||
native: [] as Package[],
|
||||
segatools: [] as Package[],
|
||||
unsupported: [] as Package[] | undefined,
|
||||
};
|
||||
grouped.unsupported = [];
|
||||
for (const pkg of searchedPkgs) {
|
||||
const loc = pkg.loc;
|
||||
if (!loc || !loc.status || typeof loc.status === 'string') {
|
||||
grouped.unsupported.push(pkg);
|
||||
} else {
|
||||
if (
|
||||
loc.status.OK[0] &
|
||||
(Feature.GameDLL | Feature.Mempatcher | Feature.AmdDLL)
|
||||
) {
|
||||
grouped.native.push(pkg);
|
||||
} else if (loc.status.OK[0] & Feature.Mod) {
|
||||
grouped.standard.push(pkg);
|
||||
}
|
||||
if (
|
||||
loc.status.OK[0] &
|
||||
(Feature.AMNet |
|
||||
Feature.Aime |
|
||||
Feature.ChuniIO |
|
||||
Feature.ChusanHook |
|
||||
Feature.Mu3IO |
|
||||
Feature.Mu3Hook)
|
||||
) {
|
||||
grouped.segatools.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (grouped.unsupported.length === 0) {
|
||||
delete grouped.unsupported;
|
||||
}
|
||||
} else {
|
||||
grouped = {} as { [key: string]: Package[] };
|
||||
for (const pkg of searchedPkgs) {
|
||||
for (const cat of pkg.rmt?.categories ?? []) {
|
||||
if (client.hiddenCategories.includes(cat)) {
|
||||
continue;
|
||||
}
|
||||
if (!(cat in grouped)) {
|
||||
grouped[cat] = [] as Package[];
|
||||
}
|
||||
grouped[cat].push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res: [string, Package[]][] = [];
|
||||
for (const [k, v] of Object.entries(grouped)) {
|
||||
if (v !== undefined) {
|
||||
res.push([k, v]);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
client.pkgListMode === 'namespace' ||
|
||||
client.pkgListMode === 'category'
|
||||
) {
|
||||
res.sort((a, b) => `${a[0]}`.localeCompare(`${b[0]}`));
|
||||
} else if (client.pkgListMode === 'type') {
|
||||
for (const entry of res) {
|
||||
entry[0] = t(`pkglist.${entry[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
const missing = computed(() => {
|
||||
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||
});
|
||||
|
||||
const dialogVisible = ref(false);
|
||||
|
||||
const defaultModel = {
|
||||
name: '',
|
||||
description: '',
|
||||
website: '',
|
||||
type: 'rainy',
|
||||
games: [] as string[],
|
||||
};
|
||||
|
||||
const creatorModel = ref({ ...defaultModel });
|
||||
|
||||
const gameModel = (game: Game) =>
|
||||
computed({
|
||||
get() {
|
||||
return (creatorModel.value.games as string[]).includes(game);
|
||||
},
|
||||
set(v: boolean) {
|
||||
creatorModel.value.games = creatorModel.value.games.filter(
|
||||
(g) => g !== game
|
||||
);
|
||||
if (v) {
|
||||
creatorModel.value.games.push(game);
|
||||
}
|
||||
},
|
||||
});
|
||||
const gameModelOngeki = gameModel('ongeki');
|
||||
const gameModelChunithm = gameModel('chunithm');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Fieldset :legend="t('store.missing')" v-if="(missing?.length ?? 0) > 0">
|
||||
<Dialog
|
||||
modal
|
||||
:visible="dialogVisible"
|
||||
:closable="false"
|
||||
:header="t('creator.header')"
|
||||
:style="{ width: '500px', scale: client.scaleValue }"
|
||||
class="creation-dialog"
|
||||
>
|
||||
<div style="position: absolute; left: 250px; top: 25px">
|
||||
<a
|
||||
href="https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Package-format"
|
||||
target="_blank"
|
||||
style="text-decoration: underline"
|
||||
class="self-center"
|
||||
>{{ t('creator.packageFormat') }}</a
|
||||
>
|
||||
</div>
|
||||
<h2>{{ t('creator.basic') }}</h2>
|
||||
<div class="flex flex-col gap-3">
|
||||
<InputText
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="t('creator.name')"
|
||||
v-model="creatorModel.name"
|
||||
/>
|
||||
<InputText
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="t('creator.description')"
|
||||
v-model="creatorModel.description"
|
||||
/>
|
||||
<InputText
|
||||
size="small"
|
||||
style="width: 100%"
|
||||
:placeholder="t('creator.website')"
|
||||
v-model="creatorModel.website"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2>{{ t('creator.type') }}</h2>
|
||||
<div class="flex flex-col items-center">
|
||||
<SelectButton
|
||||
:options="[
|
||||
{ title: t('creator.rainy'), value: 'rainy' },
|
||||
{ title: t('creator.native'), value: 'native' },
|
||||
{ title: t('creator.segatools'), value: 'segatools' },
|
||||
]"
|
||||
v-model="creatorModel.type"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2>{{ t('creator.games') }}</h2>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row">
|
||||
<div class="grow">{{ t('game.ongeki') }}</div>
|
||||
<ToggleSwitch v-model="gameModelOngeki" />
|
||||
</div>
|
||||
<div class="flex flex-row">
|
||||
<div class="grow">{{ t('game.chunithm') }}</div>
|
||||
<ToggleSwitch v-model="gameModelChunithm" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row mt-5">
|
||||
<Button
|
||||
class="ml-auto mr-1"
|
||||
style="width: 80px"
|
||||
:label="t('ok')"
|
||||
:disabled="creatorModel.games.length === 0"
|
||||
@click="
|
||||
async () => {
|
||||
await invoke('create_package', creatorModel);
|
||||
await pkgs.reloadAll();
|
||||
loadPackages();
|
||||
dialogVisible = false;
|
||||
creatorModel = { ...defaultModel };
|
||||
}
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
class="mr-auto ml-1"
|
||||
style="width: 80px"
|
||||
:label="t('cancel')"
|
||||
@click="
|
||||
() => (
|
||||
(dialogVisible = false),
|
||||
(creatorModel = { ...defaultModel })
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</Dialog>
|
||||
<div class="flex flex-row">
|
||||
<SelectButton
|
||||
:options="[
|
||||
{ title: t('pkglist.namespace'), value: 'namespace' },
|
||||
{ title: t('pkglist.type'), value: 'type' },
|
||||
{ title: t('pkglist.category'), value: 'category' },
|
||||
]"
|
||||
v-model="client.pkgListMode"
|
||||
v-on:update:model-value="
|
||||
client.save();
|
||||
emit('reload-icons');
|
||||
"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
/>
|
||||
|
||||
<div
|
||||
class="grow text-right mr-2 self-center"
|
||||
v-if="client.pkgListMode === 'category'"
|
||||
>
|
||||
{{ t('pkglist.exclusions') }}
|
||||
</div>
|
||||
<MultiSelect
|
||||
v-if="client.pkgListMode === 'category'"
|
||||
style="width: 30%"
|
||||
:showToggleAll="false"
|
||||
v-model="client.hiddenCategories"
|
||||
v-on:value-change="
|
||||
client.save();
|
||||
emit('reload-icons');
|
||||
"
|
||||
:options="allCategories"
|
||||
class="w-full grow"
|
||||
/>
|
||||
</div>
|
||||
<Fieldset :legend="t('pkglist.missing')" v-if="(missing?.length ?? 0) > 0">
|
||||
<div class="flex items-center" v-for="p in missing">
|
||||
<ModTitlecard
|
||||
show-namespace
|
||||
@ -81,8 +329,28 @@ const missing = computed(() => {
|
||||
/>
|
||||
</div>
|
||||
</Fieldset>
|
||||
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
|
||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
||||
<Fieldset :legend="t('pkglist.local')">
|
||||
<ModListEntry v-for="p in local" :pkg="p" />
|
||||
<Button
|
||||
rounded
|
||||
icon="pi pi-plus"
|
||||
severity="success"
|
||||
aria-label="install"
|
||||
size="small"
|
||||
class="self-center"
|
||||
style="width: 2rem; height: 2rem"
|
||||
v-on:click="() => (dialogVisible = true)"
|
||||
/>
|
||||
</Fieldset>
|
||||
<Fieldset v-for="[namespace, pkgs] in groupedList" :legend="namespace">
|
||||
<ModListEntry v-for="p in pkgs" :pkg="p" />
|
||||
</Fieldset>
|
||||
<div v-if="empty === true" class="text-3xl fadein">∅</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.creation-dialog h2 {
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0.4em;
|
||||
font-size: 110%;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,7 +7,7 @@ import LinkButton from './LinkButton.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import UpdateButton from './UpdateButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Feature, Package } from '../types';
|
||||
import { hasFeature } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@ -16,6 +16,7 @@ const { t } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
const pkgs = usePkgStore();
|
||||
const client = useClientStore();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
@ -39,7 +40,13 @@ if (unsupported.value === true && model.value === true) {
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||
<ModTitlecard
|
||||
show-version
|
||||
show-icon
|
||||
show-description
|
||||
:show-namespace="client.pkgListMode !== 'namespace'"
|
||||
:pkg="pkg"
|
||||
/>
|
||||
<UpdateButton :pkg="pkg" />
|
||||
<span v-tooltip="unsupported && t('store.incompatible')">
|
||||
<ToggleSwitch
|
||||
|
@ -23,7 +23,7 @@ const props = defineProps({
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game,
|
||||
game: prf.current?.meta.game ?? null,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
});
|
||||
@ -46,10 +46,10 @@ const list = () => {
|
||||
};
|
||||
|
||||
const shouldShowRecommended = computed(() => {
|
||||
if (prf.current!.meta.game === 'ongeki') {
|
||||
if (prf.current?.meta.game === 'ongeki') {
|
||||
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
|
||||
}
|
||||
if (prf.current!.meta.game === 'chunithm') {
|
||||
if (prf.current?.meta.game === 'chunithm') {
|
||||
return (
|
||||
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
|
||||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
|
||||
@ -58,21 +58,21 @@ const shouldShowRecommended = computed(() => {
|
||||
return false;
|
||||
});
|
||||
|
||||
const getRecommendedTooltip = () => {
|
||||
if (prf.current!.meta.game === 'ongeki') {
|
||||
const recommendedTooltip = computed(() => {
|
||||
if (prf.current?.meta.game === 'ongeki') {
|
||||
return 'segatools-mu3hook';
|
||||
}
|
||||
if (prf.current!.meta.game === 'chunithm') {
|
||||
if (prf.current?.meta.game === 'chunithm') {
|
||||
return 'segatools-chusanhook + mempatcher';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
});
|
||||
|
||||
const installRecommended = () => {
|
||||
if (prf.current!.meta.game === 'ongeki') {
|
||||
if (prf.current?.meta.game === 'ongeki') {
|
||||
pkgs.installFromKey('segatools-mu3hook');
|
||||
}
|
||||
if (prf.current!.meta.game === 'chunithm') {
|
||||
if (prf.current?.meta.game === 'chunithm') {
|
||||
pkgs.installFromKey('segatools-chusanhook');
|
||||
pkgs.installFromKey('mempatcher-mempatcher');
|
||||
}
|
||||
@ -101,7 +101,7 @@ const installRecommended = () => {
|
||||
<MultiSelect
|
||||
size="small"
|
||||
:showToggleAll="false"
|
||||
placeholder="Include categories"
|
||||
:placeholder="t('store.includeCategories')"
|
||||
v-model="pkgs.includeCategories"
|
||||
:options="[...pkgs.availableCategories]"
|
||||
class="w-full"
|
||||
@ -109,7 +109,7 @@ const installRecommended = () => {
|
||||
<MultiSelect
|
||||
size="small"
|
||||
:showToggleAll="false"
|
||||
placeholder="Exclude categories"
|
||||
:placeholder="t('store.excludeCategories')"
|
||||
v-model="pkgs.excludeCategories"
|
||||
:options="[...pkgs.availableCategories]"
|
||||
class="w-full"
|
||||
@ -120,7 +120,7 @@ const installRecommended = () => {
|
||||
<Button
|
||||
v-if="shouldShowRecommended"
|
||||
:label="t('store.installRecommended')"
|
||||
v-tooltip="getRecommendedTooltip"
|
||||
v-tooltip="recommendedTooltip"
|
||||
icon="pi pi-plus"
|
||||
class="mb-3"
|
||||
@click="installRecommended"
|
||||
|
@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import Chip from 'primevue/chip';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { invoke } from '../invoke';
|
||||
import { Feature, Package } from '../types';
|
||||
import { hasFeature, needsUpdate } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
@ -14,23 +19,34 @@ const props = defineProps({
|
||||
showIcon: Boolean,
|
||||
});
|
||||
|
||||
const iconSrc = computed(() => {
|
||||
const icon = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
||||
const icon = ref('/no-icon.png');
|
||||
|
||||
if (icon === undefined) {
|
||||
return '';
|
||||
} else if (icon.startsWith('https://')) {
|
||||
return icon;
|
||||
const reloadIcons = async () => {
|
||||
const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
||||
|
||||
if (src === undefined) {
|
||||
icon.value = '/no-icon.png';
|
||||
} else if (src.startsWith('https://')) {
|
||||
icon.value = src;
|
||||
} else {
|
||||
return convertFileSrc(icon);
|
||||
const convt = convertFileSrc(src);
|
||||
if (await invoke('file_exists', { path: src })) {
|
||||
icon.value = convt;
|
||||
} else {
|
||||
icon.value = '/no-icon.png';
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
reloadIcons();
|
||||
|
||||
listen('reload-icons', reloadIcons);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-if="showIcon"
|
||||
:src="iconSrc"
|
||||
:src="icon"
|
||||
class="self-center rounded-sm"
|
||||
width="32px"
|
||||
height="32px"
|
||||
@ -89,7 +105,12 @@ const iconSrc = computed(() => {
|
||||
v-if="showNamespace && pkg?.namespace"
|
||||
class="text-sm opacity-75"
|
||||
>
|
||||
by {{ pkg.namespace }}
|
||||
{{
|
||||
t('by', { namespace: pkg.namespace }).replaceAll(
|
||||
' ',
|
||||
' '
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="m-2">
|
||||
<span
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ComputedRef, computed, onMounted, ref } from 'vue';
|
||||
import { ComputedRef, computed, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import Carousel from 'primevue/carousel';
|
||||
import Dialog from 'primevue/dialog';
|
||||
@ -7,6 +7,9 @@ import { fromKeycode } from '../keyboard';
|
||||
import { useClientStore, usePrfStore } from '../stores';
|
||||
import { prettyPrint } from '../util';
|
||||
import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
const client = useClientStore();
|
||||
@ -24,38 +27,73 @@ interface Datum {
|
||||
|
||||
const game = computed(() => prf.current?.meta.game);
|
||||
|
||||
const processText = (s: string) => {
|
||||
const processText = computed(() => (s: string) => {
|
||||
// Why do I have to do this
|
||||
s = s
|
||||
.split('\n')
|
||||
.map((l) => l.trim())
|
||||
.join('\n');
|
||||
if (prf.current!.data.keyboard?.data.enabled) {
|
||||
const testKey = prf.current!.data.keyboard?.data.test;
|
||||
const readable = fromKeycode(testKey);
|
||||
if (readable !== null) {
|
||||
return s.replace(
|
||||
'%TESTMENU%',
|
||||
`${readable} or a button on the back of the controller`
|
||||
`${readable} ${t('onboarding.or')} ${t('onboarding.backButton')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
return s.replace('%TESTMENU%', 'a button on the back of the controller');
|
||||
};
|
||||
return s.replace('%TESTMENU%', t('onboarding.backButton'));
|
||||
});
|
||||
|
||||
const loadPage = async (title: string) => {
|
||||
const loadPage = computed(() => (title: string, messages?: object) => {
|
||||
return {
|
||||
text: await (await fetch(`/help-${title}.md`)).text(),
|
||||
text: t(`onboarding.${title}`, {
|
||||
endlink: '</a>',
|
||||
black: '<span class="bg-black text-white">',
|
||||
end: '</span>',
|
||||
...messages,
|
||||
}),
|
||||
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 = [];
|
||||
|
||||
const [standard, systemProcessing, lever, server, finale] = [
|
||||
loadPage.value('standard', {
|
||||
bigblack: '<div class="p-2 mt-1 mb-1 bg-black text-white">',
|
||||
endbig: '</div>',
|
||||
}),
|
||||
loadPage.value('ongeki-system-processing'),
|
||||
loadPage.value('ongeki-lever'),
|
||||
loadPage.value('chunithm-server', {
|
||||
link: '<a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ#game-is-stuck-at-checking-distribution-server" target="_blank">',
|
||||
}),
|
||||
loadPage.value('finale', {
|
||||
segaguide:
|
||||
'<a href="https://gitea.tendokyu.moe/Dniel97/SEGAguide/wiki/FAQ" target="_blank">',
|
||||
twotorial: '<a href="https://two-torial.xyz/" target="_blank">',
|
||||
}),
|
||||
];
|
||||
const standardOngeki = {
|
||||
...standard,
|
||||
image: '/help-standard-ongeki.png',
|
||||
};
|
||||
const standardChunithm = {
|
||||
...standard,
|
||||
image: '/help-standard-chunithm.png',
|
||||
};
|
||||
const finaleOngeki = {
|
||||
...finale,
|
||||
image: '/help-finale-ongeki.png',
|
||||
};
|
||||
const finaleChunithm = {
|
||||
...finale,
|
||||
image: '/help-finale-chunithm.png',
|
||||
};
|
||||
|
||||
switch (prf.current?.meta.game) {
|
||||
case 'ongeki':
|
||||
res.push(systemProcessing);
|
||||
@ -75,40 +113,18 @@ const data: ComputedRef<Datum[]> = computed(() => {
|
||||
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',
|
||||
};
|
||||
const context = ref({
|
||||
index: 0,
|
||||
});
|
||||
|
||||
const counter = ref(0);
|
||||
|
||||
const exitLabel = computed(() => {
|
||||
return props.firstTime === true && counter.value < data.value.length - 1
|
||||
? 'Skip'
|
||||
: 'Close';
|
||||
return props.firstTime === true &&
|
||||
context.value.index < data.value.length - 1
|
||||
? t('skip')
|
||||
: t('close');
|
||||
});
|
||||
|
||||
const page = ref(0);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -122,13 +138,15 @@ const exitLabel = computed(() => {
|
||||
: `${game ? prettyPrint(game) : '<game>'} help`
|
||||
"
|
||||
:style="{ width: '760px', scale: client.scaleValue }"
|
||||
v-on:show="() => ((context.index = 0), (page = 0))"
|
||||
>
|
||||
<Carousel
|
||||
:value="data"
|
||||
:num-visible="1"
|
||||
:num-scroll="1"
|
||||
:page="counter"
|
||||
v-on:update:page="(p) => (counter = p)"
|
||||
:context="context"
|
||||
:page="page"
|
||||
v-on:update:page="(p) => ((context.index = p), (page = p))"
|
||||
>
|
||||
<template #item="slotProps">
|
||||
<div class="md-container markdown">
|
||||
@ -150,10 +168,10 @@ const exitLabel = computed(() => {
|
||||
</Carousel>
|
||||
<div style="width: 100%; text-align: center">
|
||||
<Button
|
||||
v-if="counter < data.length - 1"
|
||||
v-if="context.index < data.length - 1"
|
||||
class="m-auto mr-4"
|
||||
label="Next"
|
||||
@click="() => (counter += 1)"
|
||||
:label="t('next')"
|
||||
@click="() => (page += 1)"
|
||||
/>
|
||||
<Button
|
||||
class="m-auto"
|
||||
|
@ -61,7 +61,7 @@ prf.reload();
|
||||
<MiscOptions />
|
||||
<OptionCategory
|
||||
title="Extensions"
|
||||
v-if="prf.current!.meta.game === 'chunithm'"
|
||||
v-if="prf.current?.meta.game === 'chunithm'"
|
||||
>
|
||||
<OptionRow :title="t('cfg.extensions.saekawa')">
|
||||
<FileEditor
|
||||
@ -72,7 +72,7 @@ prf.reload();
|
||||
></OptionCategory>
|
||||
<OptionCategory
|
||||
:title="t('cfg.extensions.title')"
|
||||
v-if="prf.current!.meta.game === 'ongeki'"
|
||||
v-if="prf.current?.meta.game === 'ongeki'"
|
||||
>
|
||||
<OptionRow :title="t('cfg.extensions.inohara')">
|
||||
<FileEditor
|
||||
|
@ -8,7 +8,7 @@ import { usePrfStore } from '../stores';
|
||||
import { Patch } from '../types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { t, te } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
@ -51,14 +51,14 @@ const hexModel = computed({
|
||||
|
||||
// Doesn't need to be reactive
|
||||
const nameKey = `patch.${props.patch?.id}`;
|
||||
let name = t(nameKey);
|
||||
if (name === nameKey) {
|
||||
name = props.patch?.name ?? 'No name';
|
||||
}
|
||||
const name = te(nameKey) ? t(nameKey) : props.patch?.name;
|
||||
|
||||
const tooltipKey = `patch.${props.patch?.id}-tooltip`;
|
||||
const tooltip = te(tooltipKey) ? t(tooltipKey) : props.patch?.tooltip;
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionRow :title="name" :tooltip="patch?.tooltip" :greytext="patch?.id">
|
||||
<OptionRow :title="name" :tooltip="tooltip" :greytext="patch?.id">
|
||||
<ToggleSwitch
|
||||
v-if="patch?.type === undefined"
|
||||
:model-value="prf.current!.data.patches?.[patch!.id!] !== undefined"
|
||||
|
@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, ref } from 'vue';
|
||||
// import Select from 'primevue/select';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import OptionCategory from './OptionCategory.vue';
|
||||
import PatchEntry from './PatchEntry.vue';
|
||||
@ -32,8 +33,6 @@ invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
|
||||
target: amd,
|
||||
})) as Patch[];
|
||||
})();
|
||||
|
||||
const errorMessage = t('patch.noneFound');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@ -49,7 +48,7 @@ const errorMessage = t('patch.noneFound');
|
||||
/>
|
||||
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
|
||||
<div v-if="gamePatches !== null && gamePatches.length === 0">
|
||||
{{ errorMessage }}
|
||||
{{ t('patch.noneFound') }}
|
||||
</div>
|
||||
</OptionCategory>
|
||||
<OptionCategory title="amdaemon.exe" always-found>
|
||||
@ -58,9 +57,22 @@ const errorMessage = t('patch.noneFound');
|
||||
v-for="p in amdPatches"
|
||||
:patch="p"
|
||||
/>
|
||||
<div v-if="gamePatches === null">Loading...</div>
|
||||
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
|
||||
<div v-if="amdPatches !== null && amdPatches.length === 0">
|
||||
{{ errorMessage }}
|
||||
{{ t('patch.noneFound') }}
|
||||
<!-- <br />
|
||||
<Select
|
||||
class="mt-3"
|
||||
style="width: 400px"
|
||||
:options="[
|
||||
{},
|
||||
{},
|
||||
]"
|
||||
:placeholder="t('patch.forceLoad')"
|
||||
size="small"
|
||||
option-label="title"
|
||||
option-value="value"
|
||||
></Select> -->
|
||||
</div>
|
||||
</OptionCategory>
|
||||
</template>
|
||||
|
@ -18,10 +18,17 @@ const prf = usePrfStore();
|
||||
const client = useClientStore();
|
||||
const general = useGeneralStore();
|
||||
|
||||
const hasChunithm = ref(false);
|
||||
const exportVisible = ref(false);
|
||||
const exportKeychip = ref(false);
|
||||
const files = new Set<string>();
|
||||
|
||||
(async () => {
|
||||
hasChunithm.value = (
|
||||
(await invoke('list_platform_capabilities')) as string[]
|
||||
).includes('chunithm');
|
||||
})();
|
||||
|
||||
const exportTemplate = async () => {
|
||||
const fl = [...files.values()];
|
||||
exportVisible.value = false;
|
||||
@ -30,7 +37,7 @@ const exportTemplate = async () => {
|
||||
files: fl,
|
||||
});
|
||||
await invoke('open_file', {
|
||||
path: await path.join(general.configDir, 'exports'),
|
||||
path: await path.join(await general.configDir, 'exports'),
|
||||
});
|
||||
};
|
||||
|
||||
@ -83,16 +90,16 @@ const importPick = async () => {
|
||||
modal
|
||||
:visible="exportVisible"
|
||||
:closable="false /*this shit doesn't work */"
|
||||
:header="`Export ${prf.current?.meta.name}`"
|
||||
:header="`${t('profile.export')} ${prf.current?.meta.name}`"
|
||||
:style="{ width: '300px', scale: client.scaleValue }"
|
||||
>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row">
|
||||
<div class="grow">Export keychip</div>
|
||||
<div class="grow">{{ t('profile.export') }} keychip</div>
|
||||
<ToggleSwitch v-model="exportKeychip" />
|
||||
</div>
|
||||
<div class="flex flex-row" v-for="f in fileListCurrent">
|
||||
<div class="grow">Export {{ f }}</div>
|
||||
<div class="grow">{{ t('profile.export') }} {{ f }}</div>
|
||||
<ToggleSwitch
|
||||
:model-value="true"
|
||||
@update:model-value="
|
||||
@ -134,6 +141,7 @@ const importPick = async () => {
|
||||
@click="() => prf.create('ongeki')"
|
||||
/>
|
||||
<Button
|
||||
v-if="hasChunithm"
|
||||
:label="t('profile.create', { game: t('game.chunithm') })"
|
||||
icon="pi pi-file-plus"
|
||||
class="chunithm-button profile-button"
|
||||
@ -142,14 +150,14 @@ const importPick = async () => {
|
||||
</div>
|
||||
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
|
||||
<Button
|
||||
label="Import template"
|
||||
:label="t('profile.importTemplate')"
|
||||
icon="pi pi-file-import"
|
||||
class="import-button profile-button"
|
||||
@click="() => importPick()"
|
||||
/>
|
||||
<Button
|
||||
:disabled="prf.current === null"
|
||||
label="Export template"
|
||||
:label="t('profile.exportTemplate')"
|
||||
icon="pi pi-file-export"
|
||||
class="profile-button"
|
||||
@click="() => openExportDialog()"
|
||||
@ -171,6 +179,7 @@ const importPick = async () => {
|
||||
:options="[
|
||||
{ title: 'English', value: 'en' },
|
||||
// { title: '日本語', value: 'ja' },
|
||||
{ title: 'Polski', value: 'pl' },
|
||||
]"
|
||||
size="small"
|
||||
option-label="title"
|
||||
|
@ -73,10 +73,12 @@ const promptDeleteProfile = async () => {
|
||||
|
||||
const dataExists = ref(false);
|
||||
|
||||
path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
|
||||
async (p) => {
|
||||
dataExists.value = await invoke('file_exists', { path: p });
|
||||
}
|
||||
general.dataDir.then((dataDir) =>
|
||||
path
|
||||
.join(dataDir, `profile-${props.p!.game}-${props.p!.name}`)
|
||||
.then(async (p) => {
|
||||
dataExists.value = await invoke('file_exists', { path: p });
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
||||
@ -145,11 +147,15 @@ path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
|
||||
class="self-center"
|
||||
style="width: 2rem; height: 2rem"
|
||||
@click="
|
||||
path
|
||||
.join(general.configDir, `profile-${p!.game}-${p!.name}`)
|
||||
.then(async (path) => {
|
||||
await invoke('open_file', { path });
|
||||
})
|
||||
async () =>
|
||||
path
|
||||
.join(
|
||||
await general.configDir,
|
||||
`profile-${p!.game}-${p!.name}`
|
||||
)
|
||||
.then(async (path) => {
|
||||
await invoke('open_file', { path });
|
||||
})
|
||||
"
|
||||
/>
|
||||
<Button
|
||||
@ -162,11 +168,15 @@ path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
|
||||
class="self-center"
|
||||
style="width: 2rem; height: 2rem"
|
||||
@click="
|
||||
path
|
||||
.join(general.dataDir, `profile-${p!.game}-${p!.name}`)
|
||||
.then(async (path) => {
|
||||
await invoke('open_file', { path });
|
||||
})
|
||||
async () =>
|
||||
path
|
||||
.join(
|
||||
await general.dataDir,
|
||||
`profile-${p!.game}-${p!.name}`
|
||||
)
|
||||
.then(async (path) => {
|
||||
await invoke('open_file', { path });
|
||||
})
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
@ -99,18 +99,23 @@ const createShortcut = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasShortcut = ref(false);
|
||||
|
||||
(async () => {
|
||||
hasShortcut.value = (
|
||||
(await invoke('list_platform_capabilities')) as string[]
|
||||
).includes('shortcut');
|
||||
})();
|
||||
|
||||
const menuItems = computed(() => {
|
||||
const base = [
|
||||
let base = [
|
||||
{
|
||||
label: t('start.button.unchecked'),
|
||||
icon: 'pi pi-exclamation-circle',
|
||||
command: async () => await startline(true, false),
|
||||
},
|
||||
{
|
||||
label: t('start.button.shortcut'),
|
||||
icon: 'pi pi-link',
|
||||
command: createShortcut,
|
||||
},
|
||||
];
|
||||
let baseTail = [
|
||||
{
|
||||
label: t('start.button.help'),
|
||||
icon: 'pi pi-question-circle',
|
||||
@ -123,8 +128,18 @@ const menuItems = computed(() => {
|
||||
if (prf.current === null) {
|
||||
return [];
|
||||
}
|
||||
if (hasShortcut.value === true) {
|
||||
base = [
|
||||
...base,
|
||||
{
|
||||
label: t('start.button.shortcut'),
|
||||
icon: 'pi pi-link',
|
||||
command: createShortcut,
|
||||
},
|
||||
];
|
||||
}
|
||||
if (prf.current.meta.game === 'chunithm') {
|
||||
return base;
|
||||
return [...base, ...baseTail];
|
||||
}
|
||||
if (prf.current.meta.game === 'ongeki') {
|
||||
return [
|
||||
@ -137,8 +152,9 @@ const menuItems = computed(() => {
|
||||
{
|
||||
label: t('start.button.cache'),
|
||||
icon: 'pi pi-trash',
|
||||
command: async () => {},
|
||||
command: async () => await invoke('clear_cache'),
|
||||
},
|
||||
...baseTail,
|
||||
];
|
||||
}
|
||||
});
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import { Ref, computed, onMounted, ref } from 'vue';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Select from 'primevue/select';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
@ -65,16 +65,20 @@ const loadDisplays = () => {
|
||||
.catch(() => {});
|
||||
};
|
||||
|
||||
loadDisplays();
|
||||
onMounted(() => {
|
||||
loadDisplays();
|
||||
});
|
||||
|
||||
const game = prf.current!.meta.game;
|
||||
const isVertical = game === 'ongeki';
|
||||
const adjustableRez = game === 'ongeki';
|
||||
const canSkipPrimarySwitch = game === 'ongeki';
|
||||
const game = computed(() => prf.current!.meta.game);
|
||||
const isVertical = computed(() => prf.current!.meta.game === 'ongeki');
|
||||
const adjustableRez = computed(() => prf.current!.meta.game === 'ongeki');
|
||||
const canSkipPrimarySwitch = computed(
|
||||
() => prf.current!.meta.game === 'ongeki'
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<OptionCategory title="Display">
|
||||
<OptionCategory :title="t('cfg.display.title')">
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('display')"
|
||||
:title="t('cfg.display.target')"
|
||||
@ -90,7 +94,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
class="number-input"
|
||||
title="Game resolution"
|
||||
:title="t('cfg.display.resolution')"
|
||||
v-if="adjustableRez"
|
||||
>
|
||||
<InputNumber
|
||||
@ -115,9 +119,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
|
||||
<SelectButton
|
||||
v-model="prf.current!.data.display.mode"
|
||||
:options="[
|
||||
{ title: 'Window', value: 'Window' },
|
||||
{ title: 'Borderless window', value: 'Borderless' },
|
||||
{ title: 'Fullscreen', value: 'Fullscreen' },
|
||||
{ title: t('cfg.display.window'), value: 'Window' },
|
||||
{ title: t('cfg.display.borderless'), value: 'Borderless' },
|
||||
{ title: t('cfg.display.fullscreen'), value: 'Fullscreen' },
|
||||
]"
|
||||
:allow-empty="false"
|
||||
option-label="title"
|
||||
@ -197,16 +201,19 @@ const canSkipPrimarySwitch = game === 'ongeki';
|
||||
<OptionRow
|
||||
:title="t('cfg.display.dontSwitchPrimary')"
|
||||
v-if="
|
||||
capabilities.includes('display') &&
|
||||
prf.current?.data.display.target !== 'default' &&
|
||||
(prf.current!.data.display.dont_switch_primary ||
|
||||
displayList.length > 2) &&
|
||||
canSkipPrimarySwitch
|
||||
!capabilities.includes('display') ||
|
||||
(prf.current?.data.display.target !== 'default' &&
|
||||
(prf.current!.data.display.dont_switch_primary ||
|
||||
displayList.length > 2) &&
|
||||
canSkipPrimarySwitch)
|
||||
"
|
||||
:dangerous-tooltip="t('cfg.display.dontSwitchPrimaryTooltip')"
|
||||
>
|
||||
<ToggleSwitch
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
:disabled="
|
||||
extraDisplayOptionsDisabled &&
|
||||
capabilities.includes('display')
|
||||
"
|
||||
v-model="prf.current!.data.display.dont_switch_primary"
|
||||
/>
|
||||
</OptionRow>
|
||||
@ -214,9 +221,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
|
||||
:title="t('cfg.display.index')"
|
||||
class="number-input"
|
||||
v-if="
|
||||
capabilities.includes('display') &&
|
||||
prf.current?.data.display.target !== 'default' &&
|
||||
prf.current!.data.display.dont_switch_primary
|
||||
!capabilities.includes('display') ||
|
||||
(prf.current?.data.display.target !== 'default' &&
|
||||
prf.current!.data.display.dont_switch_primary)
|
||||
"
|
||||
>
|
||||
<InputNumber
|
||||
@ -225,8 +232,12 @@ const canSkipPrimarySwitch = game === 'ongeki';
|
||||
:min="game === 'chunithm' ? 0 : 1"
|
||||
:max="32"
|
||||
:use-grouping="false"
|
||||
placeholder="1"
|
||||
v-model="prf.current!.data.display.monitor_index_override"
|
||||
:disabled="extraDisplayOptionsDisabled"
|
||||
:disabled="
|
||||
extraDisplayOptionsDisabled &&
|
||||
capabilities.includes('display')
|
||||
"
|
||||
:allow-empty="true"
|
||||
/>
|
||||
</OptionRow>
|
||||
|
@ -95,7 +95,7 @@ const prf = usePrfStore();
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="prf.current?.meta.game === 'chunithm'">
|
||||
<div class="absolute left-1/2 top-1/5">
|
||||
<div class="absolute left-9/17 top-1/12">
|
||||
<div
|
||||
class="flex flex-row flex-nowrap gap-2 self-center w-full"
|
||||
>
|
||||
@ -108,6 +108,7 @@ const prf = usePrfStore();
|
||||
button="ir"
|
||||
:index="idx - 1"
|
||||
:tooltip="`ir${idx}`"
|
||||
tall
|
||||
small
|
||||
color="rgba(0, 255, 0, 0.2)"
|
||||
/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import Select from 'primevue/select';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
@ -19,6 +19,14 @@ const prf = usePrfStore();
|
||||
const pkgs = usePkgStore();
|
||||
const confirmDialog = useConfirm();
|
||||
|
||||
const capabilities: Ref<string[]> = ref([]);
|
||||
|
||||
invoke('list_platform_capabilities').then(async (v: unknown) => {
|
||||
if (Array.isArray(v)) {
|
||||
capabilities.value.push(...v);
|
||||
}
|
||||
});
|
||||
|
||||
const names = computed(() => {
|
||||
switch (prf.current?.meta.game) {
|
||||
case 'ongeki': {
|
||||
@ -35,8 +43,6 @@ const names = computed(() => {
|
||||
io: 'chuniio',
|
||||
};
|
||||
}
|
||||
case undefined:
|
||||
throw new Error('Option tab without a profile');
|
||||
}
|
||||
});
|
||||
|
||||
@ -59,14 +65,14 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
<template>
|
||||
<OptionCategory :title="t('cfg.segatools.general')">
|
||||
<OptionRow
|
||||
:title="names.exe"
|
||||
:title="names?.exe"
|
||||
:tooltip="t('cfg.segatools.targetTooltip')"
|
||||
>
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
:promptname="names.exe"
|
||||
:promptname="names?.exe"
|
||||
extension="exe"
|
||||
:value="prf.current!.data.sgt.target"
|
||||
:value="prf.current?.data.sgt.target"
|
||||
:callback="
|
||||
(value: string) => (
|
||||
(prf.current!.data.sgt.target = value),
|
||||
@ -80,7 +86,7 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
placeholder="amfs"
|
||||
:value="prf.current!.data.sgt.amfs"
|
||||
:value="prf.current?.data.sgt.amfs"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.sgt.amfs = value)
|
||||
"
|
||||
@ -106,7 +112,7 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
:title="names.hook"
|
||||
:title="names?.hook"
|
||||
:tooltip="
|
||||
t('cfg.segatools.installTooltip', {
|
||||
thing: t('cfg.segatools.hooks'),
|
||||
@ -132,19 +138,18 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
:title="names.io"
|
||||
:tooltip="
|
||||
t('cfg.segatools.installTooltip', {
|
||||
:title="names?.io"
|
||||
:tooltip="`${t('cfg.segatools.ioModulesDesc')}
|
||||
${t('cfg.segatools.installTooltip', {
|
||||
thing: t('cfg.segatools.ioModules'),
|
||||
})
|
||||
"
|
||||
})}`"
|
||||
>
|
||||
<Select
|
||||
v-model="prf.current!.data.sgt.io2"
|
||||
:options="[
|
||||
{ title: 'native io4', value: 'hardware' },
|
||||
{ title: t('cfg.segatools.io4'), value: 'hardware' },
|
||||
{
|
||||
title: 'segatools built-in (keyboard)',
|
||||
title: t('cfg.segatools.ioBuiltIn'),
|
||||
value: 'segatools_built_in',
|
||||
},
|
||||
...pkgs
|
||||
@ -164,5 +169,29 @@ const checkSegatoolsIni = async (target: string) => {
|
||||
option-value="value"
|
||||
></Select>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('wine')"
|
||||
:title="t('cfg.wine.runtime')"
|
||||
>
|
||||
<FilePicker
|
||||
:directory="false"
|
||||
:value="prf.current!.data.wine.runtime"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.wine.runtime = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
<OptionRow
|
||||
v-if="capabilities.includes('wine')"
|
||||
:title="t('cfg.wine.prefix')"
|
||||
>
|
||||
<FilePicker
|
||||
:directory="true"
|
||||
:value="prf.current!.data.wine.prefix"
|
||||
:callback="
|
||||
(value: string) => (prf.current!.data.wine.prefix = value)
|
||||
"
|
||||
></FilePicker>
|
||||
</OptionRow>
|
||||
</OptionCategory>
|
||||
</template>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import en from './i18n/en';
|
||||
import ja from './i18n/ja';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
export type Locale = 'en' | 'ja';
|
||||
export type Locale = 'en' | 'ja' | 'pl';
|
||||
|
||||
const loadLocaleMessages = async (locale: Locale) => {
|
||||
return (await import(`./i18n/${locale}.ts`)).default;
|
||||
@ -12,7 +11,9 @@ const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: { en, ja },
|
||||
warnHtmlInMessage: false,
|
||||
warnHtmlMessage: false,
|
||||
messages: { en, ja: {}, pl: {} },
|
||||
});
|
||||
|
||||
const setLocale = async (locale: Locale) => {
|
||||
|
105
src/i18n/en.ts
105
src/i18n/en.ts
@ -4,6 +4,11 @@ export default {
|
||||
enable: 'Enable',
|
||||
disable: 'Disable',
|
||||
default: 'Default',
|
||||
search: 'Search',
|
||||
next: 'Next',
|
||||
skip: 'Skip',
|
||||
close: 'Close',
|
||||
by: 'by {namespace}',
|
||||
start: {
|
||||
failed: 'Start check failed',
|
||||
accept: 'Run anyway',
|
||||
@ -21,11 +26,11 @@ export default {
|
||||
button: {
|
||||
start: 'START',
|
||||
stop: 'STOP',
|
||||
unchecked: 'Start unchecked',
|
||||
unchecked: 'Skip checks and start',
|
||||
shortcut: 'Create desktop shortcut',
|
||||
help: 'Help',
|
||||
refresh: 'Refresh and start',
|
||||
cache: 'Clear cache',
|
||||
refresh: 'Reapply mods and start',
|
||||
cache: 'Clear mod cache',
|
||||
},
|
||||
},
|
||||
game: {
|
||||
@ -38,6 +43,22 @@ export default {
|
||||
delete: 'Delete profile',
|
||||
reallyDelete: 'Are you sure you want to delete {profile}?',
|
||||
template: 'STARTLINER template',
|
||||
importTemplate: 'Import template',
|
||||
exportTemplate: 'Export template',
|
||||
export: 'Export',
|
||||
},
|
||||
creator: {
|
||||
header: 'Package creator',
|
||||
basic: 'Basic information',
|
||||
name: 'Name',
|
||||
description: 'Description',
|
||||
website: 'Website',
|
||||
type: 'Package type',
|
||||
rainy: 'Standard',
|
||||
segatools: 'Segatools',
|
||||
native: 'Native',
|
||||
games: 'Games',
|
||||
packageFormat: 'Package format spec',
|
||||
},
|
||||
store: {
|
||||
installRecommended: 'Install recommended packages',
|
||||
@ -45,14 +66,33 @@ export default {
|
||||
deprecated: 'Show deprecated',
|
||||
nsfw: 'Show NSFW',
|
||||
incompatible: 'This package is currently incompatible with STARTLINER.',
|
||||
|
||||
includeCategories: 'Include categories',
|
||||
excludeCategories: 'Exclude categories',
|
||||
},
|
||||
pkglist: {
|
||||
missing: 'Missing',
|
||||
local: 'Local packages',
|
||||
namespace: 'By namespace',
|
||||
type: 'By type',
|
||||
category: 'By category',
|
||||
standard: 'Standard mods',
|
||||
native: 'Native mods',
|
||||
segatools: 'segatools',
|
||||
unsupported: 'Unsupported',
|
||||
exclusions: 'Exclusions:',
|
||||
},
|
||||
patch: {
|
||||
loading: 'Loading...',
|
||||
noneFound:
|
||||
"No compatible patches found. Make sure you're using unpacked and unpatched files.",
|
||||
forceLoad: 'Force load',
|
||||
// Example patch name override
|
||||
'standard-no-encryption': 'No encryption',
|
||||
// 'standard-no-encryption': 'No encryption',
|
||||
// 'standard-no-encryption-tooltip': 'Will also disable TLS',
|
||||
// It is also possible to add a tooltip where there normally is none
|
||||
// 'standard-maximum-tracks-tooltip': 'The number of tracks per credit',
|
||||
// For more info check https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Translation-%26-Localization
|
||||
},
|
||||
cfg: {
|
||||
afterRestart: 'Applied after a restart',
|
||||
@ -64,9 +104,14 @@ export default {
|
||||
'STARTLINER expects unpacked executables put into otherwise clean data.',
|
||||
hooks: 'Hooks',
|
||||
ioModules: 'IO modules',
|
||||
ioModulesDesc: 'This should match your desired input method.',
|
||||
ioBuiltIn: 'segatools built-in (keyboard)',
|
||||
io4: 'Native IO4',
|
||||
installTooltip: '{thing} can be downloaded from the package store.',
|
||||
},
|
||||
display: {
|
||||
title: 'Display',
|
||||
resolution: 'Game resolution',
|
||||
primary: 'Primary',
|
||||
target: 'Target display',
|
||||
mode: 'Mode',
|
||||
@ -82,6 +127,9 @@ export default {
|
||||
portrait: 'Portrait',
|
||||
landscape: 'Landscape',
|
||||
flipped: 'flipped',
|
||||
window: 'Window',
|
||||
borderless: 'Borderless window',
|
||||
fullscreen: 'Fullscreen',
|
||||
},
|
||||
network: {
|
||||
title: 'Network',
|
||||
@ -141,8 +189,13 @@ export default {
|
||||
'Only applicable if the IO module is set to segatools built-in (keyboard) or a compatible third-party module (like mu3io.NET)',
|
||||
leverMode: 'Lever mode',
|
||||
mouse: 'Mouse',
|
||||
irTooltip:
|
||||
'When playing on an actual keyboard, only bind ir1; leave the rest unbound',
|
||||
},
|
||||
wine: {
|
||||
prefix: 'Wine prefix',
|
||||
runtime: 'Wine runtime',
|
||||
},
|
||||
|
||||
startliner: {
|
||||
offlineMode: 'Offline mode',
|
||||
offlineModeTooltip: 'Disables the package store.',
|
||||
@ -150,4 +203,46 @@ export default {
|
||||
verbose: 'Detailed logs',
|
||||
},
|
||||
},
|
||||
onboarding: {
|
||||
or: 'or',
|
||||
backButton: 'a button on the back of the controller',
|
||||
standard: `
|
||||
You might get stuck on the following screen:
|
||||
|
||||
{bigblack}Aグループの基準機から設定を取得{endbig}
|
||||
|
||||
In which case, you should go to the test menu, and in game settings {black}ゲーム設定{end} switch from "follow the standard machine" {black}基準機に従う{end} to "standard machine" {black}基準機{end}.
|
||||
|
||||
The test menu can be accessed with %TESTMENU%.
|
||||
`,
|
||||
|
||||
'ongeki-system-processing': `
|
||||
You might get stuck on this screen for several minutes. _This is normal_. The game just takes a long time to load data.
|
||||
|
||||
If you install <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, subsequent launches will be much faster.
|
||||
`,
|
||||
|
||||
'ongeki-lever': `
|
||||
You also have to calibrate the lever, or you may get the error 3301.
|
||||
|
||||
Go to lever settings ({black}レバー設定{end}), move the lever to both edges, then press "end" ({black}終了{end}) and "save" ({black}保存する{end}).
|
||||
`,
|
||||
|
||||
'chunithm-server': `
|
||||
If you're stuck on this screen, restart the game.
|
||||
|
||||
If the problem persists, {link}check your network configuration{endlink}
|
||||
`,
|
||||
|
||||
finale: `
|
||||
You can access this page any time by right-clicking the START button.
|
||||
|
||||
Additional resources:
|
||||
|
||||
- {segaguide}SEGAguide{endlink}
|
||||
- {twotorial}two-torial{endlink}
|
||||
|
||||
## Have fun
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
288
src/i18n/pl.ts
Normal file
288
src/i18n/pl.ts
Normal file
@ -0,0 +1,288 @@
|
||||
export default {
|
||||
ok: 'OK',
|
||||
cancel: 'Anuluj',
|
||||
enable: 'Włącz',
|
||||
disable: 'Wyłącz',
|
||||
default: 'Domyślne',
|
||||
search: 'Wyszukaj',
|
||||
next: 'Dalej',
|
||||
skip: 'Pomiń',
|
||||
close: 'Zamknij',
|
||||
by: 'od {namespace}',
|
||||
start: {
|
||||
failed: 'Uruchomienie nie powiodło się',
|
||||
accept: 'Uruchom mimo to',
|
||||
error: {
|
||||
package: 'Brakujący pakiet',
|
||||
dependency: 'Brakująca dependencja',
|
||||
tool: 'Brakujące narzędzie',
|
||||
unknown: 'Nieznany błąd',
|
||||
},
|
||||
tooltip: {
|
||||
game: 'Należy najpierw wskazać lokalizację gry',
|
||||
amfs: 'Należy najpierw wskazać lokalizację amfs',
|
||||
segatools: 'Należy dodać hook segatools',
|
||||
},
|
||||
button: {
|
||||
start: 'START',
|
||||
stop: 'STOP',
|
||||
unchecked: 'Uruchom bez sprawdzania',
|
||||
shortcut: 'Utwórz skrót',
|
||||
help: 'Pomoc',
|
||||
refresh: 'Uruchom po re-aplikacji modów',
|
||||
cache: 'Wyczyść mod cache',
|
||||
},
|
||||
},
|
||||
game: {
|
||||
ongeki: 'O.N.G.E.K.I.',
|
||||
chunithm: 'CHUNITHM',
|
||||
},
|
||||
profile: {
|
||||
welcome: 'Witaj w STARTLINERZE! Zacznij od utworzenia profilu',
|
||||
create: 'Profil {game}',
|
||||
delete: 'Usuń profil',
|
||||
reallyDelete: 'Czy na pewno chcesz usunąć {profile}?',
|
||||
template: 'Szablon',
|
||||
importTemplate: 'Importuj szablon',
|
||||
exportTemplate: 'Eksportuj szablon',
|
||||
export: 'Eksportuj',
|
||||
},
|
||||
creator: {
|
||||
header: 'Kreator pakietów',
|
||||
basic: 'Podstawowe informacje',
|
||||
name: 'Nazwa',
|
||||
description: 'Opis',
|
||||
website: 'Strona internetowa',
|
||||
type: 'Typ',
|
||||
rainy: 'Standardowy',
|
||||
segatools: 'Segatools',
|
||||
native: 'Natywny',
|
||||
games: 'Gry',
|
||||
packageFormat: 'Specyfikacja formatu',
|
||||
},
|
||||
store: {
|
||||
installRecommended: 'Dodaj zalecane pakiety',
|
||||
installed: 'Pokaż zainstalowane',
|
||||
deprecated: 'Pokaż przestarzałe',
|
||||
nsfw: 'Pokaż mityczny O.N.G.E.K.I. Sex Mod dlaczego ta opcja w ogóle tu jest',
|
||||
incompatible:
|
||||
'Ten pakiet jest obecnie niekompatybilny ze STARTLINEREM.',
|
||||
includeCategories: 'Włącz kategorie',
|
||||
excludeCategories: 'Wyłącz kategorie',
|
||||
},
|
||||
pkglist: {
|
||||
missing: 'Niedostępne',
|
||||
local: 'Lokalne pakiety',
|
||||
namespace: 'Po przestrzeni nazw',
|
||||
type: 'Po typie',
|
||||
category: 'Po kategorii',
|
||||
standard: 'Standardowe mody',
|
||||
native: 'Natywne mody',
|
||||
segatools: 'segatools',
|
||||
unsupported: 'Niewspierane',
|
||||
exclusions: 'Czarna lista:',
|
||||
},
|
||||
patch: {
|
||||
loading: 'Wczytuję...',
|
||||
noneFound:
|
||||
'Brak kompatybilnych łatek. Upewnij się, że używasz czystych odpakowanych plików.',
|
||||
forceLoad: 'Wymuś załadowanie',
|
||||
'standard-shared-audio':
|
||||
'Wymuś współdzielony tryb dźwięku; częstotliwość w systemie musi wynosić 48kHz',
|
||||
'standard-shared-audio-tooltip':
|
||||
'Poprawia kompatybilność, ale może zwiększyć opóźnienie',
|
||||
'standard-2ch': 'Wymuś stereo',
|
||||
'standard-2ch-tooltip': 'Może powodować bass overload',
|
||||
'standard-song-timer': 'Wyłącz timer wyboru utworu',
|
||||
'standard-map-timer': 'Timer wyboru mapy',
|
||||
'standard-map-timer-tooltip':
|
||||
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
|
||||
'standard-ticket-timer': 'Timer wyboru biletu',
|
||||
'standard-ticket-timer-tooltip':
|
||||
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
|
||||
'standard-course-timer': 'Timer wyboru dana',
|
||||
'standard-course-timer-tooltip':
|
||||
'Jeśli ustawiony na wartość ujemną, timer wyniesie 968 + wartość (np. 968 + -1 = 967)',
|
||||
'standard-unlimited-tracks': 'Nieograniczona maksymalna liczba utworów',
|
||||
'standard-unlimited-tracks-tooltip':
|
||||
'Konieczne do grania więcej niż 7 utworów na kredyt',
|
||||
'standard-maximum-tracks': 'Maksymalna liczba utworów',
|
||||
'standard-no-encryption': 'Wyłącz szyfrowanie',
|
||||
'standard-no-encryption-tooltip': 'Wyłączy również TLS',
|
||||
'standard-no-tls': 'Wyłącz TLS',
|
||||
'standard-no-tls-tooltip': 'Obejście problemów z serwerem tytułowym',
|
||||
'standard-head-to-head': 'Napraw head to head',
|
||||
'standard-head-to-head-tooltip':
|
||||
'Naprawia nieskończoną synchronizację podczas próby połączenia w trybie head to head',
|
||||
'standard-bypass-1080p': 'Obejdź sprawdzenie 1080p',
|
||||
'standard-bypass-120hz': 'Obejdź sprawdzenie 120Hz',
|
||||
'standard-force-free-play-text': 'Wymuś tekst kredytu FREE PLAY',
|
||||
'standard-force-free-play-text-tooltip':
|
||||
'Zastępuje liczbę kredytów tekstem FREE PLAY',
|
||||
'standard-custom-free-play-length': 'Długość tekstu FREE PLAY',
|
||||
'standard-custom-free-play-length-tooltip':
|
||||
'Zmienia długość tekstu wyświetlanego, gdy włączony jest wymuszony tekst kredytu FREE PLAY',
|
||||
'standard-custom-free-play-text': 'Customowy tekst FREE PLAY',
|
||||
'standard-custom-free-play-text-tooltip': 'Zastąp tekst FREE PLAY',
|
||||
'standard-localhost':
|
||||
'Zezwól na serwer pod adresem 127.0.0.1/localhost',
|
||||
'standard-credit-freeze': 'Zamroź kredyty',
|
||||
'standard-credit-freeze-tooltip':
|
||||
'Zapobiega używaniu kredytów. Co najmniej jeden kredyt musi być dostępny, aby rozpocząć grę lub zakupić bilety premium.',
|
||||
'standard-openssl-fix': 'Napraw błąd OpenSSL SHA',
|
||||
'standard-openssl-fix-tooltip':
|
||||
'Naprawia crash na procesorach Intel 10. generacji i nowszych',
|
||||
},
|
||||
cfg: {
|
||||
afterRestart: 'Wymaga restartu.',
|
||||
hardware: 'Prawdziwy czytnik',
|
||||
segatools: {
|
||||
general: 'Ogólne',
|
||||
builtIn: 'Wbudowany emulator',
|
||||
targetTooltip:
|
||||
'STARTLINER oczekuje czystych danych, pomijając odpakowane exe.',
|
||||
hooks: 'Hooki',
|
||||
ioModules: 'Moduły IO',
|
||||
ioModulesDesc:
|
||||
'Powinien odpowiadać twojej preferowanej metodzie wejścia.',
|
||||
ioBuiltIn: 'Wbudowany emulator (klawiatura)',
|
||||
io4: 'Natywne IO4',
|
||||
installTooltip: '{thing} można pobrać z pobierajki pakietów.',
|
||||
},
|
||||
display: {
|
||||
title: 'Ekran',
|
||||
resolution: 'Rozdzielczość',
|
||||
primary: 'Główny',
|
||||
target: 'Docelowy wyświetlacz',
|
||||
mode: 'Tryb',
|
||||
rotation: 'Obrót',
|
||||
refreshRate: 'Częstotliwość odświeżania',
|
||||
borderlessFullscreen: 'Bezramkowy tryb pełnoekranowy',
|
||||
borderlessFullscreenTooltip:
|
||||
'Dopasuj rozdzielczość wyświetlacza do gry.',
|
||||
dontSwitchPrimary: 'Pomiń przełączanie głównego wyświetlacza',
|
||||
dontSwitchPrimaryTooltip:
|
||||
'Włącz tę opcję tylko wtedy, gdy przełączanie głównego wyświetlacza powoduje problemy. Monitory muszą mieć dopasowaną częstotliwość odświeżania.',
|
||||
index: 'Indeks wyświetlacza',
|
||||
portrait: 'Pion',
|
||||
landscape: 'Poziom',
|
||||
flipped: 'Odwrócony',
|
||||
window: 'Okno',
|
||||
borderless: 'Okno bez ramki',
|
||||
fullscreen: 'Pełny ekran',
|
||||
},
|
||||
network: {
|
||||
title: 'Sieć',
|
||||
type: 'Typ sieci',
|
||||
remote: 'Zdalny',
|
||||
localArtemis: 'Lokalny (ARTEMiS)',
|
||||
artemisPath: 'Lokalizacja ARTEMiSa',
|
||||
address: 'Adres serwera',
|
||||
keychip: 'Keychip',
|
||||
subnet: 'Podsieć',
|
||||
addrSuffix: 'Sufiks adresu',
|
||||
},
|
||||
aime: {
|
||||
type: 'Typ Aime',
|
||||
modules: 'Moduły Aime',
|
||||
code: 'Kod Aime',
|
||||
codeTooltip:
|
||||
'Dotyczy tylko wbudowanej emulacji lub zgodnych pakietów',
|
||||
aimedb: 'Użyj AiMeDB dla kart fizycznych',
|
||||
aimedbTooltip:
|
||||
'Decyduje czy karty fizyczne powinny używać AiMeDB do pobierania kodów dostępu. Jeśli łączysz się z hostowaną siecią, włącz tę opcję, aby załadować te same dane konta, jakie uzyskałxbyś na fizycznym cabie.',
|
||||
serialPort: 'Port szeregowy Aime',
|
||||
serialPortTooltip: `Porty można sprawdzić w Urządzeniach i drukarkach lub na googlechromelabs.github.io/serial-terminal
|
||||
Dla AIC Pico powinien być wybrany port AIME.`,
|
||||
serverName: 'Nazwa serwera',
|
||||
},
|
||||
misc: {
|
||||
title: 'Różne',
|
||||
intel: 'Obejście buga OpenSSL dla procesorów Intel ≥10 generacji',
|
||||
intelTooltip: 'Zaleca się zamiast tego załatać amdaemon.',
|
||||
other: 'Inne opcje segatools',
|
||||
otherTooltip:
|
||||
'Zaawansowane lub sytuacyjne opcje, które nie są objęte przez STARTLINERA',
|
||||
},
|
||||
extensions: {
|
||||
title: 'Rozszerzenia',
|
||||
bepInExConsole: 'Konsola BepInExa',
|
||||
audioMode: 'Tryb audio',
|
||||
audioTooltip:
|
||||
'Tryb ekskluzywny 2-kanałowy wymaga 7EVENDAYSHOLIDAYS-ExclusiveAudio',
|
||||
audioShared: 'Współdzielony',
|
||||
audio6Ch: 'Ekskluzywny 6-kanałowy',
|
||||
audio2Ch: 'Ekskluzywny 2-kanałowy',
|
||||
sampleRate: 'Częstotliwość',
|
||||
blacklist: 'Czarna lista utworów',
|
||||
blacklistTooltip:
|
||||
'Utwory w tym zakresie ID nie będą zapisywane ani przesyłane',
|
||||
bonusTracks: 'Odblokuj Bonusowe Utwory',
|
||||
bonusTracksTooltip:
|
||||
'Wyłączenie tej opcji może pomóc w uporządkowaniu listy utworów',
|
||||
saekawa: 'Plik konfiguracyjny Saekawy',
|
||||
inohara: 'Plik konfiguracyjny Inohary',
|
||||
},
|
||||
keyboard: {
|
||||
title: 'Klawiatura',
|
||||
tooltip:
|
||||
'Dotyczy tylko wtedy, gdy moduł IO jest ustawiony na wbudowaną emulację lub zgodny moduł (np. mu3io.NET)',
|
||||
leverMode: 'Tryb wajchy',
|
||||
mouse: 'Mysz',
|
||||
irTooltip:
|
||||
'Jeśli grasz na klawiaturze, ustaw tylko ir1; pozostałe zostaw wyłączone',
|
||||
},
|
||||
wine: {
|
||||
prefix: 'Wine prefix',
|
||||
runtime: 'Lokalizacja Wine',
|
||||
},
|
||||
startliner: {
|
||||
offlineMode: 'Tryb offline',
|
||||
offlineModeTooltip: 'Wyłącza pobierajkę pakietów.',
|
||||
autoUpdate: 'Automatyczne aktualizacje',
|
||||
verbose: 'Szczegółowe logi',
|
||||
},
|
||||
},
|
||||
onboarding: {
|
||||
or: 'lub',
|
||||
backButton: 'przycisku z tyłu',
|
||||
standard: `
|
||||
Możesz utknąć na następującym ekranie:
|
||||
|
||||
{bigblack}Aグループの基準機から設定を取得{endbig}
|
||||
|
||||
Wówczas musisz przejść do menu testowego i w ustawieniach gry {black}ゲーム設定{end} przełączyć z "podążaj za standardem" {black}基準機に従う{end} na "standard" {black}基準機{end}.
|
||||
|
||||
Do menu testowego możesz dostać się za pomocą %TESTMENU%.
|
||||
`,
|
||||
|
||||
'ongeki-system-processing': `
|
||||
Możesz utknąć na tym ekranie przez kilka(naście) minut. _To jest normalne_. Gra po prostu potrzebuje dużo czasu na załadowanie danych.
|
||||
|
||||
Jeśli zainstalujesz <code>7EVENDAYSHOLIDAYS/LoadBoost</code>, kolejne uruchomienia będą znacznie szybsze.
|
||||
`,
|
||||
|
||||
'ongeki-lever': `
|
||||
Musisz również skalibrować wajchę; w przeciwnym razie możesz otrzymać błąd 3301.
|
||||
|
||||
Przejdź do ustawień wajchy ({black}レバー設定{end}), przesuń wajchę do obu krawędzi, a następnie naciśnij "koniec" ({black}終了{end}) i "zapisz" ({black}保存する{end}).
|
||||
`,
|
||||
|
||||
'chunithm-server': `
|
||||
Jeśli utkniesz na tym ekranie, zrestartuj grę.
|
||||
|
||||
Jeśli problem będzie się powtarzał, {link}sprawdź swoją konfigurację sieciową{endlink}.
|
||||
`,
|
||||
|
||||
finale: `
|
||||
Możesz uzyskać dostęp do tej strony w każdej chwili, klikając prawym przyciskiem myszy przycisk START.
|
||||
|
||||
Dodatkowe zasoby:
|
||||
|
||||
- {segaguide}SEGAguide{endlink}
|
||||
- {twotorial}two-torial{endlink}
|
||||
|
||||
## Miłej zabawy
|
||||
`,
|
||||
},
|
||||
};
|
@ -32,23 +32,24 @@ export const useGeneralStore = defineStore('general', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const configDir = computed(() => {
|
||||
const loadDirs = async () => {
|
||||
if (dirs.value === null) {
|
||||
throw new Error('Invalid directory access');
|
||||
const d = (await invoke('list_directories')) as Dirs;
|
||||
dirs.value = d;
|
||||
}
|
||||
return dirs.value.config_dir;
|
||||
};
|
||||
|
||||
const configDir = computed(async () => {
|
||||
await loadDirs();
|
||||
return dirs.value!.config_dir;
|
||||
});
|
||||
const dataDir = computed(() => {
|
||||
if (dirs.value === null) {
|
||||
throw new Error('Invalid directory access');
|
||||
}
|
||||
return dirs.value.data_dir;
|
||||
const dataDir = computed(async () => {
|
||||
await loadDirs();
|
||||
return dirs.value!.data_dir;
|
||||
});
|
||||
const cacheDir = computed(() => {
|
||||
if (dirs.value === null) {
|
||||
throw new Error('Invalid directory access');
|
||||
}
|
||||
return dirs.value.cache_dir;
|
||||
const cacheDir = computed(async () => {
|
||||
await loadDirs();
|
||||
return dirs.value!.cache_dir;
|
||||
});
|
||||
|
||||
return {
|
||||
@ -321,8 +322,8 @@ export const usePrfStore = defineStore('prf', () => {
|
||||
const generalStore = useGeneralStore();
|
||||
|
||||
const configDir = computed(async () => {
|
||||
return await path.join(
|
||||
generalStore.configDir,
|
||||
return path.join(
|
||||
await generalStore.configDir,
|
||||
`profile-${current.value?.meta.game}-${current.value?.meta.name}`
|
||||
);
|
||||
});
|
||||
@ -373,6 +374,10 @@ export const useClientStore = defineStore('client', () => {
|
||||
const theme: Ref<'light' | 'dark' | 'system'> = ref('system');
|
||||
const onboarded: Ref<Game[]> = ref([]);
|
||||
const locale: Ref<Locale> = ref('en');
|
||||
const currentTab: Ref<string> = ref('users');
|
||||
const pkgListMode: Ref<'namespace' | 'type' | 'category'> =
|
||||
ref('namespace');
|
||||
const hiddenCategories: Ref<string[]> = ref([]);
|
||||
|
||||
const _scaleValue = (value: ScaleType) =>
|
||||
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2;
|
||||
@ -412,7 +417,7 @@ export const useClientStore = defineStore('client', () => {
|
||||
const input = JSON.parse(
|
||||
await readTextFile(
|
||||
await path.join(
|
||||
generalStore.configDir,
|
||||
await generalStore.configDir,
|
||||
'client-options.json'
|
||||
)
|
||||
)
|
||||
@ -439,6 +444,18 @@ export const useClientStore = defineStore('client', () => {
|
||||
if (input.locale) {
|
||||
locale.value = input.locale;
|
||||
}
|
||||
|
||||
if (input.currentTab) {
|
||||
currentTab.value = input.currentTab;
|
||||
}
|
||||
|
||||
if (input.pkgListMode) {
|
||||
pkgListMode.value = input.pkgListMode;
|
||||
}
|
||||
|
||||
if (input.hiddenCategories) {
|
||||
hiddenCategories.value = input.hiddenCategories;
|
||||
}
|
||||
await setLocale(locale.value);
|
||||
await setTheme(theme.value);
|
||||
} catch (e) {
|
||||
@ -464,7 +481,10 @@ export const useClientStore = defineStore('client', () => {
|
||||
const size = await w.innerSize();
|
||||
|
||||
await writeTextFile(
|
||||
await path.join(generalStore.configDir, 'client-options.json'),
|
||||
await path.join(
|
||||
await generalStore.configDir,
|
||||
'client-options.json'
|
||||
),
|
||||
JSON.stringify({
|
||||
scaleFactor: scaleFactor.value,
|
||||
windowSize: {
|
||||
@ -474,6 +494,9 @@ export const useClientStore = defineStore('client', () => {
|
||||
theme: theme.value,
|
||||
onboarded: onboarded.value,
|
||||
locale: locale.value,
|
||||
currentTab: currentTab.value,
|
||||
pkgListMode: pkgListMode.value,
|
||||
hiddenCategories: hiddenCategories.value,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -549,6 +572,9 @@ export const useClientStore = defineStore('client', () => {
|
||||
locale,
|
||||
timeout,
|
||||
scaleModel,
|
||||
currentTab,
|
||||
pkgListMode,
|
||||
hiddenCategories,
|
||||
_scaleValue,
|
||||
scaleValue,
|
||||
load,
|
||||
|
@ -56,6 +56,7 @@ export interface ProfileData {
|
||||
display: DisplayConfig;
|
||||
network: NetworkConfig;
|
||||
bepinex: BepInExConfig;
|
||||
wine: WineConfig;
|
||||
mu3_ini: Mu3IniConfig | undefined;
|
||||
keyboard: KeyboardConfig | undefined;
|
||||
patches: {
|
||||
@ -105,6 +106,11 @@ export interface BepInExConfig {
|
||||
console: boolean;
|
||||
}
|
||||
|
||||
export interface WineConfig {
|
||||
runtime: string;
|
||||
prefix: string;
|
||||
}
|
||||
|
||||
export interface Mu3IniConfig {
|
||||
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
|
||||
sample_rate: number;
|
||||
|
Reference in New Issue
Block a user