Search fixes (#134)

* Search fixes

* Fix small instance ui

* fix javaw issue

* menu fix

* Add confirm modal for deletion

* fix build
This commit is contained in:
Geometrically
2023-06-11 15:26:25 -07:00
committed by GitHub
parent e836738887
commit 3535f0c4b4
24 changed files with 796 additions and 576 deletions

View File

@@ -156,7 +156,7 @@ pub async fn auto_install_java(java_version: u32) -> crate::Result<PathBuf> {
"Failed to extract java zip".to_string(), "Failed to extract java zip".to_string(),
)) ))
})?; })?;
emit_loading(&loading_bar, 100.0, Some("Done extracting java")).await?; emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
Ok(path Ok(path
.join( .join(
download download

View File

@@ -175,7 +175,7 @@ pub async fn emit_loading(
); );
} }
// Emit event to tauri //Emit event to tauri
#[cfg(feature = "tauri")] #[cfg(feature = "tauri")]
event_state event_state
.app .app

View File

@@ -94,51 +94,43 @@ impl Drop for LoadingBarId {
let _event = LoadingBarType::StateInit; let _event = LoadingBarType::StateInit;
let _message = "finished".to_string(); let _message = "finished".to_string();
tokio::spawn(async move { tokio::spawn(async move {
if let Ok(event_state) = crate::EventState::get().await { if let Ok(event_state) = EventState::get().await {
{ let mut bars = event_state.loading_bars.write().await;
let mut bars = event_state.loading_bars.write().await;
bars.remove(&loader_uuid); #[cfg(any(feature = "tauri", feature = "cli"))]
if let Some(bar) = bars.remove(&loader_uuid) {
#[cfg(feature = "tauri")]
{
let loader_uuid = bar.loading_bar_uuid;
let event = bar.bar_type.clone();
let fraction = bar.current / bar.total;
use tauri::Manager;
let _ = event_state.app.emit_all(
"loading",
LoadingPayload {
fraction: None,
message: "Completed".to_string(),
event,
loader_uuid,
},
);
tracing::debug!(
"Exited at {fraction} for loading bar: {:?}",
loader_uuid
);
}
// Emit event to indicatif progress bar arc
#[cfg(feature = "cli")]
{
let cli_progress_bar = bar.cli_progress_bar.clone();
cli_progress_bar.finish();
}
} }
}
});
}
}
// When Loading bar is dropped, should attempt to throw out one last event to indicate that the loading bar is done #[cfg(not(any(feature = "tauri", feature = "cli")))]
#[cfg(feature = "tauri")] bars.remove(&loader_uuid);
impl Drop for LoadingBar {
fn drop(&mut self) {
let loader_uuid = self.loading_bar_uuid;
let event = self.bar_type.clone();
let fraction = self.current / self.total;
#[cfg(feature = "cli")]
let cli_progress_bar = self.cli_progress_bar.clone();
tokio::spawn(async move {
#[cfg(feature = "tauri")]
{
use tauri::Manager;
if let Ok(event_state) = crate::EventState::get().await {
let _ = event_state.app.emit_all(
"loading",
LoadingPayload {
fraction: None,
message: "Completed".to_string(),
event,
loader_uuid,
},
);
tracing::debug!(
"Exited at {fraction} for loading bar: {:?}",
loader_uuid
);
}
}
// Emit event to indicatif progress bar arc
#[cfg(feature = "cli")]
{
cli_progress_bar.finish();
} }
}); });
} }

View File

@@ -17,7 +17,7 @@
"dayjs": "^1.11.7", "dayjs": "^1.11.7",
"floating-vue": "^2.0.0-beta.20", "floating-vue": "^2.0.0-beta.20",
"ofetch": "^1.0.1", "ofetch": "^1.0.1",
"omorphia": "^0.4.22", "omorphia": "^0.4.24",
"pinia": "^2.1.3", "pinia": "^2.1.3",
"vite-svg-loader": "^4.0.0", "vite-svg-loader": "^4.0.0",
"vue": "^3.3.4", "vue": "^3.3.4",

View File

