forked from akanyan/STARTLINER
feat: categories and option search
This commit is contained in:
@ -9,6 +9,8 @@ pub struct V1Package {
|
|||||||
pub owner: String,
|
pub owner: String,
|
||||||
pub package_url: String,
|
pub package_url: String,
|
||||||
pub is_deprecated: bool,
|
pub is_deprecated: bool,
|
||||||
|
pub has_nsfw_content: bool,
|
||||||
|
pub categories: Vec<String>,
|
||||||
pub versions: Vec<V1Version>,
|
pub versions: Vec<V1Version>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +49,9 @@ pub struct Remote {
|
|||||||
pub package_url: String,
|
pub package_url: String,
|
||||||
pub download_url: String,
|
pub download_url: String,
|
||||||
pub deprecated: bool,
|
pub deprecated: bool,
|
||||||
pub dependencies: BTreeSet<PkgKey>
|
pub nsfw: bool,
|
||||||
|
pub categories: Vec<String>,
|
||||||
|
pub dependencies: BTreeSet<PkgKey>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Package {
|
impl Package {
|
||||||
@ -70,7 +72,9 @@ impl Package {
|
|||||||
package_url: p.package_url,
|
package_url: p.package_url,
|
||||||
download_url: v.download_url,
|
download_url: v.download_url,
|
||||||
deprecated: p.is_deprecated,
|
deprecated: p.is_deprecated,
|
||||||
|
nsfw: p.has_nsfw_content,
|
||||||
version: v.version_number,
|
version: v.version_number,
|
||||||
|
categories: p.categories,
|
||||||
dependencies: Self::sanitize_deps(v.dependencies)
|
dependencies: Self::sanitize_deps(v.dependencies)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -24,8 +24,7 @@ const general = useGeneralStore();
|
|||||||
pkg.setupListeners();
|
pkg.setupListeners();
|
||||||
|
|
||||||
const currentTab: Ref<string | number> = ref(3);
|
const currentTab: Ref<string | number> = ref(3);
|
||||||
const searchPkg = ref('');
|
const pkgSearchTerm = ref('');
|
||||||
const searchCfg = ref('');
|
|
||||||
|
|
||||||
const isProfileDisabled = computed(() => prf.current === null);
|
const isProfileDisabled = computed(() => prf.current === null);
|
||||||
|
|
||||||
@ -34,7 +33,7 @@ onMounted(async () => {
|
|||||||
general.dirs = d as Dirs;
|
general.dirs = d as Dirs;
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetch_promise = pkg.fetch();
|
const fetch_promise = pkg.fetch(true);
|
||||||
|
|
||||||
await Promise.all([prf.reloadList(), prf.reload()]);
|
await Promise.all([prf.reloadList(), prf.reload()]);
|
||||||
|
|
||||||
@ -65,7 +64,10 @@ onMounted(async () => {
|
|||||||
<Tab :disabled="isProfileDisabled" :value="0"
|
<Tab :disabled="isProfileDisabled" :value="0"
|
||||||
><div class="pi pi-list-check"></div
|
><div class="pi pi-list-check"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :disabled="isProfileDisabled" :value="1"
|
<Tab
|
||||||
|
v-if="!pkg.offline"
|
||||||
|
:disabled="isProfileDisabled"
|
||||||
|
:value="1"
|
||||||
><div class="pi pi-download"></div
|
><div class="pi pi-download"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<Tab :disabled="isProfileDisabled" :value="2"
|
<Tab :disabled="isProfileDisabled" :value="2"
|
||||||
@ -75,6 +77,7 @@ onMounted(async () => {
|
|||||||
><div class="pi pi-question-circle"></div
|
><div class="pi pi-question-circle"></div
|
||||||
></Tab>
|
></Tab>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
|
<div class="flex gap-4">
|
||||||
<div class="flex" v-if="currentTab !== 3">
|
<div class="flex" v-if="currentTab !== 3">
|
||||||
<InputIcon class="self-center mr-2">
|
<InputIcon class="self-center mr-2">
|
||||||
<i class="pi pi-search" />
|
<i class="pi pi-search" />
|
||||||
@ -84,14 +87,22 @@ onMounted(async () => {
|
|||||||
class="self-center"
|
class="self-center"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
v-model="searchCfg"
|
v-model="general.cfgSearchTerm"
|
||||||
/>
|
/>
|
||||||
<InputText
|
<InputText
|
||||||
v-else
|
v-else
|
||||||
class="self-center"
|
class="self-center"
|
||||||
size="small"
|
size="small"
|
||||||
placeholder="Search"
|
placeholder="Search"
|
||||||
v-model="searchPkg"
|
v-model="pkgSearchTerm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
v-if="pkg.offline"
|
||||||
|
class="shrink self-center"
|
||||||
|
icon="pi pi-sync"
|
||||||
|
size="small"
|
||||||
|
@click="pkg.fetch(false)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="grow"></div>
|
<div class="grow"></div>
|
||||||
@ -100,10 +111,10 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
<TabPanels class="w-full grow mt-[3rem]">
|
<TabPanels class="w-full grow mt-[3rem]">
|
||||||
<TabPanel :value="0">
|
<TabPanel :value="0">
|
||||||
<ModList :search="searchPkg" />
|
<ModList :search="pkgSearchTerm" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel :value="1">
|
<TabPanel :value="1">
|
||||||
<ModStore :search="searchPkg" />
|
<ModStore :search="pkgSearchTerm" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel :value="2">
|
<TabPanel :value="2">
|
||||||
<OptionList />
|
<OptionList />
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
|
import Divider from 'primevue/divider';
|
||||||
|
import MultiSelect from 'primevue/multiselect';
|
||||||
|
import ToggleSwitch from 'primevue/toggleswitch';
|
||||||
import ModStoreEntry from './ModStoreEntry.vue';
|
import ModStoreEntry from './ModStoreEntry.vue';
|
||||||
import { usePkgStore } from '../stores';
|
import { usePkgStore } from '../stores';
|
||||||
|
|
||||||
@ -28,6 +31,37 @@ const list = () => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<div class="flex gap-4 items-center">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="text-amber-400 grow">Show deprecated</div>
|
||||||
|
<ToggleSwitch v-model="pkgs.showDeprecated" />
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<div class="text-red-400 grow">Show NSFW</div>
|
||||||
|
<ToggleSwitch v-model="pkgs.showNSFW" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col gap-2 grow">
|
||||||
|
<MultiSelect
|
||||||
|
size="small"
|
||||||
|
:showToggleAll="false"
|
||||||
|
placeholder="Include categories"
|
||||||
|
v-model="pkgs.includeCategories"
|
||||||
|
:options="[...pkgs.availableCategories]"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
<MultiSelect
|
||||||
|
size="small"
|
||||||
|
:showToggleAll="false"
|
||||||
|
placeholder="Exclude categories"
|
||||||
|
v-model="pkgs.excludeCategories"
|
||||||
|
:options="[...pkgs.availableCategories]"
|
||||||
|
class="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Divider />
|
||||||
<div v-for="p in list()" class="flex flex-row">
|
<div v-for="p in list()" class="flex flex-row">
|
||||||
<ModStoreEntry :pkg="p" />
|
<ModStoreEntry :pkg="p" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,7 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ModTitlecard :pkg="pkg" showNamespace />
|
<ModTitlecard :pkg="pkg" show-namespace show-categories />
|
||||||
<InstallButton :pkg="pkg" />
|
<InstallButton :pkg="pkg" />
|
||||||
<Button
|
<Button
|
||||||
rounded
|
rounded
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import Chip from 'primevue/chip';
|
||||||
import { convertFileSrc } from '@tauri-apps/api/core';
|
import { convertFileSrc } from '@tauri-apps/api/core';
|
||||||
import { Package } from '../types';
|
import { Package } from '../types';
|
||||||
import { needsUpdate } from '../util';
|
import { needsUpdate } from '../util';
|
||||||
@ -7,6 +8,7 @@ const props = defineProps({
|
|||||||
pkg: Object as () => Package,
|
pkg: Object as () => Package,
|
||||||
showNamespace: Boolean,
|
showNamespace: Boolean,
|
||||||
showVersion: Boolean,
|
showVersion: Boolean,
|
||||||
|
showCategories: Boolean,
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconSrc = () => {
|
const iconSrc = () => {
|
||||||
@ -34,6 +36,18 @@ const iconSrc = () => {
|
|||||||
<span class="text-lg">
|
<span class="text-lg">
|
||||||
{{ pkg?.name ?? 'Untitled' }}
|
{{ pkg?.name ?? 'Untitled' }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="pkg?.rmt?.deprecated"
|
||||||
|
v-tooltip="'deprecated'"
|
||||||
|
class="pi pi-exclamation-circle ml-1 text-amber-400"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="pkg?.rmt?.nsfw"
|
||||||
|
v-tooltip="'NSFW'"
|
||||||
|
class="pi pi-exclamation-triangle ml-1 text-red-400"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="showNamespace && pkg?.namespace"
|
v-if="showNamespace && pkg?.namespace"
|
||||||
class="text-sm opacity-75"
|
class="text-sm opacity-75"
|
||||||
@ -58,5 +72,17 @@ const iconSrc = () => {
|
|||||||
<div class="text-sm opacity-75">
|
<div class="text-sm opacity-75">
|
||||||
{{ pkg?.description ?? 'No description' }}
|
{{ pkg?.description ?? 'No description' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="showCategories" class="mt-1 flex gap-1">
|
||||||
|
<span class="text-xs" v-for="c in pkg?.rmt?.categories"
|
||||||
|
><Chip :label="c"
|
||||||
|
/></span>
|
||||||
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.p-chip {
|
||||||
|
padding: 0.4rem !important;
|
||||||
|
font-size: 0.66rem !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Fieldset from 'primevue/fieldset';
|
import Fieldset from 'primevue/fieldset';
|
||||||
|
import { useGeneralStore } from '../stores';
|
||||||
|
|
||||||
|
const general = useGeneralStore();
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
@ -7,7 +10,11 @@ defineProps({
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Fieldset :legend="title" :toggleable="true">
|
<Fieldset
|
||||||
|
:legend="title"
|
||||||
|
:toggleable="true"
|
||||||
|
v-show="general.cfgCategories.has(title ?? '')"
|
||||||
|
>
|
||||||
<div class="flex w-full flex-col gap-1">
|
<div class="flex w-full flex-col gap-1">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
defineProps({
|
import { computed, getCurrentInstance } from 'vue';
|
||||||
|
import { useGeneralStore } from '../stores';
|
||||||
|
|
||||||
|
const general = useGeneralStore();
|
||||||
|
const category = getCurrentInstance()?.parent?.parent?.parent?.parent; // yes indeed
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
title: String,
|
title: String,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const searched = computed(() => {
|
||||||
|
const term = general.cfgSearchTerm.toLowerCase();
|
||||||
|
const categoryTitle = category?.props?.title as string | undefined;
|
||||||
|
const res =
|
||||||
|
props.title?.toLowerCase().includes(term) ||
|
||||||
|
categoryTitle?.toLowerCase().includes(term);
|
||||||
|
if (res === true && categoryTitle !== undefined) {
|
||||||
|
general.cfgCategories.add(categoryTitle);
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-row w-full p-2 gap-2 items-center">
|
<div v-if="searched" class="flex flex-row w-full p-2 gap-2 items-center">
|
||||||
<div class="grow">{{ title }}</div>
|
<div class="grow">{{ title }}</div>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
@ -24,3 +24,16 @@ export const invoke = async <T>(
|
|||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const invoke_nopopup = async <T>(
|
||||||
|
cmd: string,
|
||||||
|
args?: InvokeArgs,
|
||||||
|
options?: InvokeOptions
|
||||||
|
): Promise<T> => {
|
||||||
|
try {
|
||||||
|
return await real_invoke(cmd, args, options);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error(`Error invoking ${cmd}: ${e}`);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
@ -2,7 +2,7 @@ import { Ref, computed, ref, watchEffect } from 'vue';
|
|||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { listen } from '@tauri-apps/api/event';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
import * as path from '@tauri-apps/api/path';
|
import * as path from '@tauri-apps/api/path';
|
||||||
import { invoke } from './invoke';
|
import { invoke, invoke_nopopup } from './invoke';
|
||||||
import { Dirs, Game, Package, Profile, ProfileMeta } from './types';
|
import { Dirs, Game, Package, Profile, ProfileMeta } from './types';
|
||||||
import { changePrimaryColor, pkgKey } from './util';
|
import { changePrimaryColor, pkgKey } from './util';
|
||||||
|
|
||||||
@ -12,6 +12,17 @@ type InstallStatus = {
|
|||||||
|
|
||||||
export const useGeneralStore = defineStore('general', () => {
|
export const useGeneralStore = defineStore('general', () => {
|
||||||
const dirs: Ref<Dirs | null> = ref(null);
|
const dirs: Ref<Dirs | null> = ref(null);
|
||||||
|
const cfgCategories = ref(new Set<string>());
|
||||||
|
const _cfgSearchTerm = ref('');
|
||||||
|
const cfgSearchTerm = computed({
|
||||||
|
set(value: string) {
|
||||||
|
cfgCategories.value.clear();
|
||||||
|
_cfgSearchTerm.value = value;
|
||||||
|
},
|
||||||
|
get() {
|
||||||
|
return _cfgSearchTerm.value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const configDir = computed(() => {
|
const configDir = computed(() => {
|
||||||
if (dirs.value === null) {
|
if (dirs.value === null) {
|
||||||
@ -32,13 +43,35 @@ export const useGeneralStore = defineStore('general', () => {
|
|||||||
return dirs.value.cache_dir;
|
return dirs.value.cache_dir;
|
||||||
});
|
});
|
||||||
|
|
||||||
return { dirs, configDir, dataDir, cacheDir };
|
return {
|
||||||
|
dirs,
|
||||||
|
_cfgSearchTerm,
|
||||||
|
cfgSearchTerm,
|
||||||
|
cfgCategories,
|
||||||
|
configDir,
|
||||||
|
dataDir,
|
||||||
|
cacheDir,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usePkgStore = defineStore('pkg', {
|
export const usePkgStore = defineStore('pkg', {
|
||||||
state: (): { pkg: { [key: string]: Package } } => {
|
state: (): {
|
||||||
|
pkg: { [key: string]: Package };
|
||||||
|
offline: boolean;
|
||||||
|
showDeprecated: boolean;
|
||||||
|
showNSFW: boolean;
|
||||||
|
availableCategories: Set<string>;
|
||||||
|
includeCategories: string[];
|
||||||
|
excludeCategories: string[];
|
||||||
|
} => {
|
||||||
return {
|
return {
|
||||||
pkg: {},
|
pkg: {},
|
||||||
|
offline: false,
|
||||||
|
showDeprecated: false,
|
||||||
|
showNSFW: false,
|
||||||
|
availableCategories: new Set(),
|
||||||
|
includeCategories: [],
|
||||||
|
excludeCategories: [],
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -48,7 +81,21 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
all: (state) => Object.values(state),
|
all: (state) => Object.values(state),
|
||||||
allLocal: (state) =>
|
allLocal: (state) =>
|
||||||
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Mod'),
|
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Mod'),
|
||||||
allRemote: (state) => Object.values(state.pkg).filter((p) => p.rmt),
|
allRemote: (state) =>
|
||||||
|
Object.values(state.pkg).filter(
|
||||||
|
(p) =>
|
||||||
|
p.rmt !== null &&
|
||||||
|
(state.showDeprecated || !p.rmt.deprecated) &&
|
||||||
|
(state.showNSFW || !p.rmt.nsfw) &&
|
||||||
|
(state.includeCategories.length === 0 ||
|
||||||
|
p.rmt.categories.some((c) =>
|
||||||
|
state.includeCategories.includes(c)
|
||||||
|
)) &&
|
||||||
|
(state.excludeCategories.length === 0 ||
|
||||||
|
p.rmt.categories.every(
|
||||||
|
(c) => !state.excludeCategories.includes(c)
|
||||||
|
))
|
||||||
|
),
|
||||||
hooks: (state) =>
|
hooks: (state) =>
|
||||||
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Hook'),
|
Object.values(state.pkg).filter((p) => p.loc?.kind === 'Hook'),
|
||||||
ios: (state) =>
|
ios: (state) =>
|
||||||
@ -74,6 +121,7 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
const data = (await invoke('get_all_packages')) as {
|
const data = (await invoke('get_all_packages')) as {
|
||||||
[key: string]: Package;
|
[key: string]: Package;
|
||||||
};
|
};
|
||||||
|
this.availableCategories.clear();
|
||||||
|
|
||||||
for (const [k, v] of Object.entries(data)) {
|
for (const [k, v] of Object.entries(data)) {
|
||||||
this.reloadWith(k, v);
|
this.reloadWith(k, v);
|
||||||
@ -97,10 +145,26 @@ export const usePkgStore = defineStore('pkg', {
|
|||||||
this.pkg[key].rmt = null;
|
this.pkg[key].rmt = null;
|
||||||
}
|
}
|
||||||
Object.assign(this.pkg[key], pkg);
|
Object.assign(this.pkg[key], pkg);
|
||||||
|
|
||||||
|
if (pkg.rmt !== null) {
|
||||||
|
pkg.rmt.categories.forEach((c) =>
|
||||||
|
this.availableCategories.add(c)
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
async fetch() {
|
async fetch(nopopup: boolean) {
|
||||||
|
try {
|
||||||
|
if (nopopup) {
|
||||||
|
await invoke_nopopup('fetch_listings');
|
||||||
|
} else {
|
||||||
await invoke('fetch_listings');
|
await invoke('fetch_listings');
|
||||||
|
}
|
||||||
|
this.offline = false;
|
||||||
|
} catch (e) {
|
||||||
|
this.offline = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
await this.reloadAll();
|
await this.reloadAll();
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -14,6 +14,8 @@ export interface Package {
|
|||||||
package_url: string;
|
package_url: string;
|
||||||
download_url: string;
|
download_url: string;
|
||||||
deprecated: boolean;
|
deprecated: boolean;
|
||||||
|
nsfw: boolean;
|
||||||
|
categories: string[];
|
||||||
} | null;
|
} | null;
|
||||||
js: {
|
js: {
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
|
Reference in New Issue
Block a user