Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
e569d57788 | |||
b75cc8f240 | |||
407b34a884 |
10
CHANGELOG.md
10
CHANGELOG.md
@ -1,3 +1,13 @@
|
|||||||
|
## 0.14.0
|
||||||
|
|
||||||
|
- Added the custom FREE PLAY patch for Verse
|
||||||
|
|
||||||
|
## 0.13.0
|
||||||
|
|
||||||
|
- Added profile imports/exports
|
||||||
|
- Fixed error when trying to open an empty Ongeki profile
|
||||||
|
- Switched the default color scheme from invisible to purple
|
||||||
|
|
||||||
## 0.12.1
|
## 0.12.1
|
||||||
|
|
||||||
- Chunithm: fixed crash when using mempatcher
|
- Chunithm: fixed crash when using mempatcher
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
Looking for
|
||||||
|
|
||||||
|
- maimai DX players willing to help develop/test maimai DX support
|
||||||
|
- translators (any language other than English)
|
||||||
|
|
||||||
# STARTLINER
|
# STARTLINER
|
||||||
|
|
||||||
This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
|
This is a program that seeks to streamline game data configuration, currently supporting O.N.G.E.K.I. and CHUNITHM.
|
||||||
|
1
TODO.md
1
TODO.md
@ -1,5 +1,6 @@
|
|||||||
### Short-term
|
### Short-term
|
||||||
|
|
||||||
|
- i18n
|
||||||
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
|
- https://gitea.tendokyu.moe/TeamTofuShop/segatools/issues/63
|
||||||
|
|
||||||
### Long-term
|
### Long-term
|
||||||
|
@ -410,6 +410,33 @@ pub async fn create_shortcut(app: AppHandle, profile_meta: ProfileMeta) -> Resul
|
|||||||
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
|
util::create_shortcut(app, &profile_meta).map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn export_profile(state: State<'_, Mutex<AppData>>, export_keychip: bool, files: Vec<String>) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: export_profile({:?}, {:?} files)", export_keychip, files.len());
|
||||||
|
|
||||||
|
let appd = state.lock().await;
|
||||||
|
match &appd.profile {
|
||||||
|
Some(p) => {
|
||||||
|
p.export(export_keychip, files)
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let err = "export_profile: no profile".to_owned();
|
||||||
|
log::error!("{}", err);
|
||||||
|
return Err(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn import_profile(path: PathBuf) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: import_profile({:?})", path);
|
||||||
|
|
||||||
|
Profile::import(path).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[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");
|
||||||
|
@ -205,6 +205,8 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::save_current_profile,
|
cmd::save_current_profile,
|
||||||
cmd::load_segatools_ini,
|
cmd::load_segatools_ini,
|
||||||
cmd::create_shortcut,
|
cmd::create_shortcut,
|
||||||
|
cmd::export_profile,
|
||||||
|
cmd::import_profile,
|
||||||
|
|
||||||
cmd::get_global_config,
|
cmd::get_global_config,
|
||||||
cmd::set_global_config,
|
cmd::set_global_config,
|
||||||
|
@ -42,6 +42,7 @@ pub struct Patch {
|
|||||||
pub enum PatchData {
|
pub enum PatchData {
|
||||||
Normal(NormalPatch),
|
Normal(NormalPatch),
|
||||||
Number(NumberPatch),
|
Number(NumberPatch),
|
||||||
|
Hex(HexPatch),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug)]
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
@ -65,6 +66,12 @@ pub struct NumberPatch {
|
|||||||
pub max: i32
|
pub max: i32
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Clone, Debug)]
|
||||||
|
pub struct HexPatch {
|
||||||
|
pub offset: u64,
|
||||||
|
pub off: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Serialize for Patch {
|
impl Serialize for Patch {
|
||||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
where S: Serializer {
|
where S: Serializer {
|
||||||
@ -83,6 +90,11 @@ impl Serialize for Patch {
|
|||||||
state.serialize_field("size", &patch.size)?;
|
state.serialize_field("size", &patch.size)?;
|
||||||
state.serialize_field("min", &patch.min)?;
|
state.serialize_field("min", &patch.min)?;
|
||||||
state.serialize_field("max", &patch.max)?;
|
state.serialize_field("max", &patch.max)?;
|
||||||
|
},
|
||||||
|
PatchData::Hex(patch) => {
|
||||||
|
state.serialize_field("type", "hex")?;
|
||||||
|
state.serialize_field("offset", &patch.offset)?;
|
||||||
|
state.serialize_field("off", &patch.off)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
state.end()
|
state.end()
|
||||||
@ -114,6 +126,23 @@ impl<'de> serde::Deserialize<'de> for Patch {
|
|||||||
.ok_or_else(|| de::Error::missing_field("max"))?
|
.ok_or_else(|| de::Error::missing_field("max"))?
|
||||||
).map_err(|_| de::Error::missing_field("max"))?
|
).map_err(|_| de::Error::missing_field("max"))?
|
||||||
}),
|
}),
|
||||||
|
Some("hex") => {
|
||||||
|
let mut off_list = Vec::new();
|
||||||
|
for off in value.get("off").and_then(Value::as_array).unwrap() {
|
||||||
|
off_list.push(u8::try_from(
|
||||||
|
off.as_u64().ok_or_else(|| de::Error::missing_field("off"))?
|
||||||
|
).map_err(|_| de::Error::missing_field("off"))?);
|
||||||
|
}
|
||||||
|
// for off in value.get("off").and_then(Value::as_str).unwrap().bytes() {
|
||||||
|
// off_list.push(off);
|
||||||
|
// }
|
||||||
|
PatchData::Hex(HexPatch {
|
||||||
|
offset: value.get("offset")
|
||||||
|
.and_then(Value::as_u64)
|
||||||
|
.ok_or_else(|| de::Error::missing_field("offset"))?,
|
||||||
|
off: off_list
|
||||||
|
})
|
||||||
|
},
|
||||||
None => {
|
None => {
|
||||||
let mut patches = vec![];
|
let mut patches = vec![];
|
||||||
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
|
for patch in value.get("patches").and_then(Value::as_array).unwrap() {
|
||||||
|
@ -50,6 +50,21 @@ impl PatchSelection {
|
|||||||
} else {
|
} else {
|
||||||
log::error!("invalid number patch {:?}", patch);
|
log::error!("invalid number patch {:?}", patch);
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
PatchData::Hex(data) => {
|
||||||
|
if let PatchSelectionData::Hex(val) = sel {
|
||||||
|
res += &format!("{} F+{:X} ", filename, data.offset);
|
||||||
|
for byte in val {
|
||||||
|
res += &format!("{:02X}", byte);
|
||||||
|
}
|
||||||
|
res += " ";
|
||||||
|
for byte in &data.off {
|
||||||
|
res += &format!("{:02X}", byte);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log::error!("invalid number patch {:?}", patch);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
format!("{}\n", res)
|
format!("{}\n", res)
|
||||||
|
@ -10,6 +10,7 @@ use std::fs::File;
|
|||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::task::JoinSet;
|
use tokio::task::JoinSet;
|
||||||
|
|
||||||
|
pub mod template;
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
impl Profile {
|
impl Profile {
|
||||||
@ -43,12 +44,6 @@ impl Profile {
|
|||||||
std::fs::create_dir_all(p.config_dir())?;
|
std::fs::create_dir_all(p.config_dir())?;
|
||||||
std::fs::create_dir_all(p.data_dir())?;
|
std::fs::create_dir_all(p.data_dir())?;
|
||||||
|
|
||||||
if meta.game == Game::Ongeki {
|
|
||||||
if let Err(e) = Self::load_existing_mu3_ini(&p.data, &p.meta) {
|
|
||||||
log::error!("unable to load existing mu3.ini: {e}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match meta.game {
|
match meta.game {
|
||||||
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
|
Game::Ongeki => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-ongeki.ini"))?,
|
||||||
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
|
Game::Chunithm => std::fs::write(p.config_dir().join("segatools-base.ini"), include_bytes!("../../static/segatools-chunithm.ini"))?,
|
||||||
@ -431,12 +426,14 @@ impl Profile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> {
|
fn load_existing_mu3_ini(data: &ProfileData, meta: &ProfileMeta) -> Result<()> {
|
||||||
let mu3_ini_target_path = data.sgt.target.parent().ok_or_else(|| anyhow!("invalid target directory"))?.join("mu3.ini");
|
if let Some(parent) = data.sgt.target.parent() {
|
||||||
let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini");
|
let mu3_ini_target_path = parent.join("mu3.ini");
|
||||||
log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path);
|
let mu3_ini_profile_path = util::profile_config_dir(meta.game, &meta.name).join("mu3.ini");
|
||||||
if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() {
|
log::debug!("mu3.ini paths: {:?} {:?}", mu3_ini_target_path, mu3_ini_profile_path);
|
||||||
std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?;
|
if mu3_ini_target_path.exists() && !mu3_ini_profile_path.exists() {
|
||||||
log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path);
|
std::fs::copy(&mu3_ini_target_path, &mu3_ini_profile_path)?;
|
||||||
|
log::info!("copied mu3.ini from {:?}", &mu3_ini_target_path);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
90
rust/src/profiles/template.rs
Normal file
90
rust/src/profiles/template.rs
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
use std::{fs::File, io::{Read, Write}, path::PathBuf};
|
||||||
|
use zip::{write::FileOptions, ZipArchive, ZipWriter};
|
||||||
|
use crate::util;
|
||||||
|
use super::{Profile, ProfilePaths};
|
||||||
|
|
||||||
|
impl Profile {
|
||||||
|
fn find_template_json(archive: &mut ZipArchive<File>) -> anyhow::Result<String> {
|
||||||
|
if let Ok(mut file) = archive.by_name("template.json") {
|
||||||
|
let mut contents = Vec::new();
|
||||||
|
file.read_to_end(&mut contents)?;
|
||||||
|
Ok(String::from_utf8(contents)?)
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("invalid template: no template.json found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn import(path: PathBuf) -> anyhow::Result<()> {
|
||||||
|
let file = File::open(path)?;
|
||||||
|
let mut archive = ZipArchive::new(file)?;
|
||||||
|
|
||||||
|
match Self::find_template_json(&mut archive) {
|
||||||
|
Ok(raw_p) => {
|
||||||
|
let p = serde_json::from_str::<Profile>(&raw_p)?;
|
||||||
|
let dir = util::config_dir().join(format!("profile-{}-{}", &p.meta.game, &p.meta.name));
|
||||||
|
if dir.exists() {
|
||||||
|
anyhow::bail!("profile {} already exists", &p.meta.name);
|
||||||
|
}
|
||||||
|
std::fs::create_dir(&dir)?;
|
||||||
|
archive.extract(&dir)?;
|
||||||
|
std::fs::remove_file(dir.join("template.json"))?;
|
||||||
|
std::fs::write(dir.join("profile.json"), serde_json::to_string_pretty(&p.data)?)?;
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
pub fn export(&self, export_keychip: bool, extra_files: Vec<String>) -> anyhow::Result<()> {
|
||||||
|
let mut prf = self.clone();
|
||||||
|
|
||||||
|
let dir = util::config_dir().join("exports");
|
||||||
|
|
||||||
|
if !dir.exists() {
|
||||||
|
std::fs::create_dir(&dir)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = dir.join(format!("{}-{}-template.zip", &self.meta.game, &self.meta.name));
|
||||||
|
|
||||||
|
{
|
||||||
|
let sgt = &mut prf.data.sgt;
|
||||||
|
sgt.target = PathBuf::new();
|
||||||
|
if sgt.amfs.is_absolute() {
|
||||||
|
sgt.amfs = PathBuf::new();
|
||||||
|
}
|
||||||
|
if sgt.option.is_absolute() {
|
||||||
|
sgt.option = PathBuf::new();
|
||||||
|
}
|
||||||
|
if sgt.appdata.is_absolute() {
|
||||||
|
sgt.appdata = PathBuf::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let network = &mut prf.data.network;
|
||||||
|
if network.local_path.is_absolute() {
|
||||||
|
network.local_path = PathBuf::new();
|
||||||
|
}
|
||||||
|
if !export_keychip {
|
||||||
|
network.keychip = String::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = File::create(&path)?;
|
||||||
|
let mut zip = ZipWriter::new(file);
|
||||||
|
let options: FileOptions<'_, ()> = FileOptions::default();
|
||||||
|
zip.start_file("template.json", options)?;
|
||||||
|
zip.write_all(&serde_json::to_string_pretty(&prf)?.as_bytes())?;
|
||||||
|
|
||||||
|
for file in extra_files {
|
||||||
|
log::debug!("extra file: {file}");
|
||||||
|
zip.start_file(&file, options)?;
|
||||||
|
zip.write_all(&std::fs::read(self.config_dir().join(file))?)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
zip.finish()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
@ -326,6 +326,26 @@
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-custom-free-play-length',
|
||||||
|
type: 'number',
|
||||||
|
name: 'Custom FREE PLAY text length',
|
||||||
|
tooltip: 'Changes the length of the text displayed when Force FREE PLAY credit text is enabled',
|
||||||
|
danger: 'If this is longer than 11 characters, \"Force FREE PLAY credit text\" MUST be enabled.',
|
||||||
|
offset: 0x3875A9,
|
||||||
|
size: 1,
|
||||||
|
default: 9,
|
||||||
|
min: 0,
|
||||||
|
max: 27,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'standard-custom-free-play-text',
|
||||||
|
type: 'hex',
|
||||||
|
name: 'Custom FREE PLAY text',
|
||||||
|
tooltip: 'Replace the FREE PLAY text when using Infinite credits',
|
||||||
|
offset: 0x1A5DFB4,
|
||||||
|
off: [0x46, 0x52, 0x45, 0x45, 0x20, 0x50, 0x4c, 0x41, 0x59],
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "STARTLINER",
|
"productName": "STARTLINER",
|
||||||
"version": "0.12.1",
|
"version": "0.13.0",
|
||||||
"identifier": "zip.patafour.startliner",
|
"identifier": "zip.patafour.startliner",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
import InputNumber from 'primevue/inputnumber';
|
import InputNumber from 'primevue/inputnumber';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
import ToggleSwitch from 'primevue/toggleswitch';
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import OptionRow from './OptionRow.vue';
|
import OptionRow from './OptionRow.vue';
|
||||||
import { usePrfStore } from '../stores';
|
import { usePrfStore } from '../stores';
|
||||||
@ -23,9 +25,26 @@ const setNumber = (key: string, val: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
patch: Object as () => Patch,
|
patch: Object as () => Patch,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// One day, I will repent
|
||||||
|
const hexModel = computed({
|
||||||
|
get() {
|
||||||
|
const hex = (prf.current!.data.patches[props.patch!.id!] as any)?.hex;
|
||||||
|
if (hex !== undefined) {
|
||||||
|
return new TextDecoder().decode(new Int8Array(hex).buffer);
|
||||||
|
} else {
|
||||||
|
return 'FREE PLAY';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
set(value: string) {
|
||||||
|
(prf.current!.data.patches[props.patch!.id!] as any) = {
|
||||||
|
hex: new TextEncoder().encode(value),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -50,5 +69,6 @@ defineProps({
|
|||||||
:max="patch?.max"
|
:max="patch?.max"
|
||||||
:placeholder="(patch?.default ?? 0).toString()"
|
:placeholder="(patch?.default ?? 0).toString()"
|
||||||
/>
|
/>
|
||||||
|
<InputText v-else-if="patch?.type === 'hex'" v-model="hexModel" />
|
||||||
</OptionRow>
|
</OptionRow>
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,29 +1,154 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { Ref, ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
|
import * as path from '@tauri-apps/api/path';
|
||||||
|
import { open } from '@tauri-apps/plugin-dialog';
|
||||||
import ProfileListEntry from './ProfileListEntry.vue';
|
import ProfileListEntry from './ProfileListEntry.vue';
|
||||||
import { usePrfStore } from '../stores';
|
import { invoke } from '../invoke';
|
||||||
|
import { useClientStore, useGeneralStore, usePrfStore } from '../stores';
|
||||||
|
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
|
const client = useClientStore();
|
||||||
|
const general = useGeneralStore();
|
||||||
|
|
||||||
|
const exportVisible = ref(false);
|
||||||
|
const exportKeychip = ref(false);
|
||||||
|
const files = new Set<string>();
|
||||||
|
|
||||||
|
const exportTemplate = async () => {
|
||||||
|
const fl = [...files.values()];
|
||||||
|
exportVisible.value = false;
|
||||||
|
await invoke('export_profile', {
|
||||||
|
exportKeychip: exportKeychip.value,
|
||||||
|
files: fl,
|
||||||
|
});
|
||||||
|
await invoke('open_file', {
|
||||||
|
path: await path.join(general.configDir, 'exports'),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileList = {
|
||||||
|
ongeki: ['aime.txt', 'inohara.cfg', 'mu3.ini', 'segatools-base.ini'],
|
||||||
|
chunithm: ['aime.txt', 'saekawa.toml', 'segatools-base.ini'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const fileListCurrent: Ref<string[]> = ref([]);
|
||||||
|
|
||||||
|
const recalcFileList = async () => {
|
||||||
|
const res: string[] = [];
|
||||||
|
files.clear();
|
||||||
|
for (const idx in fileList[prf.current!.meta.game]) {
|
||||||
|
const f = fileList[prf.current!.meta.game][idx];
|
||||||
|
const p = await path.join(await prf.configDir, f);
|
||||||
|
if (await invoke('file_exists', { path: p })) {
|
||||||
|
res.push(f);
|
||||||
|
files.add(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fileListCurrent.value = res;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openExportDialog = async () => {
|
||||||
|
await recalcFileList();
|
||||||
|
exportVisible.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importPick = async () => {
|
||||||
|
const res = await open({
|
||||||
|
multiple: false,
|
||||||
|
directory: false,
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'STARTLINER template',
|
||||||
|
extensions: ['zip'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
if (res != null) {
|
||||||
|
await invoke('import_profile', { path: res });
|
||||||
|
await prf.reloadList();
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Dialog
|
||||||
|
modal
|
||||||
|
:visible="exportVisible"
|
||||||
|
:closable="false /*this shit doesn't work */"
|
||||||
|
:header="`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>
|
||||||
|
<ToggleSwitch v-model="exportKeychip" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-row" v-for="f in fileListCurrent">
|
||||||
|
<div class="grow">Export {{ f }}</div>
|
||||||
|
<ToggleSwitch
|
||||||
|
:model-value="true"
|
||||||
|
@update:model-value="
|
||||||
|
(v) => {
|
||||||
|
if (v === true) {
|
||||||
|
files.add(f);
|
||||||
|
} else {
|
||||||
|
files.delete(f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div style="width: 100%; text-align: center">
|
||||||
|
<Button
|
||||||
|
class="m-auto mr-3"
|
||||||
|
style="width: 80px"
|
||||||
|
label="OK"
|
||||||
|
@click="() => exportTemplate()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
class="m-auto"
|
||||||
|
style="width: 80px"
|
||||||
|
label="Cancel"
|
||||||
|
@click="() => (exportVisible = false)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
<div v-if="prf.list.length === 0">
|
<div v-if="prf.list.length === 0">
|
||||||
Welcome to STARTLINER! Start by creating a profile.
|
Welcome to STARTLINER! Start by creating a profile.
|
||||||
</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="O.N.G.E.K.I. profile"
|
label="O.N.G.E.K.I. profile"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-file-plus"
|
||||||
class="ongeki-button profile-button"
|
class="ongeki-button profile-button"
|
||||||
@click="() => prf.create('ongeki')"
|
@click="() => prf.create('ongeki')"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
label="CHUNITHM profile"
|
label="CHUNITHM profile"
|
||||||
icon="pi pi-plus"
|
icon="pi pi-file-plus"
|
||||||
class="chunithm-button profile-button"
|
class="chunithm-button profile-button"
|
||||||
@click="() => prf.create('chunithm')"
|
@click="() => prf.create('chunithm')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mt-4 flex flex-row flex-wrap align-middle gap-4">
|
||||||
|
<Button
|
||||||
|
label="Import template"
|
||||||
|
icon="pi pi-file-import"
|
||||||
|
class="import-button profile-button"
|
||||||
|
@click="() => importPick()"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
:disabled="prf.current === null"
|
||||||
|
label="Export template"
|
||||||
|
icon="pi pi-file-export"
|
||||||
|
class="profile-button"
|
||||||
|
@click="() => openExportDialog()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
<div class="mt-12 flex flex-col flex-wrap align-middle gap-4">
|
||||||
<div v-for="p in prf.list">
|
<div v-for="p in prf.list">
|
||||||
<ProfileListEntry :p="p" />
|
<ProfileListEntry :p="p" />
|
||||||
@ -57,4 +182,14 @@ const prf = usePrfStore();
|
|||||||
background-color: var(--p-yellow-300) !important;
|
background-color: var(--p-yellow-300) !important;
|
||||||
border-color: var(--p-yellow-300) !important;
|
border-color: var(--p-yellow-300) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-button {
|
||||||
|
background-color: var(--p-purple-400) !important;
|
||||||
|
border-color: var(--p-purple-400) !important;
|
||||||
|
}
|
||||||
|
.import-button:hover,
|
||||||
|
.import-button:active {
|
||||||
|
background-color: var(--p-purple-300) !important;
|
||||||
|
border-color: var(--p-purple-300) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -167,7 +167,7 @@ export interface Patch {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
type: undefined | 'number';
|
type: undefined | 'number' | 'hex';
|
||||||
default: number;
|
default: number;
|
||||||
min: number;
|
min: number;
|
||||||
max: number;
|
max: number;
|
||||||
|
@ -3,11 +3,7 @@ import { Feature, Game, Package } from './types';
|
|||||||
|
|
||||||
export const changePrimaryColor = (game: Game | null) => {
|
export const changePrimaryColor = (game: Game | null) => {
|
||||||
const color =
|
const color =
|
||||||
game === 'ongeki'
|
game === 'ongeki' ? 'pink' : game === 'chunithm' ? 'yellow' : 'purple';
|
||||||
? 'pink'
|
|
||||||
: game === 'chunithm'
|
|
||||||
? 'yellow'
|
|
||||||
: 'bluegray';
|
|
||||||
|
|
||||||
updatePrimaryPalette({
|
updatePrimaryPalette({
|
||||||
50: `{${color}.50}`,
|
50: `{${color}.50}`,
|
||||||
|
Reference in New Issue
Block a user