14 Commits

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

View File

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

View File

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

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 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()]);

View File

@ -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()?;

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
pub use types::{Profile, ProfileData, ProfileMeta, ProfilePaths, StartPayload};
use std::{collections::{BTreeMap, BTreeSet}, path::{Path, PathBuf}};
use crate::{model::{misc::Game, patch::{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"))?;

View File

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

View File

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

View File

@ -63,9 +63,9 @@
},
{
// Ongeki
filename: "amdaemon.exe",
filename: "amdaemon.exe",
version: "46d47eab",
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
patches: [
{
id: 'standard-localhost',

View File

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

View File

@ -20,15 +20,16 @@ import OptionList from './OptionList.vue';
import PatchList from './PatchList.vue';
import ProfileList from './ProfileList.vue';
import StartButton from './StartButton.vue';
import { invoke } from '../invoke';
import {
useClientStore,
useGeneralStore,
usePkgStore,
usePrfStore,
} from '../stores';
import { Dirs } from '../types';
import { messageSplit, shouldPreferDark } from '../util';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
document.documentElement.classList.toggle('use-dark-mode', shouldPreferDark());
@ -37,10 +38,10 @@ const prf = usePrfStore();
const general = useGeneralStore();
const client = useClientStore();
client.load();
pkg.setupListeners();
const currentTab: Ref<'users' | 'loc' | 'patches' | 'rmt' | 'cfg' | 'info'> =
ref('users');
const pkgSearchTerm = ref('');
const isProfileDisabled = computed(() => prf.current === null);
@ -56,17 +57,11 @@ listen<undefined>('update-end', (_) => {
});
onMounted(async () => {
invoke('list_directories').then((d) => {
general.dirs = d as Dirs;
client.load();
});
const fetch_promise = pkg.fetch(true);
await Promise.all([prf.reloadList(), prf.reload()]);
if (prf.current !== null) {
currentTab.value = 'loc';
await pkg.reloadAll();
}
@ -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"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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,
];
}
});

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { Ref, computed, ref } from 'vue';
import { Ref, computed, onMounted, ref } from 'vue';
import InputNumber from 'primevue/inputnumber';
import Select from 'primevue/select';
import SelectButton from 'primevue/selectbutton';
@ -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>

View File

@ -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)"
/>

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { computed } from 'vue';
import { Ref, computed, ref } from 'vue';
import Select from 'primevue/select';
import { useConfirm } from 'primevue/useconfirm';
import { emit } from '@tauri-apps/api/event';
@ -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>

View File

@ -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) => {

View File

@ -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
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) {
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,

View File

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