Compare commits
4 Commits
Author | SHA1 | Date | |
---|---|---|---|
14a65eb5bb | |||
0add9200a6 | |||
ee49da3665 | |||
f478ad9216 |
14
CHANGELOG.md
14
CHANGELOG.md
@ -1,3 +1,17 @@
|
||||
## 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
|
||||
|
BIN
public/no-icon.png
Normal file
BIN
public/no-icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
@ -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;
|
||||
@ -166,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");
|
||||
|
@ -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,
|
||||
|
@ -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>,
|
||||
|
||||
|
@ -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");
|
||||
}
|
||||
if enabled_ir {
|
||||
ini.with_section(Some("io3"))
|
||||
.set("ir", "0");
|
||||
} else {
|
||||
ini.with_section(Some("io3"))
|
||||
.set("ir", kb.ir[0].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "STARTLINER",
|
||||
"version": "0.16.0",
|
||||
"version": "0.18.1",
|
||||
"identifier": "zip.patafour.startliner",
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run dev",
|
||||
|
@ -4,6 +4,9 @@ import InputText from 'primevue/inputtext';
|
||||
import { fromKeycode, toKeycode } from '../keyboard';
|
||||
import { usePrfStore } from '../stores';
|
||||
import { OngekiButtons } from '../types';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
|
||||
@ -61,6 +64,10 @@ const handleKey = (
|
||||
}
|
||||
}
|
||||
|
||||
if (event.code === 'Escape') {
|
||||
keycode = 0;
|
||||
}
|
||||
|
||||
if (index !== undefined) {
|
||||
data[button][index] = keycode;
|
||||
} else {
|
||||
@ -160,13 +167,24 @@ const fontSize = computed(() => {
|
||||
<InputText
|
||||
:style="{
|
||||
width: small ? '2.8rem' : '5rem',
|
||||
height: small ? '2.8rem' : tall ? '10rem' : '5rem',
|
||||
height:
|
||||
small && tall
|
||||
? '5rem'
|
||||
: small
|
||||
? '2.8rem'
|
||||
: tall
|
||||
? '10rem'
|
||||
: '5rem',
|
||||
fontSize,
|
||||
backgroundColor: color,
|
||||
}"
|
||||
unstyled
|
||||
class="text-center buttoninputtext"
|
||||
v-tooltip="tooltip ? `${tooltip}: ${modelValue}` : undefined"
|
||||
v-tooltip="
|
||||
tooltip
|
||||
? `${tooltip}: ${modelValue} ${tooltip.startsWith('ir') ? `\n${t('cfg.keyboard.irTooltip')}` : ''}`
|
||||
: undefined
|
||||
"
|
||||
@contextmenu.prevent="() => {}"
|
||||
@keydown="(ev: KeyboardEvent) => handleKey(button, ev, index)"
|
||||
@mousedown="
|
||||
|
@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { Ref, computed, ref } from 'vue';
|
||||
import Button from 'primevue/button';
|
||||
import Dialog from 'primevue/dialog';
|
||||
import Fieldset from 'primevue/fieldset';
|
||||
import InputText from 'primevue/inputtext';
|
||||
import MultiSelect from 'primevue/multiselect';
|
||||
import SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
import { emit } from '@tauri-apps/api/event';
|
||||
import ModListEntry from './ModListEntry.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { Package } from '../types';
|
||||
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Feature, Game, Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@ -17,38 +23,120 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const pkgs = usePkgStore();
|
||||
const client = useClientStore();
|
||||
const prf = usePrfStore();
|
||||
const empty = ref(false);
|
||||
const gameSublist: Ref<string[]> = ref([]);
|
||||
|
||||
const loadPackages = () => {
|
||||
invoke('get_game_packages', {
|
||||
game: prf.current?.meta.game ?? null,
|
||||
}).then((list) => {
|
||||
gameSublist.value = list as string[];
|
||||
});
|
||||
};
|
||||
|
||||
const group = computed(() => {
|
||||
const res = Object.assign(
|
||||
{},
|
||||
Object.groupBy(
|
||||
pkgs.allLocal
|
||||
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 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())
|
||||
)
|
||||
.sort((p1, p2) => p1.namespace.localeCompare(p2.namespace))
|
||||
.sort((p1, p2) => p1.name.localeCompare(p2.name)),
|
||||
({ namespace }) => namespace
|
||||
)
|
||||
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
|
||||
p.namespace.toLowerCase().includes(props.search.toLowerCase())
|
||||
);
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
|
||||
let grouped;
|
||||
if (client.pkgListMode === 'namespace') {
|
||||
grouped = Object.groupBy(searchedPkgs, ({ namespace }) => namespace);
|
||||
} else if (client.pkgListMode === 'type') {
|
||||
grouped = {
|
||||
standard: [] as Package[],
|
||||
native: [] as Package[],
|
||||
segatools: [] as Package[],
|
||||
unsupported: [] as Package[] | undefined,
|
||||
};
|
||||
grouped.unsupported = [];
|
||||
for (const pkg of searchedPkgs) {
|
||||
const loc = pkg.loc;
|
||||
if (!loc || !loc.status || typeof loc.status === 'string') {
|
||||
grouped.unsupported.push(pkg);
|
||||
} else {
|
||||
if (
|
||||
loc.status.OK[0] &
|
||||
(Feature.GameDLL | Feature.Mempatcher | Feature.AmdDLL)
|
||||
) {
|
||||
grouped.native.push(pkg);
|
||||
} else if (loc.status.OK[0] & Feature.Mod) {
|
||||
grouped.standard.push(pkg);
|
||||
}
|
||||
if (
|
||||
loc.status.OK[0] &
|
||||
(Feature.AMNet |
|
||||
Feature.Aime |
|
||||
Feature.ChuniIO |
|
||||
Feature.ChusanHook |
|
||||
Feature.Mu3IO |
|
||||
Feature.Mu3Hook)
|
||||
) {
|
||||
grouped.segatools.push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (grouped.unsupported.length === 0) {
|
||||
delete grouped.unsupported;
|
||||
}
|
||||
} else {
|
||||
grouped = {} as { [key: string]: Package[] };
|
||||
for (const pkg of searchedPkgs) {
|
||||
for (const cat of pkg.rmt?.categories ?? []) {
|
||||
if (client.hiddenCategories.includes(cat)) {
|
||||
continue;
|
||||
}
|
||||
if (!(cat in grouped)) {
|
||||
grouped[cat] = [] as Package[];
|
||||
}
|
||||
grouped[cat].push(pkg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let res: [string, Package[]][] = [];
|
||||
for (const [k, v] of Object.entries(grouped)) {
|
||||
if (v !== undefined) {
|
||||
res.push([k, v]);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
client.pkgListMode === 'namespace' ||
|
||||
client.pkgListMode === 'category'
|
||||
) {
|
||||
res.sort((a, b) => `${a[0]}`.localeCompare(`${b[0]}`));
|
||||
} else if (client.pkgListMode === 'type') {
|
||||
for (const entry of res) {
|
||||
entry[0] = t(`pkglist.${entry[0]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
@ -56,12 +144,169 @@ const missing = computed(() => {
|
||||
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||
});
|
||||
|
||||
const emptyVisible = ref(false);
|
||||
setTimeout(() => (emptyVisible.value = true), 500);
|
||||
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
|
||||
@ -84,10 +329,28 @@ setTimeout(() => (emptyVisible.value = true), 500);
|
||||
/>
|
||||
</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 && emptyVisible === true" class="text-3xl fadein">
|
||||
∅
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="css" scoped>
|
||||
.creation-dialog h2 {
|
||||
margin-top: 0.6em;
|
||||
margin-bottom: 0.4em;
|
||||
font-size: 110%;
|
||||
}
|
||||
</style>
|
||||
|
@ -7,7 +7,7 @@ import LinkButton from './LinkButton.vue';
|
||||
import ModTitlecard from './ModTitlecard.vue';
|
||||
import UpdateButton from './UpdateButton.vue';
|
||||
import { invoke } from '../invoke';
|
||||
import { usePkgStore, usePrfStore } from '../stores';
|
||||
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
|
||||
import { Feature, Package } from '../types';
|
||||
import { hasFeature } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
@ -16,6 +16,7 @@ const { t } = useI18n();
|
||||
|
||||
const prf = usePrfStore();
|
||||
const pkgs = usePkgStore();
|
||||
const client = useClientStore();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
@ -39,7 +40,13 @@ if (unsupported.value === true && model.value === true) {
|
||||
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<ModTitlecard show-version show-icon show-description :pkg="pkg" />
|
||||
<ModTitlecard
|
||||
show-version
|
||||
show-icon
|
||||
show-description
|
||||
:show-namespace="client.pkgListMode !== 'namespace'"
|
||||
:pkg="pkg"
|
||||
/>
|
||||
<UpdateButton :pkg="pkg" />
|
||||
<span v-tooltip="unsupported && t('store.incompatible')">
|
||||
<ToggleSwitch
|
||||
|
@ -1,9 +1,14 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { ref } from 'vue';
|
||||
import Chip from 'primevue/chip';
|
||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||
import { listen } from '@tauri-apps/api/event';
|
||||
import { invoke } from '../invoke';
|
||||
import { Feature, Package } from '../types';
|
||||
import { hasFeature, needsUpdate } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
pkg: Object as () => Package,
|
||||
@ -14,23 +19,34 @@ const props = defineProps({
|
||||
showIcon: Boolean,
|
||||
});
|
||||
|
||||
const iconSrc = computed(() => {
|
||||
const icon = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
||||
const icon = ref('/no-icon.png');
|
||||
|
||||
if (icon === undefined) {
|
||||
return '';
|
||||
} else if (icon.startsWith('https://')) {
|
||||
return icon;
|
||||
const reloadIcons = async () => {
|
||||
const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
||||
|
||||
if (src === undefined) {
|
||||
icon.value = '/no-icon.png';
|
||||
} else if (src.startsWith('https://')) {
|
||||
icon.value = src;
|
||||
} else {
|
||||
return convertFileSrc(icon);
|
||||
const convt = convertFileSrc(src);
|
||||
if (await invoke('file_exists', { path: src })) {
|
||||
icon.value = convt;
|
||||
} else {
|
||||
icon.value = '/no-icon.png';
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
reloadIcons();
|
||||
|
||||
listen('reload-icons', reloadIcons);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<img
|
||||
v-if="showIcon"
|
||||
:src="iconSrc"
|
||||
:src="icon"
|
||||
class="self-center rounded-sm"
|
||||
width="32px"
|
||||
height="32px"
|
||||
@ -89,7 +105,12 @@ const iconSrc = computed(() => {
|
||||
v-if="showNamespace && pkg?.namespace"
|
||||
class="text-sm opacity-75"
|
||||
>
|
||||
by {{ pkg.namespace }}
|
||||
{{
|
||||
t('by', { namespace: pkg.namespace }).replaceAll(
|
||||
' ',
|
||||
' '
|
||||
)
|
||||
}}
|
||||
</span>
|
||||
<span class="m-2">
|
||||
<span
|
||||
|
@ -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)"
|
||||
/>
|
||||
|
@ -8,6 +8,7 @@ export default {
|
||||
next: 'Next',
|
||||
skip: 'Skip',
|
||||
close: 'Close',
|
||||
by: 'by {namespace}',
|
||||
start: {
|
||||
failed: 'Start check failed',
|
||||
accept: 'Run anyway',
|
||||
@ -46,16 +47,41 @@ export default {
|
||||
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',
|
||||
installed: 'Show installed',
|
||||
deprecated: 'Show deprecated',
|
||||
nsfw: 'Show NSFW',
|
||||
incompatible: 'This package is currently incompatible with STARTLINER.',
|
||||
missing: 'Missing',
|
||||
|
||||
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:
|
||||
@ -163,6 +189,8 @@ 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',
|
||||
|
@ -8,6 +8,7 @@ export default {
|
||||
next: 'Dalej',
|
||||
skip: 'Pomiń',
|
||||
close: 'Zamknij',
|
||||
by: 'od {namespace}',
|
||||
start: {
|
||||
failed: 'Uruchomienie nie powiodło się',
|
||||
accept: 'Uruchom mimo to',
|
||||
@ -46,6 +47,19 @@ export default {
|
||||
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',
|
||||
@ -53,10 +67,21 @@ export default {
|
||||
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.',
|
||||
missing: 'Niedostępne',
|
||||
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:
|
||||
@ -204,6 +229,8 @@ export default {
|
||||
'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',
|
||||
|
@ -375,6 +375,9 @@ export const useClientStore = defineStore('client', () => {
|
||||
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;
|
||||
@ -445,6 +448,14 @@ export const useClientStore = defineStore('client', () => {
|
||||
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) {
|
||||
@ -484,6 +495,8 @@ export const useClientStore = defineStore('client', () => {
|
||||
onboarded: onboarded.value,
|
||||
locale: locale.value,
|
||||
currentTab: currentTab.value,
|
||||
pkgListMode: pkgListMode.value,
|
||||
hiddenCategories: hiddenCategories.value,
|
||||
})
|
||||
);
|
||||
};
|
||||
@ -560,6 +573,8 @@ export const useClientStore = defineStore('client', () => {
|
||||
timeout,
|
||||
scaleModel,
|
||||
currentTab,
|
||||
pkgListMode,
|
||||
hiddenCategories,
|
||||
_scaleValue,
|
||||
scaleValue,
|
||||
load,
|
||||
|
Reference in New Issue
Block a user