forked from akanyan/STARTLINER
feat: add package creator
This commit is contained in:
@ -1,12 +1,16 @@
|
||||
<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 SelectButton from 'primevue/selectbutton';
|
||||
import ToggleSwitch from 'primevue/toggleswitch';
|
||||
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 { Game, Package } from '../types';
|
||||
import { pkgKey } from '../util';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
@ -17,38 +21,47 @@ 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 ?? null,
|
||||
}).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 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
|
||||
)
|
||||
const grouped = 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())
|
||||
),
|
||||
({ namespace }) => namespace
|
||||
);
|
||||
empty.value = Object.keys(res).length === 0;
|
||||
if (!('local' in grouped)) {
|
||||
grouped['local'] = [];
|
||||
}
|
||||
const res: [string, Package[]][] = [];
|
||||
for (const [k, v] of Object.entries(grouped)) {
|
||||
if (v !== undefined) {
|
||||
res.push([k, v]);
|
||||
}
|
||||
}
|
||||
res.sort((a, b) => {
|
||||
return a[0] === 'local' ? -1000000 : `${a[0]}`.localeCompare(`${b[0]}`);
|
||||
});
|
||||
|
||||
return res;
|
||||
});
|
||||
|
||||
@ -56,11 +69,132 @@ 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>
|
||||
<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>
|
||||
<Fieldset :legend="t('store.missing')" v-if="(missing?.length ?? 0) > 0">
|
||||
<div class="flex items-center" v-for="p in missing">
|
||||
<ModTitlecard
|
||||
@ -84,10 +218,27 @@ 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 v-for="[namespace, pkgs] in group" :legend="namespace">
|
||||
<ModListEntry v-for="p in pkgs" :pkg="p" />
|
||||
<div v-if="namespace === 'local'">
|
||||
<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)"
|
||||
/>
|
||||
</div>
|
||||
</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>
|
||||
|
Reference in New Issue
Block a user