@@ -14,8 +14,8 @@ dependencies:
specifier: ^1.0.1 specifier: ^1.0.1
version: 1.0.1 version: 1.0.1
omorphia: omorphia:
specifier: ^0.4.22 specifier: ^0.4.24
version: 0.4.22 version: 0.4.24
pinia: pinia:
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.3(vue@3.3.4) version: 2.1.3(vue@3.3.4)
@@ -1326,8 +1326,8 @@ packages:
ufo: 1.1.2 ufo: 1.1.2
dev: false dev: false
/omorphia@0.4.22: /omorphia@0.4.24:
resolution: {integrity: sha512-UzG/MqOu/q+F65RZ74oCOLHE4dm716cCkX9EtSP30s8RInd8AHuax4riFKK+ANfCCtE9zcDw4AmU7fJlUnX6Xw==} resolution: {integrity: sha512-tYhg88wkv9yfCNF8uVDkt6MIZ5WuCEezWrWJuBpHXP0X1yNGey6ICbH0LSuNuOMtqhGJEIupGEB7uV4Db9b7uQ==}
dependencies: dependencies:
dayjs: 1.11.7 dayjs: 1.11.7
floating-vue: 2.0.0-beta.20(vue@3.3.4) floating-vue: 2.0.0-beta.20(vue@3.3.4)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -40,7 +40,6 @@
}, },
"externalBin": [], "externalBin": [],
"icon": [ "icon": [
"icons/32x32.png",
"icons/128x128.png", "icons/128x128.png",
"icons/128x128@2x.png", "icons/128x128@2x.png",
"icons/icon.icns", "icons/icon.icns",

View File

@@ -25,7 +25,7 @@
<p>Selected</p> <p>Selected</p>
</div> </div>
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)"> <Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
<XIcon /> <TrashIcon />
</Button> </Button>
</div> </div>
<div v-else class="logged-out account"> <div v-else class="logged-out account">
@@ -41,7 +41,7 @@
<p>{{ account.username }}</p> <p>{{ account.username }}</p>
</Button> </Button>
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)"> <Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
<XIcon /> <TrashIcon />
</Button> </Button>
</div> </div>
</div> </div>
@@ -54,7 +54,7 @@
</template> </template>
<script setup> <script setup>
import { Avatar, Button, Card, PlusIcon, XIcon, UsersIcon, LogInIcon } from 'omorphia' import { Avatar, Button, Card, PlusIcon, TrashIcon, UsersIcon, LogInIcon } from 'omorphia'
import { ref, defineProps, computed, onMounted, onBeforeUnmount } from 'vue' import { ref, defineProps, computed, onMounted, onBeforeUnmount } from 'vue'
import { import {
users, users,

View File

@@ -3,7 +3,10 @@
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item"> <div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
<router-link <router-link
v-if="breadcrumb.link" v-if="breadcrumb.link"
:to="breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id))" :to="{
path: breadcrumb.link.replace('{id}', encodeURIComponent($route.params.id)),
query: breadcrumb.query,
}"
>{{ >{{
breadcrumb.name.charAt(0) === '?' breadcrumb.name.charAt(0) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1)) ? breadcrumbData.getName(breadcrumb.name.slice(1))

View File

@@ -1,11 +1,11 @@
<script setup> <script setup>
import { onUnmounted, ref, useSlots, watch } from 'vue' import { onUnmounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Card, DownloadIcon, StopCircleIcon, Avatar, AnimatedLogo, PlayIcon } from 'omorphia' import { Card, DownloadIcon, StopCircleIcon, Avatar, AnimatedLogo, PlayIcon } from 'omorphia'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import { install as pack_install } from '@/helpers/pack' import { install as pack_install } from '@/helpers/pack'
import { get, list, remove, run } from '@/helpers/profile' import { list, remove, run } from '@/helpers/profile'
import { import {
get_all_running_profile_paths, get_all_running_profile_paths,
get_uuids_by_profile_path, get_uuids_by_profile_path,
@@ -13,12 +13,10 @@ import {
} from '@/helpers/process' } from '@/helpers/process'
import { process_listener } from '@/helpers/events' import { process_listener } from '@/helpers/events'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { handleError, useSearch } from '@/store/state.js' import { handleError } from '@/store/state.js'
import { showInFolder } from '@/helpers/utils.js' import { showInFolder } from '@/helpers/utils.js'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue' import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
const searchStore = useSearch()
const props = defineProps({ const props = defineProps({
instance: { instance: {
type: Object, type: Object,
@@ -26,10 +24,6 @@ const props = defineProps({
return {} return {}
}, },
}, },
small: {
type: Boolean,
default: false,
},
}) })
const confirmModal = ref(null) const confirmModal = ref(null)
@@ -41,13 +35,14 @@ const modLoading = ref(
props.instance.install_stage ? props.instance.install_stage !== 'installed' : false props.instance.install_stage ? props.instance.install_stage !== 'installed' : false
) )
watch(props.instance, () => { watch(
modLoading.value = props.instance.install_stage () => props.instance,
? props.instance.install_stage !== 'installed' () => {
: false modLoading.value = props.instance.install_stage
}) ? props.instance.install_stage !== 'installed'
: false
const slots = useSlots() }
)
const router = useRouter() const router = useRouter()
@@ -143,8 +138,10 @@ const openFolder = async () => {
} }
const addContent = async () => { const addContent = async () => {
searchStore.instanceContext = await get(props.instance.path).catch(handleError) await router.push({
await router.push({ path: '/browse/mod' }) path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: props.instance.path },
})
} }
defineExpose({ defineExpose({
@@ -168,41 +165,7 @@ onUnmounted(() => unlisten())
<template> <template>
<div class="instance"> <div class="instance">
<Card v-if="props.small" class="instance-small-card" :class="{ 'button-base': !slots.content }"> <Card class="instance-card-item button-base" @click="seeInstance" @mouseenter="checkProcess">
<div
class="instance-small-card__description"
:class="{ 'button-base': slots.content }"
@click="seeInstance"
>
<Avatar
:src="
!props.instance.metadata.icon ||
(props.instance.metadata.icon && props.instance.metadata.icon.startsWith('http'))
? props.instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
:alt="props.instance.metadata.name"
size="sm"
/>
<div class="instance-small-card__info">
<span class="title">{{ props.instance.metadata.name }}</span>
{{
props.instance.metadata.loader.charAt(0).toUpperCase() +
props.instance.metadata.loader.slice(1)
}}
{{ props.instance.metadata.game_version }}
</div>
</div>
<div v-if="slots.content" class="instance-small-card__content">
<slot name="content" />
</div>
</Card>
<Card
v-else
class="instance-card-item button-base"
@click="seeInstance"
@mouseenter="checkProcess"
>
<Avatar <Avatar
size="sm" size="sm"
:src=" :src="
@@ -210,7 +173,7 @@ onUnmounted(() => unlisten())
? !props.instance.metadata.icon || ? !props.instance.metadata.icon ||
(props.instance.metadata.icon && props.instance.metadata.icon.startsWith('http')) (props.instance.metadata.icon && props.instance.metadata.icon.startsWith('http'))
? props.instance.metadata.icon ? props.instance.metadata.icon
: convertFileSrc(instance.metadata?.icon) : convertFileSrc(props.instance.metadata?.icon)
: props.instance.icon_url : props.instance.icon_url
" "
alt="Mod card" alt="Mod card"
@@ -224,27 +187,25 @@ onUnmounted(() => unlisten())
</p> </p>
</div> </div>
</Card> </Card>
<template v-if="!props.small"> <div
<div v-if="props.instance.metadata && playing === false && modLoading === false"
v-if="props.instance.metadata && playing === false && modLoading === false" class="install cta button-base"
class="install cta button-base" @click="play"
@click="play" >
> <PlayIcon />
<PlayIcon /> </div>
</div> <div v-else-if="modLoading === true && playing === false" class="cta loading-cta">
<div v-else-if="modLoading === true && playing === false" class="cta loading-cta"> <AnimatedLogo class="loading-indicator" />
<AnimatedLogo class="loading-indicator" /> </div>
</div> <div
<div v-else-if="playing === true"
v-else-if="playing === true" class="stop cta button-base"
class="stop cta button-base" @click="stop"
@click="stop" @mousehover="checkProcess"
@mousehover="checkProcess" >
> <StopCircleIcon />
<StopCircleIcon /> </div>
</div> <div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
<div v-else class="install cta button-base" @click="install"><DownloadIcon /></div>
</template>
<InstallConfirmModal ref="confirmModal" /> <InstallConfirmModal ref="confirmModal" />
<InstanceInstallModal ref="modInstallModal" /> <InstanceInstallModal ref="modInstallModal" />
</div> </div>
@@ -263,58 +224,13 @@ onUnmounted(() => unlisten())
</style> </style>
<style lang="scss" scoped> <style lang="scss" scoped>
.instance-small-card {
background-color: var(--color-bg) !important;
display: flex;
flex-direction: column;
min-height: min-content !important;
gap: 0.5rem;
align-items: flex-start;
padding: 0;
.instance-small-card__description {
display: flex;
flex-direction: row;
justify-content: flex-start;
gap: 1rem;
flex-grow: 1;
padding: var(--gap-xl);
padding-bottom: 0;
width: 100%;
&:not(.button-base) {
padding-bottom: var(--gap-xl);
}
}
.instance-small-card__info {
display: flex;
flex-direction: column;
justify-content: center;
.title {
color: var(--color-contrast);
font-weight: bolder;
}
}
.instance-small-card__content {
padding: var(--gap-xl);
padding-top: 0;
}
.cta {
display: none;
}
}
.instance { .instance {
position: relative; position: relative;
&:hover { &:hover {
.cta { .cta {
opacity: 1; opacity: 1;
bottom: 5.5rem; bottom: 4.75rem;
} }
.instance-card-item { .instance-card-item {
@@ -329,9 +245,7 @@ onUnmounted(() => unlisten())
background: hsl(0, 0%, 91%) !important; background: hsl(0, 0%, 91%) !important;
} }
} }
}
.light-mode {
.instance-card-item { .instance-card-item {
background: hsl(0, 0%, 100%) !important; background: hsl(0, 0%, 100%) !important;
@@ -351,7 +265,7 @@ onUnmounted(() => unlisten())
width: 3rem; width: 3rem;
height: 3rem; height: 3rem;
right: 1.25rem; right: 1.25rem;
bottom: 5rem; bottom: 4.25rem;
opacity: 0; opacity: 0;
transition: 0.2s ease-in-out bottom, 0.1s ease-in-out opacity, 0.1s ease-in-out filter !important; transition: 0.2s ease-in-out bottom, 0.1s ease-in-out opacity, 0.1s ease-in-out filter !important;
cursor: pointer; cursor: pointer;

View File

@@ -52,14 +52,12 @@ const currentSelected = ref({})
defineExpose({ defineExpose({
show: async (version, currentSelectedJava) => { show: async (version, currentSelectedJava) => {
if (version <= 8 && !!version) { if (version <= 8 && !!version) {
console.log(version)
chosenInstallOptions.value = await find_jre_8_jres().catch(handleError) chosenInstallOptions.value = await find_jre_8_jres().catch(handleError)
} else if (version >= 18) { } else if (version >= 18) {
chosenInstallOptions.value = await find_jre_18plus_jres().catch(handleError) chosenInstallOptions.value = await find_jre_18plus_jres().catch(handleError)
} else if (version) { } else if (version) {
chosenInstallOptions.value = await find_jre_17_jres().catch(handleError) chosenInstallOptions.value = await find_jre_17_jres().catch(handleError)
} else { } else {
console.log('get all')
chosenInstallOptions.value = await get_all_jre().catch(handleError) chosenInstallOptions.value = await get_all_jre().catch(handleError)
} }

View File

@@ -67,7 +67,7 @@ const showCard = ref(false)
const currentProcesses = ref(await getRunningProfiles().catch(handleError)) const currentProcesses = ref(await getRunningProfiles().catch(handleError))
await process_listener(async () => { const unlistenProcess = await process_listener(async () => {
await refresh() await refresh()
}) })
@@ -91,7 +91,7 @@ const goToTerminal = () => {
const currentLoadingBars = ref(Object.values(await progress_bars_list().catch(handleError))) const currentLoadingBars = ref(Object.values(await progress_bars_list().catch(handleError)))
await loading_listener(async () => { const unlistenLoading = await loading_listener(async () => {
await refreshInfo() await refreshInfo()
}) })
@@ -128,6 +128,8 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutside) window.removeEventListener('click', handleClickOutside)
unlistenProcess()
unlistenLoading()
}) })
</script> </script>

View File

@@ -1,5 +1,13 @@
<template> <template>
<Card class="card button-base" @click="$router.push(`/project/${project.project_id}/`)"> <Card
class="card button-base"
@click="
$router.push({
path: `/project/${project.project_id}/`,
query: { i: props.instance ? props.instance.path : undefined },
})
"
>
<div class="icon"> <div class="icon">
<Avatar :src="project.icon_url" size="md" class="search-icon" /> <Avatar :src="project.icon_url" size="md" class="search-icon" />
</div> </div>
@@ -117,14 +125,7 @@ const props = defineProps({
}) })
const installing = ref(false) const installing = ref(false)
const installed = ref(props.project.installed)
const installed = ref(
props.instance
? Object.values(props.instance.projects).some(
(p) => p.metadata?.project?.id === props.project.project_id
)
: false
)
async function install() { async function install() {
installing.value = true installing.value = true

View File

@@ -37,8 +37,8 @@ export async function get_uuids_by_profile_path(profilePath) {
/// Gets all running process IDs with a given profile path /// Gets all running process IDs with a given profile path
/// Returns [u32] /// Returns [u32]
export async function get_all_running_profile_paths(profile_path) { export async function get_all_running_profile_paths(profilePath) {
return await invoke('process_get_all_running_profile_paths', { profile_path }) return await invoke('process_get_all_running_profile_paths', { profilePath })
} }
/// Gets all running process IDs with a given profile path /// Gets all running process IDs with a given profile path

View File

@@ -1,12 +1,11 @@
<script setup> <script setup>
import { computed, onMounted, ref, watch } from 'vue' import { computed, nextTick, ref, readonly, shallowRef, watch } from 'vue'
import { import {
Pagination, Pagination,
Checkbox, Checkbox,
Button, Button,
ClearIcon, ClearIcon,
SearchIcon, SearchIcon,
DropdownSelect,
SearchFilter, SearchFilter,
Card, Card,
ClientIcon, ClientIcon,
@@ -15,74 +14,337 @@ import {
formatCategoryHeader, formatCategoryHeader,
formatCategory, formatCategory,
Promotion, Promotion,
DropdownSelect,
} from 'omorphia' } from 'omorphia'
import Multiselect from 'vue-multiselect' import Multiselect from 'vue-multiselect'
import { handleError, useSearch } from '@/store/state' import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags' import { get_categories, get_loaders, get_game_versions } from '@/helpers/tags'
import { useRoute } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { Avatar } from 'omorphia'
import SearchCard from '@/components/ui/SearchCard.vue' import SearchCard from '@/components/ui/SearchCard.vue'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue' import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue' import SplashScreen from '@/components/ui/SplashScreen.vue'
import Instance from '@/components/ui/Instance.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue' import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { check_installed, get as getInstance } from '@/helpers/profile.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
const router = useRouter()
const route = useRoute() const route = useRoute()
const searchStore = useSearch()
searchStore.projectType = route.params.projectType
const showVersions = computed(
() => searchStore.instanceContext === null || searchStore.ignoreInstance
)
const showLoaders = computed(
() =>
searchStore.projectType !== 'datapack' &&
searchStore.projectType !== 'resourcepack' &&
searchStore.projectType !== 'shader' &&
(searchStore.instanceContext === null || searchStore.ignoreInstance)
)
const confirmModal = ref(null) const confirmModal = ref(null)
const modInstallModal = ref(null) const modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null) const incompatibilityWarningModal = ref(null)
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: route.query })
const loading = ref(false)
const query = ref('')
const facets = ref([])
const orFacets = ref([])
const selectedVersions = ref([])
const onlyOpenSource = ref(false)
const showSnapshots = ref(false) const showSnapshots = ref(false)
const loading = ref(true) const selectedEnvironments = ref([])
const sortTypes = readonly([
{ display: 'Relevance', name: 'relevance' },
{ display: 'Download count', name: 'downloads' },
{ display: 'Follow count', name: 'follows' },
{ display: 'Recently published', name: 'newest' },
{ display: 'Recently updated', name: 'updated' },
])
const sortType = ref(sortTypes[0])
const maxResults = ref(20)
const currentPage = ref(1)
const projectType = ref(route.params.projectType)
const instanceContext = ref(null)
const ignoreInstanceLoaders = ref(false)
const ignoreInstanceGameVersions = ref(false)
const categories = ref([]) const results = shallowRef([])
const loaders = ref([]) const pageCount = computed(() =>
const availableGameVersions = ref([]) results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1
)
breadcrumbs.setContext({ name: 'Browse', link: route.path }) function getArrayOrString(x) {
if (typeof x === 'string' || x instanceof String) {
if (searchStore.projectType === 'modpack') { return [x]
searchStore.instanceContext = null } else {
return x
}
} }
onMounted(async () => { if (route.query.iv) {
;[categories.value, loaders.value, availableGameVersions.value] = await Promise.all([ ignoreInstanceGameVersions.value = route.query.iv === 'true'
get_categories().catch(handleError), }
get_loaders().catch(handleError), if (route.query.il) {
get_game_versions().catch(handleError), ignoreInstanceLoaders.value = route.query.il === 'true'
]) }
breadcrumbs.setContext({ name: 'Browse', link: route.path }) if (route.query.i) {
if (searchStore.projectType === 'modpack') { instanceContext.value = await getInstance(route.query.i, true)
searchStore.instanceContext = null }
if (route.query.q) {
query.value = route.query.q
}
if (route.query.f) {
facets.value = getArrayOrString(route.query.f)
}
if (route.query.g) {
orFacets.value = getArrayOrString(route.query.g)
}
if (route.query.v) {
selectedVersions.value = getArrayOrString(route.query.v)
}
if (route.query.l) {
onlyOpenSource.value = route.query.l === 'true'
}
if (route.query.h) {
showSnapshots.value = route.query.h === 'true'
}
if (route.query.e) {
selectedEnvironments.value = getArrayOrString(route.query.e)
}
if (route.query.s) {
sortType.value.name = route.query.s
switch (sortType.value.name) {
case 'relevance':
sortType.value.display = 'Relevance'
break
case 'downloads':
sortType.value.display = 'Downloads'
break
case 'newest':
sortType.value.display = 'Recently published'
break
case 'updated':
sortType.value.display = 'Recently updated'
break
case 'follows':
sortType.value.display = 'Follow count'
break
} }
searchStore.searchInput = '' }
await handleReset()
loading.value = false if (route.query.m) {
}) maxResults.value = route.query.m
}
if (route.query.o) {
currentPage.value = Math.ceil(route.query.o / maxResults.value) + 1
}
async function refreshSearch() {
const base = 'https://api.modrinth.com/v2/'
const params = [`limit=${maxResults.value}`, `index=${sortType.value.name}`]
if (query.value.length > 0) {
params.push(`query=${query.value.replace(/ /g, '+')}`)
}
if (instanceContext.value) {
if (!ignoreInstanceLoaders.value && projectType.value === 'mod') {
orFacets.value = [`categories:${encodeURIComponent(instanceContext.value.metadata.loader)}`]
}
if (!ignoreInstanceGameVersions.value) {
selectedVersions.value = [instanceContext.value.metadata.game_version]
}
}
if (
facets.value.length > 0 ||
orFacets.value.length > 0 ||
selectedVersions.value.length > 0 ||
selectedEnvironments.value.length > 0 ||
projectType.value
) {
let formattedFacets = []
for (const facet of facets.value) {
formattedFacets.push([facet])
}
// loaders specifier
if (orFacets.value.length > 0) {
formattedFacets.push(orFacets.value)
} else if (projectType.value === 'mod') {
formattedFacets.push(
['forge', 'fabric', 'quilt'].map((x) => `categories:'${encodeURIComponent(x)}'`)
)
} else if (projectType.value === 'datapack') {
formattedFacets.push(['datapack'].map((x) => `categories:'${encodeURIComponent(x)}'`))
}
if (selectedVersions.value.length > 0) {
const versionFacets = []
for (const facet of selectedVersions.value) {
versionFacets.push('versions:' + facet)
}
formattedFacets.push(versionFacets)
}
if (onlyOpenSource.value) {
formattedFacets.push(['open_source:true'])
}
if (selectedEnvironments.value.length > 0) {
let environmentFacets = []
const includesClient = selectedEnvironments.value.includes('client')
const includesServer = selectedEnvironments.value.includes('server')
if (includesClient && includesServer) {
environmentFacets = [['client_side:required'], ['server_side:required']]
} else {
if (includesClient) {
environmentFacets = [
['client_side:optional', 'client_side:required'],
['server_side:optional', 'server_side:unsupported'],
]
}
if (includesServer) {
environmentFacets = [
['client_side:optional', 'client_side:unsupported'],
['server_side:optional', 'server_side:required'],
]
}
}
formattedFacets = [...formattedFacets, ...environmentFacets]
}
if (projectType.value) {
formattedFacets.push([
`project_type:${projectType.value === 'datapack' ? 'mod' : projectType.value}`,
])
}
params.push(`facets=${JSON.stringify(formattedFacets)}`)
}
const offset = (currentPage.value - 1) * maxResults.value
if (currentPage.value !== 1) {
params.push(`offset=${offset}`)
}
let url = 'search'
if (params.length > 0) {
for (let i = 0; i < params.length; i++) {
url += i === 0 ? `?${params[i]}` : `&${params[i]}`
}
}
let val = `${base}${url}`
const rawResults = await useFetch(val, 'search results')
if (instanceContext.value) {
for (let val of rawResults.hits) {
val.installed = await check_installed(instanceContext.value.path, val.project_id).then(
(x) => (val.installed = x)
)
}
}
results.value = rawResults
}
async function onSearchChange(newPageNumber) {
currentPage.value = newPageNumber
if (query.value === null) {
return
}
await refreshSearch()
const obj = getSearchUrl((currentPage.value - 1) * maxResults.value, true)
await router.replace({ path: route.path, query: obj })
breadcrumbs.setContext({ name: 'Browse', link: route.path, query: obj })
}
const searchWrapper = ref(null)
async function onSearchChangeToTop(newPageNumber) {
await onSearchChange(newPageNumber)
await nextTick()
searchWrapper.value.scrollTo({ top: 0, behavior: 'smooth' })
}
function getSearchUrl(offset, useObj) {
const queryItems = []
const obj = {}
if (query.value) {
queryItems.push(`q=${encodeURIComponent(query.value)}`)
obj.q = query.value
}
if (offset > 0) {
queryItems.push(`o=${offset}`)
obj.o = offset
}
if (facets.value.length > 0) {
queryItems.push(`f=${encodeURIComponent(facets.value)}`)
obj.f = facets.value
}
if (orFacets.value.length > 0) {
queryItems.push(`g=${encodeURIComponent(orFacets.value)}`)
obj.g = orFacets.value
}
if (selectedVersions.value.length > 0) {
queryItems.push(`v=${encodeURIComponent(selectedVersions.value)}`)
obj.v = selectedVersions.value
}
if (onlyOpenSource.value) {
queryItems.push('l=true')
obj.l = true
}
if (showSnapshots.value) {
queryItems.push('h=true')
obj.h = true
}
if (selectedEnvironments.value.length > 0) {
queryItems.push(`e=${encodeURIComponent(selectedEnvironments.value)}`)
obj.e = selectedEnvironments.value
}
if (sortType.value.name !== 'relevance') {
queryItems.push(`s=${encodeURIComponent(sortType.value.name)}`)
obj.s = sortType.value.name
}
if (maxResults.value !== 20) {
queryItems.push(`m=${encodeURIComponent(maxResults.value)}`)
obj.m = maxResults.value
}
if (instanceContext.value) {
queryItems.push(`i=${encodeURIComponent(instanceContext.value.path)}`)
obj.i = instanceContext.value.path
}
if (ignoreInstanceGameVersions.value) {
queryItems.push('iv=true')
obj.iv = true
}
if (ignoreInstanceLoaders.value) {
queryItems.push('il=true')
obj.il = true
}
let url = `${route.path}`
if (queryItems.length > 0) {
url += `?${queryItems[0]}`
for (let i = 1; i < queryItems.length; i++) {
url += `&${queryItems[i]}`
}
}
return useObj ? obj : url
}
const sortedCategories = computed(() => { const sortedCategories = computed(() => {
const values = new Map() const values = new Map()
for (const category of categories.value.filter( for (const category of categories.value.filter(
(cat) => (cat) => cat.project_type === (projectType.value === 'datapack' ? 'mod' : projectType.value)
cat.project_type ===
(searchStore.projectType === 'datapack' ? 'mod' : searchStore.projectType)
)) { )) {
if (!values.has(category.header)) { if (!values.has(category.header)) {
values.set(category.header, []) values.set(category.header, [])
@@ -92,128 +354,188 @@ const sortedCategories = computed(() => {
return values return values
}) })
const getSearchResults = async () => { async function clearFilters() {
const queryString = searchStore.getQueryString() for (const facet of [...facets.value]) {
const response = await useFetch( await toggleFacet(facet, true)
`https://api.modrinth.com/v2/search${queryString}`, }
'search results' for (const facet of [...orFacets.value]) {
) await toggleOrFacet(facet, true)
searchStore.setSearchResults(response) }
onlyOpenSource.value = false
selectedVersions.value = []
selectedEnvironments.value = []
await onSearchChange(1)
} }
const handleReset = async () => { async function toggleFacet(elementName, doNotSendRequest = false) {
searchStore.currentPage = 1 const index = facets.value.indexOf(elementName)
searchStore.offset = 0
searchStore.resetFilters() if (index !== -1) {
await getSearchResults() facets.value.splice(index, 1)
} else {
facets.value.push(elementName)
}
if (!doNotSendRequest) {
await onSearchChange(1)
}
} }
const toggleFacet = async (facet) => { async function toggleOrFacet(elementName, doNotSendRequest) {
searchStore.currentPage = 1 const index = orFacets.value.indexOf(elementName)
searchStore.offset = 0 if (index !== -1) {
const index = searchStore.facets.indexOf(facet) orFacets.value.splice(index, 1)
} else {
orFacets.value.push(elementName)
}
if (index !== -1) searchStore.facets.splice(index, 1) if (!doNotSendRequest) {
else searchStore.facets.push(facet) await onSearchChange(1)
}
await switchPage(1)
} }
const toggleOrFacet = async (orFacet) => { function toggleEnv(environment, sendRequest) {
const index = searchStore.orFacets.indexOf(orFacet) const index = selectedEnvironments.value.indexOf(environment)
if (index !== -1) {
selectedEnvironments.value.splice(index, 1)
} else {
selectedEnvironments.value.push(environment)
}
if (index !== -1) searchStore.orFacets.splice(index, 1) if (!sendRequest) {
else searchStore.orFacets.push(orFacet) onSearchChange(1)
}
await switchPage(1)
}
const switchPage = async (page) => {
searchStore.currentPage = parseInt(page)
if (page === 1) searchStore.offset = 0
else searchStore.offset = (searchStore.currentPage - 1) * searchStore.limit
await getSearchResults()
} }
watch( watch(
() => route.params.projectType, () => route.params.projectType,
async (projectType) => { async (newType) => {
if (!projectType) return if (!newType) return
searchStore.projectType = projectType projectType.value = newType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${searchStore.projectType}` }) breadcrumbs.setContext({ name: 'Browse', link: `/browse/${projectType.value}` })
await handleReset()
await switchPage(1) sortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
loading.value = true
await clearFilters()
loading.value = false
} }
) )
const handleInstanceSwitch = async (value) => { const [categories, loaders, availableGameVersions] = await Promise.all([
searchStore.ignoreInstance = value get_categories().catch(handleError).then(ref),
await switchPage(1) get_loaders().catch(handleError).then(ref),
} get_game_versions().catch(handleError).then(ref),
refreshSearch(),
])
const selectableProjectTypes = computed(() => { const selectableProjectTypes = computed(() => {
const values = [ const values = [
{ label: 'Data Packs', href: `/browse/datapack` },
{ label: 'Shaders', href: `/browse/shader` }, { label: 'Shaders', href: `/browse/shader` },
{ label: 'Resource Packs', href: `/browse/resourcepack` }, { label: 'Resource Packs', href: `/browse/resourcepack` },
] ]
if (searchStore.instanceContext) { if (instanceContext.value) {
if (searchStore.instanceContext.metadata.loader !== 'vanilla') { if (
availableGameVersions.value.findIndex(
(x) => x.version === instanceContext.value.metadata.game_version
) <= availableGameVersions.value.findIndex((x) => x.version === '1.13')
) {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
}
if (instanceContext.value.metadata.loader !== 'vanilla') {
values.unshift({ label: 'Mods', href: '/browse/mod' }) values.unshift({ label: 'Mods', href: '/browse/mod' })
} }
} else { } else {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
values.unshift({ label: 'Mods', href: '/browse/mod' }) values.unshift({ label: 'Mods', href: '/browse/mod' })
values.unshift({ label: 'Modpacks', href: '/browse/modpack' }) values.unshift({ label: 'Modpacks', href: '/browse/modpack' })
} }
return values return values
}) })
const showVersions = computed(
() => instanceContext.value === null || ignoreInstanceGameVersions.value
)
const showLoaders = computed(
() =>
(projectType.value !== 'datapack' &&
projectType.value !== 'resourcepack' &&
projectType.value !== 'shader' &&
instanceContext.value === null) ||
ignoreInstanceLoaders.value
)
</script> </script>
<template> <template>
<div class="search-container"> <div class="search-container">
<aside class="filter-panel"> <aside class="filter-panel">
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small> <div v-if="instanceContext" class="small-instance">
<template #content> <div class="instance">
<Checkbox <Avatar
:model-value="searchStore.ignoreInstance" :src="
:checked="searchStore.ignoreInstance" !instanceContext.metadata.icon ||
label="Show unsupported content" (instanceContext.metadata.icon && instanceContext.metadata.icon.startsWith('http'))
class="filter-checkbox" ? instanceContext.metadata.icon
@update:model-value="(value) => handleInstanceSwitch(value)" : convertFileSrc(instanceContext.metadata.icon)
"
:alt="instanceContext.metadata.name"
size="sm"
/> />
</template> <div class="small-instance_info">
</Instance> <span class="title">{{ instanceContext.metadata.name }}</span>
<span>
{{
instanceContext.metadata.loader.charAt(0).toUpperCase() +
instanceContext.metadata.loader.slice(1)
}}
{{ instanceContext.metadata.game_version }}
</span>
</div>
</div>
<Checkbox
v-model="ignoreInstanceGameVersions"
label="Override game versions"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
/>
<Checkbox
v-model="ignoreInstanceLoaders"
label="Override loaders"
class="filter-checkbox"
@update:model-value="onSearchChangeToTop(1)"
/>
</div>
<Card class="search-panel-card"> <Card class="search-panel-card">
<Button <Button
role="button" role="button"
:disabled=" :disabled="
!( onlyOpenSource === false &&
searchStore.facets.length > 0 || selectedEnvironments.length === 0 &&
searchStore.orFacets.length > 0 || selectedVersions.length === 0 &&
searchStore.environments.server === true || facets.length === 0 &&
searchStore.environments.client === true || orFacets.length === 0
searchStore.openSource === true ||
searchStore.activeVersions.length > 0
)
" "
@click="handleReset" @click="clearFilters"
><ClearIcon />Clear Filters</Button
> >
<ClearIcon /> Clear Filters
</Button>
<div v-if="showLoaders" class="loaders"> <div v-if="showLoaders" class="loaders">
<h2>Loaders</h2> <h2>Loaders</h2>
<div <div
v-for="loader in loaders.filter( v-for="loader in loaders.filter(
(l) => (l) =>
(searchStore.projectType !== 'mod' && (projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
l.supported_project_types?.includes(searchStore.projectType)) || (projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
(searchStore.projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
)" )"
:key="loader" :key="loader"
> >
<SearchFilter <SearchFilter
:active-filters="searchStore.orFacets" :active-filters="orFacets"
:icon="loader.icon" :icon="loader.icon"
:display-name="formatCategory(loader.name)" :display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`" :facet-name="`categories:${encodeURIComponent(loader.name)}`"
@@ -222,49 +544,11 @@ const selectableProjectTypes = computed(() => {
/> />
</div> </div>
</div> </div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="searchStore.facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div v-if="searchStore.projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
v-model="searchStore.environments.client"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@click="getSearchResults"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
v-model="searchStore.environments.server"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@click="getSearchResults"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div v-if="showVersions" class="versions"> <div v-if="showVersions" class="versions">
<h2>Minecraft versions</h2> <h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" /> <Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect <multiselect
v-model="searchStore.activeVersions" v-model="selectedVersions"
:options=" :options="
showSnapshots showSnapshots
? availableGameVersions.map((x) => x.version) ? availableGameVersions.map((x) => x.version)
@@ -279,21 +563,59 @@ const selectableProjectTypes = computed(() => {
:clear-search-on-select="false" :clear-search-on-select="false"
:show-labels="false" :show-labels="false"
placeholder="Choose versions..." placeholder="Choose versions..."
@update:model-value="getSearchResults" @update:model-value="onSearchChange(1)"
/> />
</div> </div>
<div
v-for="categoryList in Array.from(sortedCategories)"
:key="categoryList[0]"
class="categories"
>
<h2>{{ formatCategoryHeader(categoryList[0]) }}</h2>
<div v-for="category in categoryList[1]" :key="category.name">
<SearchFilter
:active-filters="facets"
:icon="category.icon"
:display-name="formatCategory(category.name)"
:facet-name="`categories:${encodeURIComponent(category.name)}`"
class="filter-checkbox"
@toggle="toggleFacet"
/>
</div>
</div>
<div v-if="projectType !== 'datapack'" class="environment">
<h2>Environments</h2>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Client"
facet-name="client"
class="filter-checkbox"
@toggle="toggleEnv"
>
<ClientIcon aria-hidden="true" />
</SearchFilter>
<SearchFilter
:active-filters="selectedEnvironments"
display-name="Server"
facet-name="server"
class="filter-checkbox"
@toggle="toggleEnv"
>
<ServerIcon aria-hidden="true" />
</SearchFilter>
</div>
<div class="open-source"> <div class="open-source">
<h2>Open source</h2> <h2>Open source</h2>
<Checkbox <Checkbox
v-model="searchStore.openSource" v-model="onlyOpenSource"
label="Open source only"
class="filter-checkbox" class="filter-checkbox"
label="Open source" @update:model-value="onSearchChange(1)"
@click="getSearchResults"
/> />
</div> </div>
</Card> </Card>
</aside> </aside>
<div class="search"> <div ref="searchWrapper" class="search">
<Promotion class="promotion" /> <Promotion class="promotion" />
<Card class="project-type-container"> <Card class="project-type-container">
<NavRow :links="selectableProjectTypes" /> <NavRow :links="selectableProjectTypes" />
@@ -302,64 +624,59 @@ const selectableProjectTypes = computed(() => {
<div class="iconified-input"> <div class="iconified-input">
<SearchIcon aria-hidden="true" /> <SearchIcon aria-hidden="true" />
<input <input
v-model="searchStore.searchInput" v-model="query"
autocomplete="off" autocomplete="off"
type="text" type="text"
:placeholder="`Search ${searchStore.projectType}s...`" :placeholder="`Search ${projectType}s...`"
@input="getSearchResults" @input="onSearchChange(1)"
/> />
</div> </div>
<div class="inline-option"> <div class="inline-option">
<span>Sort by</span> <span>Sort by</span>
<DropdownSelect <DropdownSelect
v-model="searchStore.filter" v-model="sortType"
name="Sort dropdown" name="Sort by"
:options="[ :options="sortTypes"
'Relevance', :display-name="(option) => option?.display"
'Download count', @change="onSearchChange(1)"
'Follow count',
'Recently published',
'Recently updated',
]"
class="sort-dropdown"
@change="getSearchResults"
/> />
</div> </div>
<div class="inline-option"> <div class="inline-option">
<span>Show per page</span> <span>Show per page</span>
<DropdownSelect <DropdownSelect
v-model="searchStore.limit" v-model="maxResults"
name="Limit dropdown" name="Max results"
:options="[5, 10, 15, 20, 50, 100]" :options="[5, 10, 15, 20, 50, 100]"
:default-value="searchStore.limit.toString()" :default-value="maxResults"
:model-value="searchStore.limit.toString()" :model-value="maxResults"
class="limit-dropdown" class="limit-dropdown"
@change="getSearchResults" @change="onSearchChange(1)"
/> />
</div> </div>
</Card> </Card>
<Pagination <Pagination
:page="searchStore.currentPage" :page="currentPage"
:count="searchStore.pageCount" :count="pageCount"
@switch-page="switchPage" :link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-before"
@switch-page="onSearchChange"
/> />
<SplashScreen v-if="loading" /> <SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list"> <section v-else class="project-list display-mode--list instance-results" role="list">
<SearchCard <SearchCard
v-for="result in searchStore.searchResults" v-for="result in results.hits"
:key="result?.project_id" :key="result?.project_id"
:project="result" :project="result"
:instance="searchStore.instanceContext" :instance="instanceContext"
:categories="[ :categories="[
...categories.filter( ...categories.filter(
(cat) => (cat) =>
result?.display_categories.includes(cat.name) && result?.display_categories.includes(cat.name) && cat.project_type === projectType
cat.project_type === searchStore.projectType
), ),
...loaders.filter( ...loaders.filter(
(loader) => (loader) =>
result?.display_categories.includes(loader.name) && result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(searchStore.projectType) loader.supported_project_types?.includes(projectType)
), ),
]" ]"
:confirm-modal="confirmModal" :confirm-modal="confirmModal"
@@ -367,10 +684,12 @@ const selectableProjectTypes = computed(() => {
:incompatibility-warning-modal="incompatibilityWarningModal" :incompatibility-warning-modal="incompatibilityWarningModal"
/> />
</section> </section>
<Pagination <pagination
:page="searchStore.currentPage" :page="currentPage"
:count="searchStore.pageCount" :count="pageCount"
@switch-page="switchPage" :link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-after"
@switch-page="onSearchChangeToTop"
/> />
</div> </div>
</div> </div>
@@ -381,6 +700,32 @@ const selectableProjectTypes = computed(() => {
<style src="vue-multiselect/dist/vue-multiselect.css"></style> <style src="vue-multiselect/dist/vue-multiselect.css"></style>
<style lang="scss"> <style lang="scss">
.small-instance {
background: var(--color-bg);
padding: var(--gap-lg);
border-radius: var(--radius-md);
margin-bottom: var(--gap-md);
.instance {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
.title {
font-weight: 600;
color: var(--color-contrast);
}
}
.small-instance_info {
display: flex;
flex-direction: column;
gap: 0.25rem;
justify-content: space-between;
padding: 0.25rem 0;
}
}
.filter-checkbox { .filter-checkbox {
margin-bottom: 0.3rem; margin-bottom: 0.3rem;
font-size: 1rem; font-size: 1rem;
@@ -492,7 +837,8 @@ const selectableProjectTypes = computed(() => {
} }
.search { .search {
margin: 0 1rem 0 21rem; scroll-behavior: smooth;
margin: 0 1rem 0.5rem 21rem;
width: calc(100% - 22rem); width: calc(100% - 22rem);
.loading { .loading {

View File

@@ -36,7 +36,6 @@ const getInstances = async () => {
} }
const getFeaturedModpacks = async () => { const getFeaturedModpacks = async () => {
console.log(filter.value)
const response = await useFetch( const response = await useFetch(
`https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`, `https://api.modrinth.com/v2/search?facets=[["project_type:modpack"]]&limit=10&index=follows&filters=${filter.value}`,
'featured modpacks' 'featured modpacks'

View File

@@ -21,35 +21,52 @@ fetchSettings.envArgs = fetchSettings.custom_env_args.map((x) => x.join('=')).jo
const settings = ref(fetchSettings) const settings = ref(fetchSettings)
const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024)) const maxMemory = ref(Math.floor((await get_max_memory().catch(handleError)) / 1024))
watch(settings.value, async (oldSettings, newSettings) => { watch(
const setSettings = JSON.parse(JSON.stringify(newSettings)) settings,
async (oldSettings, newSettings) => {
const setSettings = JSON.parse(JSON.stringify(newSettings))
if (setSettings.java_globals.JAVA_8?.path === '') { if (setSettings.java_globals.JAVA_8?.path === '') {
setSettings.java_globals.JAVA_8 = undefined setSettings.java_globals.JAVA_8 = undefined
} }
if (setSettings.java_globals.JAVA_17?.path === '') { if (setSettings.java_globals.JAVA_17?.path === '') {
setSettings.java_globals.JAVA_17 = undefined setSettings.java_globals.JAVA_17 = undefined
} }
setSettings.custom_java_args = setSettings.javaArgs.trim().split(/\s+/).filter(Boolean) if (setSettings.java_globals.JAVA_8?.path) {
setSettings.custom_env_args = setSettings.envArgs setSettings.java_globals.JAVA_8.path = setSettings.java_globals.JAVA_8.path.replace(
.trim() 'java.exe',
.split(/\s+/) 'javaw.exe'
.filter(Boolean) )
.map((x) => x.split('=').filter(Boolean)) }
if (setSettings.java_globals.JAVA_17?.path) {
setSettings.java_globals.JAVA_17.path = setSettings.java_globals.JAVA_17?.path.replace(
'java.exe',
'javaw.exe'
)
}
if (!setSettings.hooks.pre_launch) { setSettings.custom_java_args = setSettings.javaArgs.trim().split(/\s+/).filter(Boolean)
setSettings.hooks.pre_launch = null setSettings.custom_env_args = setSettings.envArgs
} .trim()
if (!setSettings.hooks.wrapper) { .split(/\s+/)
setSettings.hooks.wrapper = null .filter(Boolean)
} .map((x) => x.split('=').filter(Boolean))
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
await set(setSettings) if (!setSettings.hooks.pre_launch) {
}) setSettings.hooks.pre_launch = null
}
if (!setSettings.hooks.wrapper) {
setSettings.hooks.wrapper = null
}
if (!setSettings.hooks.post_exit) {
setSettings.hooks.post_exit = null
}
await set(setSettings)
},
{ deep: true }
)
</script> </script>
<template> <template>
@@ -119,7 +136,7 @@ watch(settings.value, async (oldSettings, newSettings) => {
id="max-downloads" id="max-downloads"
v-model="settings.max_concurrent_downloads" v-model="settings.max_concurrent_downloads"
:min="1" :min="1"
:max="100" :max="10"
:step="1" :step="1"
/> />
</div> </div>
@@ -136,7 +153,7 @@ watch(settings.value, async (oldSettings, newSettings) => {
id="max-writes" id="max-writes"
v-model="settings.max_concurrent_writes" v-model="settings.max_concurrent_writes"
:min="1" :min="1"
:max="100" :max="50"
:step="1" :step="1"
/> />
</div> </div>

View File

@@ -126,22 +126,22 @@ import { process_listener, profile_listener } from '@/helpers/events'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ref, onUnmounted } from 'vue' import { ref, onUnmounted } from 'vue'
import { convertFileSrc } from '@tauri-apps/api/tauri' import { convertFileSrc } from '@tauri-apps/api/tauri'
import { handleError, useBreadcrumbs, useLoading, useSearch } from '@/store/state' import { handleError, useBreadcrumbs, useLoading } from '@/store/state'
import { showInFolder } from '@/helpers/utils.js' import { showInFolder } from '@/helpers/utils.js'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const searchStore = useSearch()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
const instance = ref(await get(route.params.id).catch(handleError)) const instance = ref(await get(route.params.id).catch(handleError))
searchStore.instanceContext = instance.value
breadcrumbs.setName('Instance', instance.value.metadata.name) breadcrumbs.setName('Instance', instance.value.metadata.name)
breadcrumbs.setContext({ breadcrumbs.setContext({
name: instance.value.metadata.name, name: instance.value.metadata.name,
link: route.path, link: route.path,
query: route.query,
}) })
const loadingBar = useLoading() const loadingBar = useLoading()
@@ -183,7 +183,6 @@ const stopInstance = async () => {
const unlistenProfiles = await profile_listener(async (event) => { const unlistenProfiles = await profile_listener(async (event) => {
if (event.path === route.params.id) { if (event.path === route.params.id) {
instance.value = await get(route.params.id).catch(handleError) instance.value = await get(route.params.id).catch(handleError)
searchStore.instanceContext = instance.value
} }
}) })
@@ -242,6 +241,7 @@ const handleOptionsClick = async (args) => {
case 'add_content': case 'add_content':
await router.push({ await router.push({
path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`, path: `/browse/${instance.value.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: route.params.id },
}) })
break break
case 'edit': case 'edit':

View File

@@ -2,12 +2,12 @@
<Card class="log-card"> <Card class="log-card">
<div class="button-row"> <div class="button-row">
<DropdownSelect <DropdownSelect
v-model="selectedLogIndex"
:default-value="0"
name="Log date" name="Log date"
:model-value="logs[selectedLogIndex]" :options="logs.map((_, index) => index)"
:options="logs" :display-name="(option) => logs[option]?.name"
:display-name="(option) => option?.name"
:disabled="logs.length === 0" :disabled="logs.length === 0"
@change="(value) => (selectedLogIndex = value.index)"
/> />
<div class="button-group"> <div class="button-group">
<Button :disabled="!logs[selectedLogIndex]" @click="copyLog()"> <Button :disabled="!logs[selectedLogIndex]" @click="copyLog()">
@@ -30,7 +30,11 @@
</div> </div>
</div> </div>
<div ref="logContainer" class="log-text"> <div ref="logContainer" class="log-text">
<span v-for="line in logs[selectedLogIndex]?.stdout.split('\n')" :key="line" class="no-wrap"> <span
v-for="(line, index) in logs[selectedLogIndex]?.stdout.split('\n')"
:key="index"
class="no-wrap"
>
{{ line }} <br /> {{ line }} <br />
</span> </span>
</div> </div>
@@ -68,15 +72,18 @@ const props = defineProps({
}) })
async function getLiveLog() { async function getLiveLog() {
const uuids = await get_uuids_by_profile_path(route.params.id).catch(handleError) if (route.params.id) {
let returnValue const uuids = await get_uuids_by_profile_path(route.params.id).catch(handleError)
if (uuids.length === 0) { let returnValue
returnValue = 'No live game detected. \nStart your game to proceed' if (uuids.length === 0) {
} else { returnValue = 'No live game detected. \nStart your game to proceed'
returnValue = await get_stdout_by_uuid(uuids[0]).catch(handleError) } else {
} returnValue = await get_stdout_by_uuid(uuids[0]).catch(handleError)
}
return { name: 'Live Log', stdout: returnValue, live: true } return { name: 'Live Log', stdout: returnValue, live: true }
}
return null
} }
async function getLogs() { async function getLogs() {

View File

@@ -30,6 +30,7 @@
@click=" @click="
router.push({ router.push({
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`, path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: $route.params.id },
}) })
" "
> >
@@ -80,7 +81,11 @@
</Button> </Button>
</div> </div>
<div class="table-cell table-text name-cell"> <div class="table-cell table-text name-cell">
<router-link v-if="mod.slug" :to="`/project/${mod.slug}/`" class="mod-text"> <router-link
v-if="mod.slug"
:to="{ path: `/project/${mod.slug}/`, query: { i: props.instance.path } }"
class="mod-text"
>
<Avatar :src="mod.icon" /> <Avatar :src="mod.icon" />
{{ mod.name }} {{ mod.name }}
</router-link> </router-link>
@@ -305,7 +310,6 @@ async function updateProject(mod) {
async function toggleDisableMod(mod) { async function toggleDisableMod(mod) {
mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError) mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError)
console.log(mod.disabled)
mod.disabled = !mod.disabled mod.disabled = !mod.disabled
} }

View File

@@ -1,4 +1,12 @@
<template> <template>
<ModalConfirm
ref="modal_confirm"
title="Are you sure you want to delete this instance?"
description="If you proceed, all data for your instance will be removed. You will not be able to recover it."
:has-to-type="false"
proceed-label="Delete"
@proceed="removeProfile"
/>
<Modal ref="changeVersionsModal" header="Change instance versions"> <Modal ref="changeVersionsModal" header="Change instance versions">
<div class="change-versions-modal universal-body"> <div class="change-versions-modal universal-body">
<div class="input-row"> <div class="input-row">
@@ -105,8 +113,8 @@
placeholder="Select categories..." placeholder="Select categories..."
@tag=" @tag="
(newTag) => { (newTag) => {
groups.push(newTag) groups.push(newTag.trim().substring(0, 32))
availableGroups.push(newTag) availableGroups.push(newTag.trim().substring(0, 32))
} }
" "
/> />
@@ -288,7 +296,7 @@
id="delete-profile" id="delete-profile"
class="btn btn-danger" class="btn btn-danger"
:disabled="removing" :disabled="removing"
@click="removeProfile" @click="$refs.modal_confirm.show()"
> >
<TrashIcon /> Delete <TrashIcon /> Delete
</button> </button>
@@ -311,6 +319,7 @@ import {
XIcon, XIcon,
SaveIcon, SaveIcon,
HammerIcon, HammerIcon,
ModalConfirm,
} from 'omorphia' } from 'omorphia'
import { Multiselect } from 'vue-multiselect' import { Multiselect } from 'vue-multiselect'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -398,7 +407,8 @@ const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
watch( watch(
[ [
title, title,
groups.value, groups,
groups,
overrideJavaInstall, overrideJavaInstall,
javaInstall, javaInstall,
overrideJavaArgs, overrideJavaArgs,
@@ -406,17 +416,17 @@ watch(
overrideEnvVars, overrideEnvVars,
envVars, envVars,
overrideMemorySettings, overrideMemorySettings,
memory.value, memory,
overrideWindowSettings, overrideWindowSettings,
resolution.value, resolution,
overrideHooks, overrideHooks,
hooks.value, hooks,
], ],
async () => { async () => {
const editProfile = { const editProfile = {
metadata: { metadata: {
name: title.value, name: title.value.trim().substring(0, 16) ?? 'Instance',
groups: groups.value, groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
}, },
java: {}, java: {},
} }
@@ -424,6 +434,10 @@ watch(
if (overrideJavaInstall.value) { if (overrideJavaInstall.value) {
if (javaInstall.value.path !== '') { if (javaInstall.value.path !== '') {
editProfile.java.override_version = javaInstall.value editProfile.java.override_version = javaInstall.value
editProfile.java.override_version.path = editProfile.java.override_version.path.replace(
'java.exe',
'javaw.exe'
)
} }
} }
@@ -456,7 +470,8 @@ watch(
} }
await edit(props.instance.path, editProfile) await edit(props.instance.path, editProfile)
} },
{ deep: true }
) )
const repairing = ref(false) const repairing = ref(false)

View File

@@ -1,7 +1,29 @@
<template> <template>
<div class="root-container"> <div class="root-container">
<div v-if="data" class="project-sidebar"> <div v-if="data" class="project-sidebar">
<Instance v-if="instance" :instance="instance" small /> <div v-if="instance" class="small-instance">
<div class="instance">
<Avatar
:src="
!instance.metadata.icon ||
(instance.metadata.icon && instance.metadata.icon.startsWith('http'))
? instance.metadata.icon
: convertFileSrc(instance.metadata?.icon)
"
:alt="instance.metadata.name"
size="sm"
/>
<div class="small-instance_info">
<span class="title">{{ instance.metadata.name }}</span>
<span>
{{
instance.metadata.loader.charAt(0).toUpperCase() + instance.metadata.loader.slice(1)
}}
{{ instance.metadata.game_version }}
</span>
</div>
</div>
</div>
<Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick"> <Card class="sidebar-card" @contextmenu.prevent.stop="handleRightClick">
<Avatar size="lg" :src="data.icon_url" /> <Avatar size="lg" :src="data.icon_url" />
<div class="instance-info"> <div class="instance-info">
@@ -119,8 +141,8 @@
<span>Wiki</span> <span>Wiki</span>
</a> </a>
<a <a
v-if="data.wiki_url" v-if="data.discord_url"
:href="data.wiki_url" :href="data.discord_url"
class="title" class="title"
rel="noopener nofollow ugc external" rel="noopener nofollow ugc external"
> >
@@ -238,7 +260,12 @@ import {
} from '@/assets/external' } from '@/assets/external'
import { get_categories, get_loaders } from '@/helpers/tags' import { get_categories, get_loaders } from '@/helpers/tags'
import { install as packInstall } from '@/helpers/pack' import { install as packInstall } from '@/helpers/pack'
import { list, add_project_from_version as installMod, check_installed } from '@/helpers/profile' import {
list,
add_project_from_version as installMod,
check_installed,
get as getInstance,
} from '@/helpers/profile'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
@@ -246,16 +273,13 @@ import { ref, shallowRef, watch } from 'vue'
import { installVersionDependencies } from '@/helpers/utils' import { installVersionDependencies } from '@/helpers/utils'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue' import InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue' import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import Instance from '@/components/ui/Instance.vue'
import { useSearch } from '@/store/search'
import { useBreadcrumbs } from '@/store/breadcrumbs' import { useBreadcrumbs } from '@/store/breadcrumbs'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue' import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js' import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js' import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue' import ContextMenu from '@/components/ui/ContextMenu.vue'
const searchStore = useSearch()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const breadcrumbs = useBreadcrumbs() const breadcrumbs = useBreadcrumbs()
@@ -263,11 +287,11 @@ const breadcrumbs = useBreadcrumbs()
const confirmModal = ref(null) const confirmModal = ref(null)
const modInstallModal = ref(null) const modInstallModal = ref(null)
const incompatibilityWarning = ref(null) const incompatibilityWarning = ref(null)
const options = ref(null) const options = ref(null)
const instance = ref(searchStore.instanceContext)
const installing = ref(false) const installing = ref(false)
const [data, versions, members, dependencies, categories, loaders] = await Promise.all([ const [data, versions, members, dependencies, categories, loaders, instance] = await Promise.all([
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}`, 'project').then(shallowRef), useFetch(`https://api.modrinth.com/v2/project/${route.params.id}`, 'project').then(shallowRef),
useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`, 'project').then( useFetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`, 'project').then(
shallowRef shallowRef
@@ -280,6 +304,7 @@ const [data, versions, members, dependencies, categories, loaders] = await Promi
), ),
get_loaders().then(ref).catch(handleError), get_loaders().then(ref).catch(handleError),
get_categories().then(ref).catch(handleError), get_categories().then(ref).catch(handleError),
route.query.i ? getInstance(route.query.i, true).then(ref) : Promise.resolve().then(ref),
]) ])
const installed = ref( const installed = ref(
@@ -587,4 +612,29 @@ const handleOptionsClick = (args) => {
color: var(--color-contrast); color: var(--color-contrast);
} }
} }
.small-instance {
background: var(--color-bg);
padding: var(--gap-lg);
border-radius: var(--radius-md);
margin-bottom: var(--gap-md);
.instance {
display: flex;
gap: 0.5rem;
margin-bottom: 0.5rem;
.title {
font-weight: 600;
color: var(--color-contrast);
}
}
.small-instance_info {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 0.25rem 0;
}
}
</style> </style>

View File

@@ -1,126 +0,0 @@
import { defineStore } from 'pinia'
export const useSearch = defineStore('searchStore', {
state: () => ({
searchResults: [],
searchInput: '',
totalHits: 0,
currentPage: 1,
pageCount: 1,
offset: 0,
filter: 'Relevance',
projectType: '',
facets: [],
orFacets: [],
environments: {
client: false,
server: false,
},
activeVersions: [],
openSource: false,
limit: 20,
instanceContext: null,
ignoreInstance: false,
}),
actions: {
getQueryString() {
let andFacets = [`project_type:${this.projectType === 'datapack' ? 'mod' : this.projectType}`]
if (this.instanceContext && !this.ignoreInstance) {
this.activeVersions = [this.instanceContext.metadata.game_version]
}
// Iterate through possible andFacets
this.facets.forEach((facet) => {
andFacets.push(facet)
})
// Add open source to andFacets if enabled
if (this.openSource) andFacets.push('open_source:true')
// Create andFacet string
let formattedAndFacets = ''
if (this.projectType === 'datapack') {
;[...andFacets, `categories:${encodeURIComponent('datapack')}`].forEach(
(f) => (formattedAndFacets += `["${f}"],`)
)
} else if (this.instanceContext && !this.ignoreInstance && this.projectType === 'mod') {
;[
...andFacets,
`categories:${encodeURIComponent(this.instanceContext.metadata.loader)}`,
].forEach((f) => (formattedAndFacets += `["${f}"],`))
} else {
andFacets.forEach((f) => (formattedAndFacets += `["${f}"],`))
}
formattedAndFacets = formattedAndFacets.slice(0, formattedAndFacets.length - 1)
formattedAndFacets += ''
// If orFacets are present, start building formatted orFacet filter
let formattedOrFacets = ''
if (this.orFacets.length > 0) {
formattedOrFacets += '['
this.orFacets.forEach((orF) => (formattedOrFacets += `"${orF}",`))
formattedOrFacets = formattedOrFacets.slice(0, formattedOrFacets.length - 1)
formattedOrFacets += '],'
}
// Snip normal orFacets and start version orFacets
if (this.activeVersions.length > 0) {
formattedOrFacets += '['
this.activeVersions.forEach((ver) => (formattedOrFacets += `"versions:${ver}",`))
formattedOrFacets = formattedOrFacets.slice(0, formattedOrFacets.length - 1)
formattedOrFacets += '],'
}
// Add environments to orFacets if enabled
if (this.environments.client)
formattedOrFacets += '["client_side:optional","client_side:required"]]'
if (this.environments.server)
formattedOrFacets += '["server_side:optional","server_side:required"]]'
formattedOrFacets = formattedOrFacets.slice(0, formattedOrFacets.length - 1)
// Aggregate facet query string
const facets = `&facets=[${formattedAndFacets}${
formattedOrFacets.length > 0 ? `,${formattedOrFacets}` : ''
}]`
// Configure results sorting
let indexSort
switch (this.filter) {
case 'Download count':
indexSort = 'downloads'
break
case 'Follow count':
indexSort = 'follows'
break
case 'Recently published':
indexSort = 'newest'
break
case 'Recently updated':
indexSort = 'updated'
break
default:
indexSort = 'relevance'
}
return `?query=${this.searchInput || ''}&limit=${this.limit}&offset=${this.offset || 0}${
facets || ''
}&index=${indexSort}`
},
setSearchResults(response) {
this.searchResults = [...response.hits]
this.totalHits = response.total_hits
this.offset = response.offset
this.pageCount = Math.ceil(this.totalHits / this.limit)
},
resetFilters() {
this.facets = []
this.orFacets = []
Object.keys(this.environments).forEach((env) => {
this.environments[env] = false
})
this.activeVersions = []
this.openSource = false
},
},
})

View File

@@ -1,7 +1,6 @@
import { useSearch } from './search'
import { useTheming } from './theme' import { useTheming } from './theme'
import { useBreadcrumbs } from './breadcrumbs' import { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading' import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications' import { useNotifications, handleError } from './notifications'
export { useSearch, useTheming, useBreadcrumbs, useLoading, useNotifications, handleError } export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError }