13 Commits

Author SHA1 Message Date
2dad0de4f1 fix: update rainycolor's domain 2025-04-30 06:59:38 +00:00
14a65eb5bb fix: keyboard unbinding and IR fixes 2025-04-29 19:59:21 +00:00
0add9200a6 feat: new grouping options 2025-04-28 22:00:33 +00:00
ee49da3665 fix: category sort 2025-04-28 16:47:45 +00:00
f478ad9216 feat: add package creator 2025-04-28 16:44:04 +00:00
c59dbcc35c feat: add polish localization 2025-04-27 20:37:46 +00:00
91d38b58c4 fix: localization fixes 2025-04-27 20:30:22 +00:00
240f60b283 fix: some more polish 2025-04-27 18:53:01 +00:00
6a32ad65a5 feat: onboarding i18n 2025-04-27 07:35:38 +00:00
6cc7a537b6 feat: remember current tab 2025-04-27 05:56:04 +00:00
bf4c06ee2d fix: begin fixing linux support 2025-04-23 17:17:59 +02:00
f26d83f291 fix: misc cleanup 2025-04-23 14:24:35 +00:00
8b2c1a04ee fix: remove chuniio from segatools-chunithm.ini 2025-04-23 13:37:19 +00:00
43 changed files with 1279 additions and 306 deletions

View File

@ -1,3 +1,33 @@
## 0.18.2
- Update 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 ## 0.15.0
- Added internationalization - Added internationalization

View File

@ -9,7 +9,7 @@ This is a program that seeks to streamline game data configuration, currently su
STARTLINER is four things: 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 configuration GUI for segatools,
- a glorified `start.bat` clicker, with automatic monitor setup and rollback, - 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). - [an abstraction allowing data configuration without touching the game directory](https://gitea.tendokyu.moe/akanyan/STARTLINER/wiki/Architecture-details).

View File

@ -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>

View File

@ -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

View File

@ -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>).

View File

