feat: add package creator
This commit is contained in:
@ -1,3 +1,8 @@
|
|||||||
|
## 0.17.0
|
||||||
|
|
||||||
|
- Added a package creation prompt
|
||||||
|
- Added a default package icon
|
||||||
|
|
||||||
## 0.16.0
|
## 0.16.0
|
||||||
|
|
||||||
- Fixed the clear cache button not working
|
- 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 ini::Ini;
|
||||||
use log;
|
use log;
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tauri::{AppHandle, Manager, State};
|
use tauri::{AppHandle, Manager, State};
|
||||||
use crate::model::config::GlobalConfigField;
|
use crate::model::config::GlobalConfigField;
|
||||||
|
use crate::model::local::PackageManifest;
|
||||||
use crate::model::misc::Game;
|
use crate::model::misc::Game;
|
||||||
use crate::model::patch::Patch;
|
use crate::model::patch::Patch;
|
||||||
use crate::modules::package::prepare_dlls;
|
use crate::modules::package::prepare_dlls;
|
||||||
@ -166,6 +167,65 @@ pub async fn toggle_package(state: State<'_, tokio::sync::Mutex<AppData>>, key:
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn create_package(
|
||||||
|
name: String,
|
||||||
|
description: String,
|
||||||
|
website: String,
|
||||||
|
r#type: String,
|
||||||
|
games: Vec<Game>
|
||||||
|
) -> Result<(), String> {
|
||||||
|
log::debug!("invoke: create_package");
|
||||||
|
|
||||||
|
let dir = util::pkg_dir_of("local", &name);
|
||||||
|
|
||||||
|
if dir.exists() {
|
||||||
|
return Err("Package already exists".to_owned());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut installers = Vec::new();
|
||||||
|
|
||||||
|
if r#type == "segatools" {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(
|
||||||
|
"identifier".to_owned(),
|
||||||
|
serde_json::Value::String("segatools".to_owned())
|
||||||
|
);
|
||||||
|
installers.push(map);
|
||||||
|
} else if r#type == "native" {
|
||||||
|
let mut map = BTreeMap::new();
|
||||||
|
map.insert(
|
||||||
|
"identifier".to_owned(),
|
||||||
|
serde_json::Value::String("native_mod".to_owned())
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"dll-game".to_owned(),
|
||||||
|
serde_json::Value::String("some.dll".to_owned())
|
||||||
|
);
|
||||||
|
map.insert(
|
||||||
|
"dll-amdaemon".to_owned(),
|
||||||
|
serde_json::Value::String("another.dll".to_owned())
|
||||||
|
);
|
||||||
|
installers.push(map);
|
||||||
|
}
|
||||||
|
|
||||||
|
let manifest = PackageManifest {
|
||||||
|
name,
|
||||||
|
version_number: "1.0.0".to_owned(),
|
||||||
|
description,
|
||||||
|
website_url: website,
|
||||||
|
dependencies: BTreeSet::new(),
|
||||||
|
installers,
|
||||||
|
games: Some(games)
|
||||||
|
};
|
||||||
|
|
||||||
|
std::fs::create_dir(&dir).map_err(|e| e.to_string())?;
|
||||||
|
let json = serde_json::to_string_pretty(&manifest).map_err(|e| e.to_string())?;
|
||||||
|
std::fs::write(dir.join("manifest.json"), json).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
|
pub async fn reload_all_packages(state: State<'_, tokio::sync::Mutex<AppData>>) -> Result<(), String> {
|
||||||
log::debug!("invoke: reload_all_packages");
|
log::debug!("invoke: reload_all_packages");
|
||||||
|
@ -193,6 +193,7 @@ pub async fn run(_args: Vec<String>) {
|
|||||||
cmd::install_package,
|
cmd::install_package,
|
||||||
cmd::delete_package,
|
cmd::delete_package,
|
||||||
cmd::toggle_package,
|
cmd::toggle_package,
|
||||||
|
cmd::create_package,
|
||||||
|
|
||||||
cmd::list_profiles,
|
cmd::list_profiles,
|
||||||
cmd::init_profile,
|
cmd::init_profile,
|
||||||
|
@ -6,10 +6,11 @@ use super::misc::Game;
|
|||||||
|
|
||||||
// manifest.json
|
// manifest.json
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct PackageManifest {
|
pub struct PackageManifest {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub version_number: String,
|
pub version_number: String,
|
||||||
|
pub website_url: String,
|
||||||
pub description: String,
|
pub description: String,
|
||||||
pub dependencies: BTreeSet<PkgKeyVersion>,
|
pub dependencies: BTreeSet<PkgKeyVersion>,
|
||||||
|
|
||||||
|
@ -63,9 +63,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Ongeki
|
// Ongeki
|
||||||
filename: "amdaemon.exe",
|
filename: "amdaemon.exe",
|
||||||
version: "46d47eab",
|
version: "46d47eab",
|
||||||
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
|
sha256: '962C76331306D0839AFD40808EA99D83E651D39C4708C448ADE0C77E8BC0A1B0',
|
||||||
patches: [
|
patches: [
|
||||||
{
|
{
|
||||||
id: 'standard-localhost',
|
id: 'standard-localhost',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "STARTLINER",
|
"productName": "STARTLINER",
|
||||||
"version": "0.16.0",
|
"version": "0.17.0",
|
||||||
"identifier": "zip.patafour.startliner",
|
"identifier": "zip.patafour.startliner",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun run dev",
|
"beforeDevCommand": "bun run dev",
|
||||||
|
@ -1,12 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Ref, computed, ref } from 'vue';
|
import { Ref, computed, ref } from 'vue';
|
||||||
import Button from 'primevue/button';
|
import Button from 'primevue/button';
|
||||||
|
import Dialog from 'primevue/dialog';
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
|
import InputText from 'primevue/inputtext';
|
||||||
|
import SelectButton from 'primevue/selectbutton';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import ModListEntry from './ModListEntry.vue';
|
import ModListEntry from './ModListEntry.vue';
|
||||||
import ModTitlecard from './ModTitlecard.vue';
|
import ModTitlecard from './ModTitlecard.vue';
|
||||||
import { invoke } from '../invoke';
|
import { invoke } from '../invoke';
|
||||||
import { usePkgStore, usePrfStore } from '../stores';
|
import { useClientStore, usePkgStore, usePrfStore } from '../stores';
|
||||||
import { Package } from '../types';
|
import { Game, Package } from '../types';
|
||||||
import { pkgKey } from '../util';
|
import { pkgKey } from '../util';
|
||||||
import { useI18n } from 'vue-i18n';
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
@ -17,38 +21,47 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const pkgs = usePkgStore();
|
const pkgs = usePkgStore();
|
||||||
|
const client = useClientStore();
|
||||||
const prf = usePrfStore();
|
const prf = usePrfStore();
|
||||||
const empty = ref(false);
|
|
||||||
const gameSublist: Ref<string[]> = ref([]);
|
const gameSublist: Ref<string[]> = ref([]);
|
||||||
|
|
||||||
invoke('get_game_packages', {
|
const loadPackages = () => {
|
||||||
game: prf.current?.meta.game ?? null,
|
invoke('get_game_packages', {
|
||||||
}).then((list) => {
|
game: prf.current?.meta.game ?? null,
|
||||||
gameSublist.value = list as string[];
|
}).then((list) => {
|
||||||
});
|
gameSublist.value = list as string[];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
loadPackages();
|
||||||
|
|
||||||
const group = computed(() => {
|
const group = computed(() => {
|
||||||
const res = Object.assign(
|
const grouped = Object.groupBy(
|
||||||
{},
|
pkgs.allLocal
|
||||||
Object.groupBy(
|
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
||||||
pkgs.allLocal
|
.filter(
|
||||||
.filter((p) => gameSublist.value.includes(pkgKey(p)))
|
(p) =>
|
||||||
.filter(
|
props.search === undefined ||
|
||||||
(p) =>
|
p.name.toLowerCase().includes(props.search.toLowerCase()) ||
|
||||||
props.search === undefined ||
|
p.namespace
|
||||||
p.name
|
.toLowerCase()
|
||||||
.toLowerCase()
|
.includes(props.search.toLowerCase())
|
||||||
.includes(props.search.toLowerCase()) ||
|
),
|
||||||
p.namespace
|
({ namespace }) => 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;
|
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;
|
return res;
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -56,11 +69,132 @@ const missing = computed(() => {
|
|||||||
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
return prf.current?.data.mods.filter((m) => !pkgs.hasLocal(m)) ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const emptyVisible = ref(false);
|
const dialogVisible = ref(false);
|
||||||
setTimeout(() => (emptyVisible.value = true), 500);
|
|
||||||
|
const defaultModel = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
website: '',
|
||||||
|
type: 'rainy',
|
||||||
|
games: [] as string[],
|
||||||
|
};
|
||||||
|
|
||||||
|
const creatorModel = ref({ ...defaultModel });
|
||||||
|
|
||||||
|
const gameModel = (game: Game) =>
|
||||||
|
computed({
|
||||||
|
get() {
|
||||||
|
return (creatorModel.value.games as string[]).includes(game);
|
||||||
|
},
|
||||||
|
set(v: boolean) {
|
||||||
|
creatorModel.value.games = creatorModel.value.games.filter(
|
||||||
|
(g) => g !== game
|
||||||
|
);
|
||||||
|
if (v) {
|
||||||
|
creatorModel.value.games.push(game);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const gameModelOngeki = gameModel('ongeki');
|
||||||
|
const gameModelChunithm = gameModel('chunithm');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<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">
|
<Fieldset :legend="t('store.missing')" v-if="(missing?.length ?? 0) > 0">
|
||||||
<div class="flex items-center" v-for="p in missing">
|
<div class="flex items-center" v-for="p in missing">
|
||||||
<ModTitlecard
|
<ModTitlecard
|
||||||
@ -84,10 +218,27 @@ setTimeout(() => (emptyVisible.value = true), 500);
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Fieldset>
|
</Fieldset>
|
||||||
<Fieldset v-for="(namespace, key) in group" :legend="key.toString()">
|
<Fieldset v-for="[namespace, pkgs] in group" :legend="namespace">
|
||||||
<ModListEntry v-for="p in namespace" :pkg="p" />
|
<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>
|
</Fieldset>
|
||||||
<div v-if="empty === true && emptyVisible === true" class="text-3xl fadein">
|
|
||||||
∅
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.creation-dialog h2 {
|
||||||
|
margin-top: 0.6em;
|
||||||
|
margin-bottom: 0.4em;
|
||||||
|
font-size: 110%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { ref } from 'vue';
|
||||||
import Chip from 'primevue/chip';
|
import Chip from 'primevue/chip';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||||
|
import { invoke } from '../invoke';
|
||||||
import { Feature, Package } from '../types';
|
import { Feature, Package } from '../types';
|
||||||
import { hasFeature, needsUpdate } from '../util';
|
import { hasFeature, needsUpdate } from '../util';
|
||||||
|
|
||||||
@ -14,23 +15,30 @@ const props = defineProps({
|
|||||||
showIcon: Boolean,
|
showIcon: Boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconSrc = computed(() => {
|
const icon = ref('/no-icon.png');
|
||||||
const icon = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
|
||||||
|
|
||||||
if (icon === undefined) {
|
(async () => {
|
||||||
return '';
|
const src = props.pkg?.loc?.icon ?? props.pkg?.rmt?.icon;
|
||||||
} else if (icon.startsWith('https://')) {
|
|
||||||
return icon;
|
if (src === undefined) {
|
||||||
|
icon.value = '/no-icon.png';
|
||||||
|
} else if (src.startsWith('https://')) {
|
||||||
|
icon.value = src;
|
||||||
} else {
|
} else {
|
||||||
return convertFileSrc(icon);
|
const convt = convertFileSrc(src);
|
||||||
|
if (await invoke('file_exists', { path: src })) {
|
||||||
|
icon.value = convt;
|
||||||
|
} else {
|
||||||
|
icon.value = '/no-icon.png';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<img
|
<img
|
||||||
v-if="showIcon"
|
v-if="showIcon"
|
||||||
:src="iconSrc"
|
:src="icon"
|
||||||
class="self-center rounded-sm"
|
class="self-center rounded-sm"
|
||||||
width="32px"
|
width="32px"
|
||||||
height="32px"
|
height="32px"
|
||||||
|
@ -46,6 +46,19 @@ export default {
|
|||||||
exportTemplate: 'Export template',
|
exportTemplate: 'Export template',
|
||||||
export: 'Export',
|
export: 'Export',
|
||||||
},
|
},
|
||||||
|
creator: {
|
||||||
|
header: 'Package creator',
|
||||||
|
basic: 'Basic information',
|
||||||
|
name: 'Name',
|
||||||
|
description: 'Description',
|
||||||
|
website: 'Website',
|
||||||
|
type: 'Package type',
|
||||||
|
rainy: 'Standard',
|
||||||
|
segatools: 'Segatools',
|
||||||
|
native: 'Native',
|
||||||
|
games: 'Games',
|
||||||
|
packageFormat: 'Package format spec',
|
||||||
|
},
|
||||||
store: {
|
store: {
|
||||||
installRecommended: 'Install recommended packages',
|
installRecommended: 'Install recommended packages',
|
||||||
installed: 'Show installed',
|
installed: 'Show installed',
|
||||||
|
@ -46,6 +46,19 @@ export default {
|
|||||||
exportTemplate: 'Eksportuj szablon',
|
exportTemplate: 'Eksportuj szablon',
|
||||||
export: 'Eksportuj',
|
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: {
|
store: {
|
||||||
installRecommended: 'Dodaj zalecane pakiety',
|
installRecommended: 'Dodaj zalecane pakiety',
|
||||||
installed: 'Pokaż zainstalowane',
|
installed: 'Pokaż zainstalowane',
|
||||||
|
Reference in New Issue
Block a user