@ -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.

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -1,11 +1,12 @@
use ini::Ini; use ini::Ini;
use log; use log;
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::path::PathBuf; use std::path::PathBuf;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tokio::fs; use tokio::fs;
use tauri::{AppHandle, Manager, State}; use tauri::{AppHandle, Manager, State};
use crate::model::config::GlobalConfigField; use crate::model::config::GlobalConfigField;
use crate::model::local::PackageManifest;
use crate::model::misc::Game; use crate::model::misc::Game;
use crate::model::patch::Patch; use crate::model::patch::Patch;
use crate::modules::package::prepare_dlls; 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") &p.data.sgt.target.parent().unwrap().join("amdaemon.exe")
).map_err(|e| e.to_string())?; ).map_err(|e| e.to_string())?;
#[cfg(target_os = "windows")]
let info = p.prepare_display() let info = p.prepare_display()
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let lineup_res = p.line_up(hash, refresh, patches_enabled).await 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()) .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] #[tauri::command]
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> { pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
log::debug!("invoke: reload_all_packages"); log::debug!("invoke: reload_all_packages");
@ -190,12 +251,16 @@ pub async fn get_all_packages(state: State<'_, Mutex<AppData>>) -> Result<HashMa
#[tauri::command] #[tauri::command]
pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Game) -> Result<Vec<PkgKey>, ()> { pub async fn get_game_packages(state: State<'_, Mutex<AppData>>, game: Option<Game>) -> Result<Vec<PkgKey>, ()> {
log::debug!("invoke: get_game_packages {game}"); log::debug!("invoke: get_game_packages {game:?}");
let appd = state.lock().await; let appd = state.lock().await;
if let Some(game) = game {
Ok(appd.pkgs.get_game_list(game)) Ok(appd.pkgs.get_game_list(game))
} else {
Ok(Vec::new())
}
} }
#[tauri::command] #[tauri::command]
@ -404,10 +469,14 @@ pub async fn load_segatools_ini(state: State<'_, Mutex<AppData>>, path: PathBuf)
} }
#[tauri::command] #[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); 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] #[tauri::command]
@ -437,12 +506,32 @@ pub async fn import_profile(path: PathBuf) -> Result<(), String> {
Profile::import(path).map_err(|e| e.to_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] #[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");
#[cfg(target_os = "windows")] #[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")] #[cfg(target_os = "linux")]
return Ok(vec!["wine".to_owned()]); return Ok(vec!["wine".to_owned()]);

View File

@ -193,6 +193,7 @@ pub async fn run(_args: Vec<String>) {
cmd::install_package, cmd::install_package,
cmd::delete_package, cmd::delete_package,
cmd::toggle_package, cmd::toggle_package,
cmd::create_package,
cmd::list_profiles, cmd::list_profiles,
cmd::init_profile, cmd::init_profile,
@ -207,6 +208,7 @@ pub async fn run(_args: Vec<String>) {
cmd::create_shortcut, cmd::create_shortcut,
cmd::export_profile, cmd::export_profile,
cmd::import_profile, cmd::import_profile,
cmd::clear_cache,
cmd::get_global_config, cmd::get_global_config,
cmd::set_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<()> { fn open_window(apph: AppHandle) -> anyhow::Result<()> {
let config = apph.config().clone(); let config = apph.config().clone();
tauri::WebviewWindowBuilder::new(&apph, "main", tauri::WebviewUrl::App("index.html".into())) 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) .inner_size(900f64, 600f64)
.min_inner_size(900f64, 600f64) .min_inner_size(900f64, 600f64)
.build()?; .build()?;

View File

@ -6,10 +6,11 @@ use super::misc::Game;
// manifest.json // manifest.json
#[derive(Deserialize)] #[derive(Serialize, Deserialize)]
pub struct PackageManifest { pub struct PackageManifest {
pub name: String, pub name: String,
pub version_number: String, pub version_number: String,
pub website_url: String,
pub description: String, pub description: String,
pub dependencies: BTreeSet<PkgKeyVersion>, pub dependencies: BTreeSet<PkgKeyVersion>,

View File

@ -108,7 +108,10 @@ impl Display {
Game::Ongeki => 60, Game::Ongeki => 60,
}, },
borderless_fullscreen: true, borderless_fullscreen: true,
#[cfg(target_os = "windows")]
dont_switch_primary: false, dont_switch_primary: false,
#[cfg(not(target_os = "windows"))]
dont_switch_primary: true,
monitor_index_override: None, monitor_index_override: None,
} }
} }
@ -141,7 +144,7 @@ pub struct BepInEx {
pub console: bool, pub console: bool,
} }
#[derive(Deserialize, Serialize, Clone)] #[derive(Deserialize, Serialize, Clone, Debug)]
#[serde(default)] #[serde(default)]
pub struct Wine { pub struct Wine {
pub runtime: PathBuf, pub runtime: PathBuf,

View File

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

View File

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

View File

@ -8,3 +8,6 @@ pub mod mempatcher;
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
pub mod display_windows; pub mod display_windows;
#[cfg(target_os = "linux")]
pub mod display_linux;

View File

@ -7,12 +7,20 @@ use crate::pkg_store::PackageStore;
use crate::util; use crate::util;
use crate::profiles::types::ProfilePaths; 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"); log::debug!("begin prepare packages");
let pfx_dir = p.data_dir(); let pfx_dir = p.data_dir();
let opt_dir = pfx_dir.join("option"); 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 redo_bepinex {
if pfx_dir.join("BepInEx").exists() { if pfx_dir.join("BepInEx").exists() {
util::remove_dir_all(pfx_dir.join("BepInEx")).await?; util::remove_dir_all(pfx_dir.join("BepInEx")).await?;

View File

@ -132,7 +132,7 @@ impl PackageStore {
prelude::*, 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 let reader = response
.bytes_stream() .bytes_stream()

View File

@ -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::{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 tauri::Emitter;
use std::process::Stdio; use std::process::Stdio;
use crate::model::profile::BepInEx; use crate::model::profile::BepInEx;
@ -10,9 +10,23 @@ use std::fs::File;
use tokio::process::Command; use tokio::process::Command;
use tokio::task::JoinSet; use tokio::task::JoinSet;
#[cfg(target_os = "windows")]
use crate::modules::display_windows::DisplayInfo;
pub mod template; pub mod template;
pub mod types; 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 { impl Profile {
pub fn new(mut meta: ProfileMeta) -> Result<Self> { pub fn new(mut meta: ProfileMeta) -> Result<Self> {
meta.name = fixed_name(&meta, true); meta.name = fixed_name(&meta, true);
@ -176,6 +190,7 @@ impl Profile {
self.data.patches = source.patches; self.data.patches = source.patches;
} }
} }
#[cfg(target_os = "windows")]
pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> { pub fn prepare_display(&self) -> Result<Option<DisplayInfo>> {
let info = match &self.data.display { let info = match &self.data.display {
None => None, None => None,
@ -252,8 +267,8 @@ impl Profile {
} }
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
game_builder = Command::new(&self.wine.runtime); game_builder = Command::new(&self.data.wine.runtime);
amd_builder = Command::new(&self.wine.runtime); amd_builder = Command::new(&self.data.wine.runtime);
game_builder.arg(sgt_dir.join(self.meta.game.inject_exe())); game_builder.arg(sgt_dir.join(self.meta.game.inject_exe()));
amd_builder.arg("cmd.exe"); amd_builder.arg("cmd.exe");
@ -349,8 +364,8 @@ impl Profile {
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
{ {
amd_builder.env("WINEPREFIX", &self.wine.prefix); amd_builder.env("WINEPREFIX", &self.data.wine.prefix);
game_builder.env("WINEPREFIX", &self.wine.prefix); game_builder.env("WINEPREFIX", &self.data.wine.prefix);
} }
let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?; let amd_log = File::create(self.data_dir().join("amdaemon.exe.log"))?;

View File

@ -152,6 +152,7 @@ impl PathStr for PathBuf {
} }
} }
#[allow(dead_code)]
pub fn bool_to_01(val: bool) -> &'static str { pub fn bool_to_01(val: bool) -> &'static str {
return if val { "1" } else { "0" } return if val { "1" } else { "0" }
} }

View File

@ -65,18 +65,3 @@ controllerLedOutputOpeNITHM=0
; ;
; Board 2 is the slider and has 31 LEDs: ; Board 2 is the slider and has 31 LEDs:
; [0]-[31]: slider LEDs right to left BRG, alternating between keys and dividers ; [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=

View File

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

View File

@ -20,15 +20,16 @@ import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue'; import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue'; import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue'; import StartButton from './StartButton.vue';
import { invoke } from '../invoke';
import { import {
useClientStore, useClientStore,
useGeneralStore, useGeneralStore,
usePkgStore, usePkgStore,
usePrfStore, usePrfStore,
} from '../stores'; } from '../stores';
import { Dirs } from '../types';
import { messageSplit, shouldPreferDark } from '../util'; import { messageSplit, shouldPreferDark } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark()); document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
@ -37,10 +38,10 @@ const prf = usePrfStore();
const general = useGeneralStore(); const general = useGeneralStore();
const client = useClientStore(); const client = useClientStore();
client.load();
pkg.setupListeners(); pkg.setupListeners();
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref(''); const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null); const isProfileDisabled = computed(() => prf.current === null);
@ -56,17 +57,11 @@ listen<undefined>('update-end', (_) => {
}); });
onMounted(async () => { onMounted(async () => {
invoke('list_directories').then((d) => {
general.dirs = d as Dirs;
client.load();
});
const fetch_promise = pkg.fetch(true); const fetch_promise = pkg.fetch(true);
await Promise.all([prf.reloadList(), prf.reload()]); await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) { if (prf.current !== null) {
currentTab.value = 'loc';
await pkg.reloadAll(); await pkg.reloadAll();
} }
@ -211,10 +206,11 @@ listen<DownloadingStatus>('download-progress', (event) => {
<Tabs <Tabs
lazy lazy
:value="currentTab" :value="client.currentTab"
v-on:update:value=" v-on:update:value="
(value) => { (value) => {
currentTab = value as any; client.currentTab = value as string;
client.save();
} }
" "
class="h-screen" class="h-screen"
@ -222,6 +218,13 @@ listen<DownloadingStatus>('download-progress', (event) => {
<div class="fixed w-full flex z-100"> <div class="fixed w-full flex z-100">
<TabList class="grow" :show-navigators="false"> <TabList class="grow" :show-navigators="false">
<Tab value="users"><div class="pi pi-home"></div></Tab> <Tab value="users"><div class="pi pi-home"></div></Tab>
<Tab
:disabled="
isProfileDisabled || pkg.networkStatus !== 'online'
"
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" value="loc" <Tab :disabled="isProfileDisabled" value="loc"
><div class="pi pi-box"></div ><div class="pi pi-box"></div
></Tab> ></Tab>
@ -230,12 +233,7 @@ listen<DownloadingStatus>('download-progress', (event) => {
value="patches" value="patches"
><div class="pi pi-ticket"></div ><div class="pi pi-ticket"></div
></Tab> ></Tab>
<Tab
v-if="pkg.networkStatus === 'online'"
:disabled="isProfileDisabled"
value="rmt"
><div class="pi pi-download"></div
></Tab>
<Tab :disabled="isProfileDisabled" value="cfg" <Tab :disabled="isProfileDisabled" value="cfg"
><div class="pi pi-cog"></div ><div class="pi pi-cog"></div
></Tab> ></Tab>
@ -248,17 +246,21 @@ listen<DownloadingStatus>('download-progress', (event) => {
<div class="flex gap-4"> <div class="flex gap-4">
<div <div
class="flex" class="flex"
v-if="['loc', 'rmt', 'cfg'].includes(currentTab)" v-if="
['loc', 'rmt', 'cfg'].includes(
client.currentTab
)
"
> >
<InputIcon class="self-center mr-2"> <InputIcon class="self-center mr-2">
<i class="pi pi-search" /> <i class="pi pi-search" />
</InputIcon> </InputIcon>
<InputText <InputText
v-if="currentTab === 'cfg'" v-if="client.currentTab === 'cfg'"
style="min-width: 0; width: 25dvw" style="min-width: 0; width: 25dvw"
class="self-center" class="self-center"
size="small" size="small"
placeholder="Search" :placeholder="t('search')"
v-model="general.cfgSearchTerm" v-model="general.cfgSearchTerm"
/> />
<InputText <InputText
@ -266,7 +268,7 @@ listen<DownloadingStatus>('download-progress', (event) => {
style="min-width: 0; width: 25dvw" style="min-width: 0; width: 25dvw"
class="self-center" class="self-center"
size="small" size="small"
placeholder="Search" :placeholder="t('search')"
v-model="pkgSearchTerm" v-model="pkgSearchTerm"
/> />
</div> </div>
@ -304,19 +306,19 @@ listen<DownloadingStatus>('download-progress', (event) => {
</TabList> </TabList>
</div> </div>
<TabPanels class="w-full grow mt-[3rem]"> <TabPanels class="w-full grow mt-[3rem]">
<TabPanel value="loc"> <TabPanel value="loc" v-if="!isProfileDisabled">
<ModList :search="pkgSearchTerm" /> <ModList :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel value="rmt"> <TabPanel value="rmt" v-if="!isProfileDisabled">
<ModStore :search="pkgSearchTerm" /> <ModStore :search="pkgSearchTerm" />
</TabPanel> </TabPanel>
<TabPanel value="cfg"> <TabPanel value="cfg" v-if="!isProfileDisabled">
<OptionList /> <OptionList />
</TabPanel> </TabPanel>
<TabPanel value="users"> <TabPanel value="users">
<ProfileList /> <ProfileList />
</TabPanel> </TabPanel>
<TabPanel value="patches"> <TabPanel value="patches" v-if="!isProfileDisabled">
<PatchList <PatchList
v-if=" v-if="
pkg.hasLocal('mempatcher-mempatcher') && pkg.hasLocal('mempatcher-mempatcher') &&
@ -347,7 +349,12 @@ listen<DownloadingStatus>('download-progress', (event) => {
<InfoPage /> <InfoPage />
</TabPanel> </TabPanel>
</TabPanels> </TabPanels>
<div v-if="currentTab === 'users' || currentTab === 'info'"> <div
v-if="
client.currentTab === 'users' ||
client.currentTab === 'info'
"
>
<img <img
v-if="prf.current?.meta.game === 'ongeki'" v-if="prf.current?.meta.game === 'ongeki'"
src="/sticker-ongeki.svg" src="/sticker-ongeki.svg"

View File

@ -4,6 +4,9 @@ import InputText from 'primevue/inputtext';
import { fromKeycode, toKeycode } from '../keyboard'; import { fromKeycode, toKeycode } from '../keyboard';
import { usePrfStore } from '../stores'; import { usePrfStore } from '../stores';
import { OngekiButtons } from '../types'; import { OngekiButtons } from '../types';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
@ -61,6 +64,10 @@ const handleKey = (
} }
} }
if (event.code === 'Escape') {
keycode = 0;
}
if (index !== undefined) { if (index !== undefined) {
data[button][index] = keycode; data[button][index] = keycode;
} else { } else {
@ -160,13 +167,24 @@ const fontSize = computed(() => {
<InputText <InputText
:style="{ :style="{
width: small ? '2.8rem' : '5rem', width: small ? '2.8rem' : '5rem',
height: small ? '2.8rem' : tall ? '10rem' : '5rem', height:
small && tall
? '5rem'
: small
? '2.8rem'
: tall
? '10rem'
: '5rem',
fontSize, fontSize,
backgroundColor: color, backgroundColor: color,
}" }"
unstyled unstyled
class="text-center buttoninputtext" 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="() => {}" @contextmenu.prevent="() => {}"
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)" @keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
@mousedown=" @mousedown="

View File

@ -1,12 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, computed, ref } from 'vue'; import { Ref, computed, ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Fieldset from 'primevue/fieldset'; 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 ModListEntry from './ModListEntry.vue';
import ModTitlecard from './ModTitlecard.vue'; import ModTitlecard from './ModTitlecard.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores'; import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Package } from '../types'; import { Feature, Game, Package } from '../types';
import { pkgKey } from '../util'; import { pkgKey } from '../util';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -17,48 +23,290 @@ const props = defineProps({
}); });
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const client = useClientStore();
const prf = usePrfStore(); const prf = usePrfStore();
const empty = ref(false);
const gameSublist: Ref<string[]> = ref([]); const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', { const loadPackages = () => {
game: prf.current?.meta.game, invoke('get_game_packages', {
}).then((list) => { game: prf.current?.meta.game ?? null,
}).then((list) => {
gameSublist.value = list as string[]; 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 local = computed(() => {
const res = Object.assign( return pkgs.allLocal
{},
Object.groupBy(
pkgs.allLocal
.filter((p) => gameSublist.value.includes(pkgKey(p))) .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( .filter(
(p) => (p) =>
props.search === undefined || props.search === undefined ||
p.name p.name.toLowerCase().includes(props.search.toLowerCase()) ||
.toLowerCase() p.namespace.toLowerCase().includes(props.search.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;
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; return res;
}); });
const missing = computed(() => { const missing = computed(() => {
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? []; 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> </script>
<template> <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"> <div class="flex items-center" v-for="p in missing">
<ModTitlecard <ModTitlecard
show-namespace show-namespace
@ -81,8 +329,28 @@ const missing = computed(() => {
/> />
</div> </div>
</Fieldset> </Fieldset>
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()"> <Fieldset :legend="t('pkglist.local')">
<ModListEntry v-for="p in namespace" :pkg="p" /> <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> </Fieldset>
<div v-if="empty === true" class="text-3xl fadein"></div>
</template> </template>
<style lang="css" scoped>
.creation-dialog h2 {
margin-top: 0.6em;
margin-bottom: 0.4em;
font-size: 110%;
}
</style>

View File

@ -7,7 +7,7 @@ import LinkButton from './LinkButton.vue';
import ModTitlecard from './ModTitlecard.vue'; import ModTitlecard from './ModTitlecard.vue';
import UpdateButton from './UpdateButton.vue'; import UpdateButton from './UpdateButton.vue';
import { invoke } from '../invoke'; import { invoke } from '../invoke';
import { usePkgStore, usePrfStore } from '../stores'; import { useClientStore, usePkgStore, usePrfStore } from '../stores';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature } from '../util'; import { hasFeature } from '../util';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -16,6 +16,7 @@ const { t } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const client = useClientStore();
const props = defineProps({ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
@ -39,7 +40,13 @@ if (unsupported.value === true && model.value === true) {
<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
:show-namespace="client.pkgListMode !== 'namespace'"
:pkg="pkg"
/>
<UpdateButton :pkg="pkg" /> <UpdateButton :pkg="pkg" />
<span v-tooltip="unsupported && t('store.incompatible')"> <span v-tooltip="unsupported && t('store.incompatible')">
<ToggleSwitch <ToggleSwitch

View File

@ -23,7 +23,7 @@ const props = defineProps({
const gameSublist: Ref<string[]> = ref([]); const gameSublist: Ref<string[]> = ref([]);
invoke('get_game_packages', { invoke('get_game_packages', {
game: prf.current?.meta.game, game: prf.current?.meta.game ?? null,
}).then((list) => { }).then((list) => {
gameSublist.value = list as string[]; gameSublist.value = list as string[];
}); });
@ -46,10 +46,10 @@ const list = () => {
}; };
const shouldShowRecommended = computed(() => { const shouldShowRecommended = computed(() => {
if (prf.current!.meta.game === 'ongeki') { if (prf.current?.meta.game === 'ongeki') {
return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook'); return !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-mu3hook');
} }
if (prf.current!.meta.game === 'chunithm') { if (prf.current?.meta.game === 'chunithm') {
return ( return (
!pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') || !pkgs.allLocal.some((p) => pkgKey(p) === 'segatools-chusanhook') ||
!pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher') !pkgs.allLocal.some((p) => pkgKey(p) === 'mempatcher-mempatcher')
@ -58,21 +58,21 @@ const shouldShowRecommended = computed(() => {
return false; return false;
}); });
const getRecommendedTooltip = () => { const recommendedTooltip = computed(() => {
if (prf.current!.meta.game === 'ongeki') { if (prf.current?.meta.game === 'ongeki') {
return 'segatools-mu3hook'; return 'segatools-mu3hook';
} }
if (prf.current!.meta.game === 'chunithm') { if (prf.current?.meta.game === 'chunithm') {
return 'segatools-chusanhook + mempatcher'; return 'segatools-chusanhook + mempatcher';
} }
return ''; return '';
}; });
const installRecommended = () => { const installRecommended = () => {
if (prf.current!.meta.game === 'ongeki') { if (prf.current?.meta.game === 'ongeki') {
pkgs.installFromKey('segatools-mu3hook'); pkgs.installFromKey('segatools-mu3hook');
} }
if (prf.current!.meta.game === 'chunithm') { if (prf.current?.meta.game === 'chunithm') {
pkgs.installFromKey('segatools-chusanhook'); pkgs.installFromKey('segatools-chusanhook');
pkgs.installFromKey('mempatcher-mempatcher'); pkgs.installFromKey('mempatcher-mempatcher');
} }
@ -101,7 +101,7 @@ const installRecommended = () => {
<MultiSelect <MultiSelect
size="small" size="small"
:showToggleAll="false" :showToggleAll="false"
placeholder="Include categories" :placeholder="t('store.includeCategories')"
v-model="pkgs.includeCategories" v-model="pkgs.includeCategories"
:options="[...pkgs.availableCategories]" :options="[...pkgs.availableCategories]"
class="w-full" class="w-full"
@ -109,7 +109,7 @@ const installRecommended = () => {
<MultiSelect <MultiSelect
size="small" size="small"
:showToggleAll="false" :showToggleAll="false"
placeholder="Exclude categories" :placeholder="t('store.excludeCategories')"
v-model="pkgs.excludeCategories" v-model="pkgs.excludeCategories"
:options="[...pkgs.availableCategories]" :options="[...pkgs.availableCategories]"
class="w-full" class="w-full"
@ -120,7 +120,7 @@ const installRecommended = () => {
<Button <Button
v-if="shouldShowRecommended" v-if="shouldShowRecommended"
:label="t('store.installRecommended')" :label="t('store.installRecommended')"
v-tooltip="getRecommendedTooltip" v-tooltip="recommendedTooltip"
icon="pi pi-plus" icon="pi pi-plus"
class="mb-3" class="mb-3"
@click="installRecommended" @click="installRecommended"

View File

@ -1,9 +1,14 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { ref } from 'vue';
import Chip from 'primevue/chip'; import Chip from 'primevue/chip';
import { convertFileSrc } from '@tauri-apps/api/core'; import { convertFileSrc } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { invoke } from '../invoke';
import { Feature, Package } from '../types'; import { Feature, Package } from '../types';
import { hasFeature, needsUpdate } from '../util'; import { hasFeature, needsUpdate } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({ const props = defineProps({
pkg: Object as () => Package, pkg: Object as () => Package,
@ -14,23 +19,34 @@ const props = defineProps({
showIcon: Boolean, showIcon: Boolean,
}); });
const iconSrc = computed(() => { const icon = ref('/no-icon.png');
const icon = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
if (icon === undefined) { const reloadIcons = async () => {
return ''; const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
} else if (icon.startsWith('https://')) {
return icon; if (src === undefined) {
icon.value = '/no-icon.png';
} else if (src.startsWith('https://')) {
icon.value = src;
} else { } 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> </script>
<template> <template>
<img <img
v-if="showIcon" v-if="showIcon"
:src="iconSrc" :src="icon"
class="self-center rounded-sm" class="self-center rounded-sm"
width="32px" width="32px"
height="32px" height="32px"
@ -89,7 +105,12 @@ const iconSrc = computed(() => {
v-if="showNamespace && pkg?.namespace" v-if="showNamespace && pkg?.namespace"
class="text-sm opacity-75" class="text-sm opacity-75"
> >
by&nbsp;{{ pkg.namespace }} &nbsp;{{
t('by', { namespace: pkg.namespace }).replaceAll(
' ',
'&nbsp;'
)
}}
</span> </span>
<span class="m-2"> <span class="m-2">
<span <span

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ComputedRef, computed, onMounted, ref } from 'vue'; import { ComputedRef, computed, ref } from 'vue';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Carousel from 'primevue/carousel'; import Carousel from 'primevue/carousel';
import Dialog from 'primevue/dialog'; import Dialog from 'primevue/dialog';
@ -7,6 +7,9 @@ import { fromKeycode } from '../keyboard';
import { useClientStore, usePrfStore } from '../stores'; import { useClientStore, usePrfStore } from '../stores';
import { prettyPrint } from '../util'; import { prettyPrint } from '../util';
import { VueMarkdownIt } from '@f3ve/vue-markdown-it'; import { VueMarkdownIt } from '@f3ve/vue-markdown-it';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
const client = useClientStore(); const client = useClientStore();
@ -24,38 +27,73 @@ interface Datum {
const game = computed(() => prf.current?.meta.game); 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) { if (prf.current!.data.keyboard?.data.enabled) {
const testKey = prf.current!.data.keyboard?.data.test; const testKey = prf.current!.data.keyboard?.data.test;
const readable = fromKeycode(testKey); const readable = fromKeycode(testKey);
if (readable !== null) { if (readable !== null) {
return s.replace( return s.replace(
'%TESTMENU%', '%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 { 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`, 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 data: ComputedRef<Datum[]> = computed(() => {
const res = []; 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) { switch (prf.current?.meta.game) {
case 'ongeki': case 'ongeki':
res.push(systemProcessing); res.push(systemProcessing);
@ -75,40 +113,18 @@ const data: ComputedRef<Datum[]> = computed(() => {
return res; return res;
}); });
onMounted(async () => { const context = ref({
[standardOngeki, systemProcessing, lever, server, finaleOngeki] = index: 0,
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 counter = ref(0);
const exitLabel = computed(() => { const exitLabel = computed(() => {
return props.firstTime === true && counter.value < data.value.length - 1 return props.firstTime === true &&
? 'Skip' context.value.index < data.value.length - 1
: 'Close'; ? t('skip')
: t('close');
}); });
const page = ref(0);
</script> </script>
<template> <template>
@ -122,13 +138,15 @@ const exitLabel = computed(() => {
: `${game ? prettyPrint(game) : '<game>'} help` : `${game ? prettyPrint(game) : '<game>'} help`
" "
:style="{ width: '760px', scale: client.scaleValue }" :style="{ width: '760px', scale: client.scaleValue }"
v-on:show="() => ((context.index = 0), (page = 0))"
> >
<Carousel <Carousel
:value="data" :value="data"
:num-visible="1" :num-visible="1"
:num-scroll="1" :num-scroll="1"
:page="counter" :context="context"
v-on:update:page="(p) => (counter = p)" :page="page"
v-on:update:page="(p) => ((context.index = p), (page = p))"
> >
<template #item="slotProps"> <template #item="slotProps">
<div class="md-container markdown"> <div class="md-container markdown">
@ -150,10 +168,10 @@ const exitLabel = computed(() => {
</Carousel> </Carousel>
<div style="width: 100%; text-align: center"> <div style="width: 100%; text-align: center">
<Button <Button
v-if="counter < data.length - 1" v-if="context.index < data.length - 1"
class="m-auto mr-4" class="m-auto mr-4"
label="Next" :label="t('next')"
@click="() => (counter += 1)" @click="() => (page += 1)"
/> />
<Button <Button
class="m-auto" class="m-auto"

View File

@ -61,7 +61,7 @@ prf.reload();
<MiscOptions /> <MiscOptions />
<OptionCategory <OptionCategory
title="Extensions" title="Extensions"
v-if="prf.current!.meta.game === 'chunithm'" v-if="prf.current?.meta.game === 'chunithm'"
> >
<OptionRow :title="t('cfg.extensions.saekawa')"> <OptionRow :title="t('cfg.extensions.saekawa')">
<FileEditor <FileEditor
@ -72,7 +72,7 @@ prf.reload();
></OptionCategory> ></OptionCategory>
<OptionCategory <OptionCategory
:title="t('cfg.extensions.title')" :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')"> <OptionRow :title="t('cfg.extensions.inohara')">
<FileEditor <FileEditor

View File

@ -8,7 +8,7 @@ import { usePrfStore } from '../stores';
import { Patch } from '../types'; import { Patch } from '../types';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const { t } = useI18n(); const { t, te } = useI18n();
const prf = usePrfStore(); const prf = usePrfStore();
@ -51,14 +51,14 @@ const hexModel = computed({
// Doesn't need to be reactive // Doesn't need to be reactive
const nameKey = `patch.${props.patch?.id}`; const nameKey = `patch.${props.patch?.id}`;
let name = t(nameKey); const name = te(nameKey) ? t(nameKey) : props.patch?.name;
if (name === nameKey) {
name = props.patch?.name ?? 'No name'; const tooltipKey = `patch.${props.patch?.id}-tooltip`;
} const tooltip = te(tooltipKey) ? t(tooltipKey) : props.patch?.tooltip;
</script> </script>
<template> <template>
<OptionRow :title="name" :tooltip="patch?.tooltip" :greytext="patch?.id"> <OptionRow :title="name" :tooltip="tooltip" :greytext="patch?.id">
<ToggleSwitch <ToggleSwitch
v-if="patch?.type === undefined" v-if="patch?.type === undefined"
:model-value="prf.current!.data.patches?.[patch!.id!] !== undefined" :model-value="prf.current!.data.patches?.[patch!.id!] !== undefined"

View File

@ -1,5 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from 'vue'; import { Ref, ref } from 'vue';
// import Select from 'primevue/select';
import * as path from '@tauri-apps/api/path'; import * as path from '@tauri-apps/api/path';
import OptionCategory from './OptionCategory.vue'; import OptionCategory from './OptionCategory.vue';
import PatchEntry from './PatchEntry.vue'; import PatchEntry from './PatchEntry.vue';
@ -32,8 +33,6 @@ invoke('list_patches', { target: prf.current!.data.sgt.target }).then(
target: amd, target: amd,
})) as Patch[]; })) as Patch[];
})(); })();
const errorMessage = t('patch.noneFound');
</script> </script>
<template> <template>
@ -49,7 +48,7 @@ const errorMessage = t('patch.noneFound');
/> />
<div v-if="gamePatches === null">{{ t('patch.loading') }}</div> <div v-if="gamePatches === null">{{ t('patch.loading') }}</div>
<div v-if="gamePatches !== null && gamePatches.length === 0"> <div v-if="gamePatches !== null && gamePatches.length === 0">
{{ errorMessage }} {{ t('patch.noneFound') }}
</div> </div>
</OptionCategory> </OptionCategory>
<OptionCategory title="amdaemon.exe" always-found> <OptionCategory title="amdaemon.exe" always-found>
@ -58,9 +57,22 @@ const errorMessage = t('patch.noneFound');
v-for="p in amdPatches" v-for="p in amdPatches"
:patch="p" :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"> <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> </div>
</OptionCategory> </OptionCategory>
</template> </template>

View File

@ -18,10 +18,17 @@ const prf = usePrfStore();
const client = useClientStore(); const client = useClientStore();
const general = useGeneralStore(); const general = useGeneralStore();
const hasChunithm = ref(false);
const exportVisible = ref(false); const exportVisible = ref(false);
const exportKeychip = ref(false); const exportKeychip = ref(false);
const files = new Set<string>(); const files = new Set<string>();
(async () => {
hasChunithm.value = (
(await invoke('list_platform_capabilities')) as string[]
).includes('chunithm');
})();
const exportTemplate = async () => { const exportTemplate = async () => {
const fl = [...files.values()]; const fl = [...files.values()];
exportVisible.value = false; exportVisible.value = false;
@ -30,7 +37,7 @@ const exportTemplate = async () => {
files: fl, files: fl,
}); });
await invoke('open_file', { 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 modal
:visible="exportVisible" :visible="exportVisible"
:closable="false /*this shit doesn't work */" :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 }" :style="{ width: '300px', scale: client.scaleValue }"
> >
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<div class="flex flex-row"> <div class="flex flex-row">
<div class="grow">Export keychip</div> <div class="grow">{{ t('profile.export') }} keychip</div>
<ToggleSwitch v-model="exportKeychip" /> <ToggleSwitch v-model="exportKeychip" />
</div> </div>
<div class="flex flex-row" v-for="f in fileListCurrent"> <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 <ToggleSwitch
:model-value="true" :model-value="true"
@update:model-value=" @update:model-value="
@ -134,6 +141,7 @@ const importPick = async () => {
@click="() => prf.create('ongeki')" @click="() => prf.create('ongeki')"
/> />
<Button <Button
v-if="hasChunithm"
:label="t('profile.create', { game: t('game.chunithm') })" :label="t('profile.create', { game: t('game.chunithm') })"
icon="pi pi-file-plus" icon="pi pi-file-plus"
class="chunithm-button profile-button" class="chunithm-button profile-button"
@ -142,14 +150,14 @@ const importPick = async () => {
</div> </div>
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4"> <div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
<Button <Button
label="Import template" :label="t('profile.importTemplate')"
icon="pi pi-file-import" icon="pi pi-file-import"
class="import-button profile-button" class="import-button profile-button"
@click="() => importPick()" @click="() => importPick()"
/> />
<Button <Button
:disabled="prf.current === null" :disabled="prf.current === null"
label="Export template" :label="t('profile.exportTemplate')"
icon="pi pi-file-export" icon="pi pi-file-export"
class="profile-button" class="profile-button"
@click="() => openExportDialog()" @click="() => openExportDialog()"
@ -171,6 +179,7 @@ const importPick = async () => {
:options="[ :options="[
{ title: 'English', value: 'en' }, { title: 'English', value: 'en' },
// { title: '日本語', value: 'ja' }, // { title: '日本語', value: 'ja' },
{ title: 'Polski', value: 'pl' },
]" ]"
size="small" size="small"
option-label="title" option-label="title"

View File

@ -73,10 +73,12 @@ const promptDeleteProfile = async () => {
const dataExists = ref(false); const dataExists = ref(false);
path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then( general.dataDir.then((dataDir) =>
async (p) => { path
.join(dataDir, `profile-${props.p!.game}-${props.p!.name}`)
.then(async (p) => {
dataExists.value = await invoke('file_exists', { path: p }); dataExists.value = await invoke('file_exists', { path: p });
} })
); );
</script> </script>
@ -145,8 +147,12 @@ path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
class="self-center" class="self-center"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
@click=" @click="
async () =>
path path
.join(general.configDir, `profile-${p!.game}-${p!.name}`) .join(
await general.configDir,
`profile-${p!.game}-${p!.name}`
)
.then(async (path) => { .then(async (path) => {
await invoke('open_file', { path }); await invoke('open_file', { path });
}) })
@ -162,8 +168,12 @@ path.join(general.dataDir, `profile-${props.p!.game}-${props.p!.name}`).then(
class="self-center" class="self-center"
style="width: 2rem; height: 2rem" style="width: 2rem; height: 2rem"
@click=" @click="
async () =>
path path
.join(general.dataDir, `profile-${p!.game}-${p!.name}`) .join(
await general.dataDir,
`profile-${p!.game}-${p!.name}`
)
.then(async (path) => { .then(async (path) => {
await invoke('open_file', { path }); await invoke('open_file', { path });
}) })

View File

@ -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 menuItems = computed(() => {
const base = [ let base = [
{ {
label: t('start.button.unchecked'), label: t('start.button.unchecked'),
icon: 'pi pi-exclamation-circle', icon: 'pi pi-exclamation-circle',
command: async () => await startline(true, false), command: async () => await startline(true, false),
}, },
{ ];
label: t('start.button.shortcut'), let baseTail = [
icon: 'pi pi-link',
command: createShortcut,
},
{ {
label: t('start.button.help'), label: t('start.button.help'),
icon: 'pi pi-question-circle', icon: 'pi pi-question-circle',
@ -123,8 +128,18 @@ const menuItems = computed(() => {
if (prf.current === null) { if (prf.current === null) {
return []; return [];
} }
if (hasShortcut.value === true) {
base = [
...base,
{
label: t('start.button.shortcut'),
icon: 'pi pi-link',
command: createShortcut,
},
];
}
if (prf.current.meta.game === 'chunithm') { if (prf.current.meta.game === 'chunithm') {
return base; return [...base, ...baseTail];
} }
if (prf.current.meta.game === 'ongeki') { if (prf.current.meta.game === 'ongeki') {
return [ return [
@ -137,8 +152,9 @@ const menuItems = computed(() => {
{ {
label: t('start.button.cache'), label: t('start.button.cache'),
icon: 'pi pi-trash', icon: 'pi pi-trash',
command: async () => {}, command: async () => await invoke('clear_cache'),
}, },
...baseTail,
]; ];
} }
}); });

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Ref, computed, ref } from 'vue'; import { Ref, computed, onMounted, ref } from 'vue';
import InputNumber from 'primevue/inputnumber'; import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select'; import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton'; import SelectButton from 'primevue/selectbutton';
@ -65,16 +65,20 @@ const loadDisplays = () => {
.catch(() => {}); .catch(() => {});
}; };
loadDisplays(); onMounted(() => {
loadDisplays();
});
const game = prf.current!.meta.game; const game = computed(() => prf.current!.meta.game);
const isVertical = game === 'ongeki'; const isVertical = computed(() => prf.current!.meta.game === 'ongeki');
const adjustableRez = game === 'ongeki'; const adjustableRez = computed(() => prf.current!.meta.game === 'ongeki');
const canSkipPrimarySwitch = game === 'ongeki'; const canSkipPrimarySwitch = computed(
() => prf.current!.meta.game === 'ongeki'
);
</script> </script>
<template> <template>
<OptionCategory title="Display"> <OptionCategory :title="t('cfg.display.title')">
<OptionRow <OptionRow
v-if="capabilities.includes('display')" v-if="capabilities.includes('display')"
:title="t('cfg.display.target')" :title="t('cfg.display.target')"
@ -90,7 +94,7 @@ const canSkipPrimarySwitch = game === 'ongeki';
</OptionRow> </OptionRow>
<OptionRow <OptionRow
class="number-input" class="number-input"
title="Game resolution" :title="t('cfg.display.resolution')"
v-if="adjustableRez" v-if="adjustableRez"
> >
<InputNumber <InputNumber
@ -115,9 +119,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
<SelectButton <SelectButton
v-model="prf.current!.data.display.mode" v-model="prf.current!.data.display.mode"
:options="[ :options="[
{ title: 'Window', value: 'Window' }, { title: t('cfg.display.window'), value: 'Window' },
{ title: 'Borderless window', value: 'Borderless' }, { title: t('cfg.display.borderless'), value: 'Borderless' },
{ title: 'Fullscreen', value: 'Fullscreen' }, { title: t('cfg.display.fullscreen'), value: 'Fullscreen' },
]" ]"
:allow-empty="false" :allow-empty="false"
option-label="title" option-label="title"
@ -197,16 +201,19 @@ const canSkipPrimarySwitch = game === 'ongeki';
<OptionRow <OptionRow
:title="t('cfg.display.dontSwitchPrimary')" :title="t('cfg.display.dontSwitchPrimary')"
v-if=" v-if="
capabilities.includes('display') && !capabilities.includes('display') ||
prf.current?.data.display.target !== 'default' && (prf.current?.data.display.target !== 'default' &&
(prf.current!.data.display.dont_switch_primary || (prf.current!.data.display.dont_switch_primary ||
displayList.length > 2) && displayList.length > 2) &&
canSkipPrimarySwitch canSkipPrimarySwitch)
" "
:dangerous-tooltip="t('cfg.display.dontSwitchPrimaryTooltip')" :dangerous-tooltip="t('cfg.display.dontSwitchPrimaryTooltip')"
> >
<ToggleSwitch <ToggleSwitch
:disabled="extraDisplayOptionsDisabled" :disabled="
extraDisplayOptionsDisabled &&
capabilities.includes('display')
"
v-model="prf.current!.data.display.dont_switch_primary" v-model="prf.current!.data.display.dont_switch_primary"
/> />
</OptionRow> </OptionRow>
@ -214,9 +221,9 @@ const canSkipPrimarySwitch = game === 'ongeki';
:title="t('cfg.display.index')" :title="t('cfg.display.index')"
class="number-input" class="number-input"
v-if=" v-if="
capabilities.includes('display') && !capabilities.includes('display') ||
prf.current?.data.display.target !== 'default' && (prf.current?.data.display.target !== 'default' &&
prf.current!.data.display.dont_switch_primary prf.current!.data.display.dont_switch_primary)
" "
> >
<InputNumber <InputNumber
@ -225,8 +232,12 @@ const canSkipPrimarySwitch = game === 'ongeki';
:min="game === 'chunithm' ? 0 : 1" :min="game === 'chunithm' ? 0 : 1"
:max="32" :max="32"
:use-grouping="false" :use-grouping="false"
placeholder="1"
v-model="prf.current!.data.display.monitor_index_override" v-model="prf.current!.data.display.monitor_index_override"
:disabled="extraDisplayOptionsDisabled" :disabled="
extraDisplayOptionsDisabled &&
capabilities.includes('display')
"
:allow-empty="true" :allow-empty="true"
/> />
</OptionRow> </OptionRow>

View File

@ -95,7 +95,7 @@ const prf = usePrfStore();
</div> </div>
</div> </div>
<div v-if="prf.current?.meta.game === 'chunithm'"> <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 <div
class="flex flex-row flex-nowrap gap-2 self-center w-full" class="flex flex-row flex-nowrap gap-2 self-center w-full"
> >
@ -108,6 +108,7 @@ const prf = usePrfStore();
button="ir" button="ir"
:index="idx - 1" :index="idx - 1"
:tooltip="`ir${idx}`" :tooltip="`ir${idx}`"
tall
small small
color="rgba(0, 255, 0, 0.2)" color="rgba(0, 255, 0, 0.2)"
/> />

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { Ref, computed, ref } from 'vue';
import Select from 'primevue/select'; import Select from 'primevue/select';
import { useConfirm } from 'primevue/useconfirm'; import { useConfirm } from 'primevue/useconfirm';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
@ -19,6 +19,14 @@ const prf = usePrfStore();
const pkgs = usePkgStore(); const pkgs = usePkgStore();
const confirmDialog = useConfirm(); 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(() => { const names = computed(() => {
switch (prf.current?.meta.game) { switch (prf.current?.meta.game) {
case 'ongeki': { case 'ongeki': {
@ -35,8 +43,6 @@ const names = computed(() => {
io: 'chuniio', io: 'chuniio',
}; };
} }
case undefined:
throw new Error('Option tab without a profile');
} }
}); });
@ -59,14 +65,14 @@ const checkSegatoolsIni = async (target: string) => {
<template> <template>
<OptionCategory :title="t('cfg.segatools.general')"> <OptionCategory :title="t('cfg.segatools.general')">
<OptionRow <OptionRow
:title="names.exe" :title="names?.exe"
:tooltip="t('cfg.segatools.targetTooltip')" :tooltip="t('cfg.segatools.targetTooltip')"
> >
<FilePicker <FilePicker
:directory="false" :directory="false"
:promptname="names.exe" :promptname="names?.exe"
extension="exe" extension="exe"
:value="prf.current!.data.sgt.target" :value="prf.current?.data.sgt.target"
:callback=" :callback="
(value: string) => ( (value: string) => (
(prf.current!.data.sgt.target = value), (prf.current!.data.sgt.target = value),
@ -80,7 +86,7 @@ const checkSegatoolsIni = async (target: string) => {
<FilePicker <FilePicker
:directory="true" :directory="true"
placeholder="amfs" placeholder="amfs"
:value="prf.current!.data.sgt.amfs" :value="prf.current?.data.sgt.amfs"
:callback=" :callback="
(value: string) => (prf.current!.data.sgt.amfs = value) (value: string) => (prf.current!.data.sgt.amfs = value)
" "
@ -106,7 +112,7 @@ const checkSegatoolsIni = async (target: string) => {
></FilePicker> ></FilePicker>
</OptionRow> </OptionRow>
<OptionRow <OptionRow
:title="names.hook" :title="names?.hook"
:tooltip=" :tooltip="
t('cfg.segatools.installTooltip', { t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.hooks'), thing: t('cfg.segatools.hooks'),
@ -132,19 +138,18 @@ const checkSegatoolsIni = async (target: string) => {
></Select> ></Select>
</OptionRow> </OptionRow>
<OptionRow <OptionRow
:title="names.io" :title="names?.io"
:tooltip=" :tooltip="`${t('cfg.segatools.ioModulesDesc')}
t('cfg.segatools.installTooltip', { ${t('cfg.segatools.installTooltip', {
thing: t('cfg.segatools.ioModules'), thing: t('cfg.segatools.ioModules'),
}) })}`"
"
> >
<Select <Select
v-model="prf.current!.data.sgt.io2" v-model="prf.current!.data.sgt.io2"
:options="[ :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', value: 'segatools_built_in',
}, },
...pkgs ...pkgs
@ -164,5 +169,29 @@ const checkSegatoolsIni = async (target: string) => {
option-value="value" option-value="value"
></Select> ></Select>
</OptionRow> </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> </OptionCategory>
</template> </template>

View File

@ -1,8 +1,7 @@
import en from './i18n/en'; import en from './i18n/en';
import ja from './i18n/ja';
import { createI18n } from 'vue-i18n'; import { createI18n } from 'vue-i18n';
export type Locale = 'en' | 'ja'; export type Locale = 'en' | 'ja' | 'pl';
const loadLocaleMessages = async (locale: Locale) => { const loadLocaleMessages = async (locale: Locale) => {
return (await import(`./i18n/${locale}.ts`)).default; return (await import(`./i18n/${locale}.ts`)).default;
@ -12,7 +11,9 @@ const i18n = createI18n({
legacy: false, legacy: false,
locale: 'en', locale: 'en',
fallbackLocale: 'en', fallbackLocale: 'en',
messages: { en, ja }, warnHtmlInMessage: false,
warnHtmlMessage: false,
messages: { en, ja: {}, pl: {} },
}); });
const setLocale = async (locale: Locale) => { const setLocale = async (locale: Locale) => {

View File

@ -4,6 +4,11 @@ export default {
enable: 'Enable', enable: 'Enable',
disable: 'Disable', disable: 'Disable',
default: 'Default', default: 'Default',
search: 'Search',
next: 'Next',
skip: 'Skip',
close: 'Close',
by: 'by {namespace}',
start: { start: {
failed: 'Start check failed', failed: 'Start check failed',
accept: 'Run anyway', accept: 'Run anyway',
@ -21,11 +26,11 @@ export default {
button: { button: {
start: 'START', start: 'START',
stop: 'STOP', stop: 'STOP',
unchecked: 'Start unchecked', unchecked: 'Skip checks and start',
shortcut: 'Create desktop shortcut', shortcut: 'Create desktop shortcut',
help: 'Help', help: 'Help',
refresh: 'Refresh and start', refresh: 'Reapply mods and start',
cache: 'Clear cache', cache: 'Clear mod cache',
}, },
}, },
game: { game: {
@ -38,6 +43,22 @@ export default {
delete: 'Delete profile', delete: 'Delete profile',
reallyDelete: 'Are you sure you want to delete {profile}?', reallyDelete: 'Are you sure you want to delete {profile}?',
template: 'STARTLINER template', 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: { store: {
installRecommended: 'Install recommended packages', installRecommended: 'Install recommended packages',
@ -45,14 +66,33 @@ export default {
deprecated: 'Show deprecated', deprecated: 'Show deprecated',
nsfw: 'Show NSFW', nsfw: 'Show NSFW',
incompatible: 'This package is currently incompatible with STARTLINER.', incompatible: 'This package is currently incompatible with STARTLINER.',
includeCategories: 'Include categories',
excludeCategories: 'Exclude categories',
},
pkglist: {
missing: 'Missing', 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: { patch: {
loading: 'Loading...', loading: 'Loading...',
noneFound: noneFound:
"No compatible patches found. Make sure you're using unpacked and unpatched files.", "No compatible patches found. Make sure you're using unpacked and unpatched files.",
forceLoad: 'Force load',
// Example patch name override // 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: { cfg: {
afterRestart: 'Applied after a restart', afterRestart: 'Applied after a restart',
@ -64,9 +104,14 @@ export default {
'STARTLINER expects unpacked executables put into otherwise clean data.', 'STARTLINER expects unpacked executables put into otherwise clean data.',
hooks: 'Hooks', hooks: 'Hooks',
ioModules: 'IO modules', 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.', installTooltip: '{thing} can be downloaded from the package store.',
}, },
display: { display: {
title: 'Display',
resolution: 'Game resolution',
primary: 'Primary', primary: 'Primary',
target: 'Target display', target: 'Target display',
mode: 'Mode', mode: 'Mode',
@ -82,6 +127,9 @@ export default {
portrait: 'Portrait', portrait: 'Portrait',
landscape: 'Landscape', landscape: 'Landscape',
flipped: 'flipped', flipped: 'flipped',
window: 'Window',
borderless: 'Borderless window',
fullscreen: 'Fullscreen',
}, },
network: { network: {
title: '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)', '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', leverMode: 'Lever mode',
mouse: 'Mouse', mouse: 'Mouse',
irTooltip:
'When playing on an actual keyboard, only bind ir1; leave the rest unbound',
},
wine: {
prefix: 'Wine prefix',
runtime: 'Wine runtime',
}, },
startliner: { startliner: {
offlineMode: 'Offline mode', offlineMode: 'Offline mode',
offlineModeTooltip: 'Disables the package store.', offlineModeTooltip: 'Disables the package store.',
@ -150,4 +203,46 @@ export default {
verbose: 'Detailed logs', 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
View File

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

View File

@ -32,23 +32,24 @@ export const useGeneralStore = defineStore('general', () => {
}, },
}); });
const configDir = computed(() => { const loadDirs = async () => {
if (dirs.value === null) { 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(() => { const dataDir = computed(async () => {
if (dirs.value === null) { await loadDirs();
throw new Error('Invalid directory access'); return dirs.value!.data_dir;
}
return dirs.value.data_dir;
}); });
const cacheDir = computed(() => { const cacheDir = computed(async () => {
if (dirs.value === null) { await loadDirs();
throw new Error('Invalid directory access'); return dirs.value!.cache_dir;
}
return dirs.value.cache_dir;
}); });
return { return {
@ -321,8 +322,8 @@ export const usePrfStore = defineStore('prf', () => {
const generalStore = useGeneralStore(); const generalStore = useGeneralStore();
const configDir = computed(async () => { const configDir = computed(async () => {
return await path.join( return path.join(
generalStore.configDir, await generalStore.configDir,
`profile-${current.value?.meta.game}-${current.value?.meta.name}` `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 theme: Ref<'light' | 'dark' | 'system'> = ref('system');
const onboarded: Ref<Game[]> = ref([]); const onboarded: Ref<Game[]> = ref([]);
const locale: Ref<Locale> = ref('en'); const locale: Ref<Locale> = ref('en');
const currentTab: Ref<string> = ref('users');
const pkgListMode: Ref<'namespace' | 'type' | 'category'> =
ref('namespace');
const hiddenCategories: Ref<string[]> = ref([]);
const _scaleValue = (value: ScaleType) => const _scaleValue = (value: ScaleType) =>
value === 's' ? 1 : value === 'm' ? 1.25 : value === 'l' ? 1.5 : 2; 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( const input = JSON.parse(
await readTextFile( await readTextFile(
await path.join( await path.join(
generalStore.configDir, await generalStore.configDir,
'client-options.json' 'client-options.json'
) )
) )
@ -439,6 +444,18 @@ export const useClientStore = defineStore('client', () => {
if (input.locale) { if (input.locale) {
locale.value = 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 setLocale(locale.value);
await setTheme(theme.value); await setTheme(theme.value);
} catch (e) { } catch (e) {
@ -464,7 +481,10 @@ export const useClientStore = defineStore('client', () => {
const size = await w.innerSize(); const size = await w.innerSize();
await writeTextFile( await writeTextFile(
await path.join(generalStore.configDir, 'client-options.json'), await path.join(
await generalStore.configDir,
'client-options.json'
),
JSON.stringify({ JSON.stringify({
scaleFactor: scaleFactor.value, scaleFactor: scaleFactor.value,
windowSize: { windowSize: {
@ -474,6 +494,9 @@ export const useClientStore = defineStore('client', () => {
theme: theme.value, theme: theme.value,
onboarded: onboarded.value, onboarded: onboarded.value,
locale: locale.value, locale: locale.value,
currentTab: currentTab.value,
pkgListMode: pkgListMode.value,
hiddenCategories: hiddenCategories.value,
}) })
); );
}; };
@ -549,6 +572,9 @@ export const useClientStore = defineStore('client', () => {
locale, locale,
timeout, timeout,
scaleModel, scaleModel,
currentTab,
pkgListMode,
hiddenCategories,
_scaleValue, _scaleValue,
scaleValue, scaleValue,
load, load,

View File

@ -56,6 +56,7 @@ export interface ProfileData {
display: DisplayConfig; display: DisplayConfig;
network: NetworkConfig; network: NetworkConfig;
bepinex: BepInExConfig; bepinex: BepInExConfig;
wine: WineConfig;
mu3_ini: Mu3IniConfig | undefined; mu3_ini: Mu3IniConfig | undefined;
keyboard: KeyboardConfig | undefined; keyboard: KeyboardConfig | undefined;
patches: { patches: {
@ -105,6 +106,11 @@ export interface BepInExConfig {
console: boolean; console: boolean;
} }
export interface WineConfig {
runtime: string;
prefix: string;
}
export interface Mu3IniConfig { export interface Mu3IniConfig {
audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch'; audio?: 'Shared' | 'Excl6Ch' | 'Excl2Ch';
sample_rate: number; sample_rate: number;