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(),
))
})?;
emit_loading(&loading_bar, 100.0, Some("Done extracting java")).await?;
emit_loading(&loading_bar, 10.0, Some("Done extracting java")).await?;
Ok(path
.join(
download

View File

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

View File

@@ -94,51 +94,43 @@ impl Drop for LoadingBarId {
let _event = LoadingBarType::StateInit;
let _message = "finished".to_string();
tokio::spawn(async move {
if let Ok(event_state) = crate::EventState::get().await {
{
let mut bars = event_state.loading_bars.write().await;
bars.remove(&loader_uuid);
if let Ok(event_state) = EventState::get().await {
let mut bars = event_state.loading_bars.write().await;
#[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(feature = "tauri")]
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();
#[cfg(not(any(feature = "tauri", feature = "cli")))]
bars.remove(&loader_uuid);
}
});
}

View File

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

View File

@@ -14,8 +14,8 @@ dependencies:
specifier: ^1.0.1
version: 1.0.1
omorphia:
specifier: ^0.4.22
version: 0.4.22
specifier: ^0.4.24
version: 0.4.24
pinia:
specifier: ^2.1.3
version: 2.1.3(vue@3.3.4)
@@ -1326,8 +1326,8 @@ packages:
ufo: 1.1.2
dev: false
/omorphia@0.4.22:
resolution: {integrity: sha512-UzG/MqOu/q+F65RZ74oCOLHE4dm716cCkX9EtSP30s8RInd8AHuax4riFKK+ANfCCtE9zcDw4AmU7fJlUnX6Xw==}
/omorphia@0.4.24:
resolution: {integrity: sha512-tYhg88wkv9yfCNF8uVDkt6MIZ5WuCEezWrWJuBpHXP0X1yNGey6ICbH0LSuNuOMtqhGJEIupGEB7uV4Db9b7uQ==}
dependencies:
dayjs: 1.11.7
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": [],
"icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",

View File

@@ -25,7 +25,7 @@
<p>Selected</p>
</div>
<Button v-tooltip="'Log out'" icon-only color="raised" @click="logout(selectedAccount.id)">
<XIcon />
<TrashIcon />
</Button>
</div>
<div v-else class="logged-out account">
@@ -41,7 +41,7 @@
<p>{{ account.username }}</p>
</Button>
<Button v-tooltip="'Log out'" icon-only @click="logout(account.id)">
<XIcon />
<TrashIcon />
</Button>
</div>
</div>
@@ -54,7 +54,7 @@
</template>
<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 {
users,

View File

@@ -3,7 +3,10 @@
<div v-for="breadcrumb in breadcrumbs" :key="breadcrumb.name" class="breadcrumbs__item">
<router-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) === '?'
? breadcrumbData.getName(breadcrumb.name.slice(1))

View File

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

View File

@@ -52,14 +52,12 @@ const currentSelected = ref({})
defineExpose({
show: async (version, currentSelectedJava) => {
if (version <= 8 && !!version) {
console.log(version)
chosenInstallOptions.value = await find_jre_8_jres().catch(handleError)
} else if (version >= 18) {
chosenInstallOptions.value = await find_jre_18plus_jres().catch(handleError)
} else if (version) {
chosenInstallOptions.value = await find_jre_17_jres().catch(handleError)
} else {
console.log('get all')
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))
await process_listener(async () => {
const unlistenProcess = await process_listener(async () => {
await refresh()
})
@@ -91,7 +91,7 @@ const goToTerminal = () => {
const currentLoadingBars = ref(Object.values(await progress_bars_list().catch(handleError)))
await loading_listener(async () => {
const unlistenLoading = await loading_listener(async () => {
await refreshInfo()
})
@@ -128,6 +128,8 @@ onMounted(() => {
onBeforeUnmount(() => {
window.removeEventListener('click', handleClickOutside)
unlistenProcess()
unlistenLoading()
})
</script>

View File

@@ -1,5 +1,13 @@
<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">
<Avatar :src="project.icon_url" size="md" class="search-icon" />
</div>
@@ -117,14 +125,7 @@ const props = defineProps({
})
const installing = ref(false)
const installed = ref(
props.instance
? Object.values(props.instance.projects).some(
(p) => p.metadata?.project?.id === props.project.project_id
)
: false
)
const installed = ref(props.project.installed)
async function install() {
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
/// Returns [u32]
export async function get_all_running_profile_paths(profile_path) {
return await invoke('process_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', { profilePath })
}
/// Gets all running process IDs with a given profile path

View File

@@ -1,12 +1,11 @@
<script setup>
import { computed, onMounted, ref, watch } from 'vue'
import { computed, nextTick, ref, readonly, shallowRef, watch } from 'vue'
import {
Pagination,
Checkbox,
Button,
ClearIcon,
SearchIcon,
DropdownSelect,
SearchFilter,
Card,
ClientIcon,
@@ -15,74 +14,337 @@ import {
formatCategoryHeader,
formatCategory,
Promotion,
DropdownSelect,
} from 'omorphia'
import Multiselect from 'vue-multiselect'
import { handleError, useSearch } from '@/store/state'
import { handleError } from '@/store/state'
import { useBreadcrumbs } from '@/store/breadcrumbs'
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 InstallConfirmModal from '@/components/ui/InstallConfirmModal.vue'
import InstanceInstallModal from '@/components/ui/InstanceInstallModal.vue'
import SplashScreen from '@/components/ui/SplashScreen.vue'
import Instance from '@/components/ui/Instance.vue'
import IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
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 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 modInstallModal = ref(null)
const incompatibilityWarningModal = ref(null)
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 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 loaders = ref([])
const availableGameVersions = ref([])
const results = shallowRef([])
const pageCount = computed(() =>
results.value ? Math.ceil(results.value.total_hits / results.value.limit) : 1
)
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (searchStore.projectType === 'modpack') {
searchStore.instanceContext = null
function getArrayOrString(x) {
if (typeof x === 'string' || x instanceof String) {
return [x]
} else {
return x
}
}
onMounted(async () => {
;[categories.value, loaders.value, availableGameVersions.value] = await Promise.all([
get_categories().catch(handleError),
get_loaders().catch(handleError),
get_game_versions().catch(handleError),
])
breadcrumbs.setContext({ name: 'Browse', link: route.path })
if (searchStore.projectType === 'modpack') {
searchStore.instanceContext = null
if (route.query.iv) {
ignoreInstanceGameVersions.value = route.query.iv === 'true'
}
if (route.query.il) {
ignoreInstanceLoaders.value = route.query.il === 'true'
}
if (route.query.i) {
instanceContext.value = await getInstance(route.query.i, true)
}
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 values = new Map()
for (const category of categories.value.filter(
(cat) =>
cat.project_type ===
(searchStore.projectType === 'datapack' ? 'mod' : searchStore.projectType)
(cat) => cat.project_type === (projectType.value === 'datapack' ? 'mod' : projectType.value)
)) {
if (!values.has(category.header)) {
values.set(category.header, [])
@@ -92,128 +354,188 @@ const sortedCategories = computed(() => {
return values
})
const getSearchResults = async () => {
const queryString = searchStore.getQueryString()
const response = await useFetch(
`https://api.modrinth.com/v2/search${queryString}`,
'search results'
)
searchStore.setSearchResults(response)
async function clearFilters() {
for (const facet of [...facets.value]) {
await toggleFacet(facet, true)
}
for (const facet of [...orFacets.value]) {
await toggleOrFacet(facet, true)
}
onlyOpenSource.value = false
selectedVersions.value = []
selectedEnvironments.value = []
await onSearchChange(1)
}
const handleReset = async () => {
searchStore.currentPage = 1
searchStore.offset = 0
searchStore.resetFilters()
await getSearchResults()
async function toggleFacet(elementName, doNotSendRequest = false) {
const index = facets.value.indexOf(elementName)
if (index !== -1) {
facets.value.splice(index, 1)
} else {
facets.value.push(elementName)
}
if (!doNotSendRequest) {
await onSearchChange(1)
}
}
const toggleFacet = async (facet) => {
searchStore.currentPage = 1
searchStore.offset = 0
const index = searchStore.facets.indexOf(facet)
async function toggleOrFacet(elementName, doNotSendRequest) {
const index = orFacets.value.indexOf(elementName)
if (index !== -1) {
orFacets.value.splice(index, 1)
} else {
orFacets.value.push(elementName)
}
if (index !== -1) searchStore.facets.splice(index, 1)
else searchStore.facets.push(facet)
await switchPage(1)
if (!doNotSendRequest) {
await onSearchChange(1)
}
}
const toggleOrFacet = async (orFacet) => {
const index = searchStore.orFacets.indexOf(orFacet)
function toggleEnv(environment, sendRequest) {
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)
else searchStore.orFacets.push(orFacet)
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()
if (!sendRequest) {
onSearchChange(1)
}
}
watch(
() => route.params.projectType,
async (projectType) => {
if (!projectType) return
searchStore.projectType = projectType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${searchStore.projectType}` })
await handleReset()
await switchPage(1)
async (newType) => {
if (!newType) return
projectType.value = newType
breadcrumbs.setContext({ name: 'Browse', link: `/browse/${projectType.value}` })
sortType.value = { display: 'Relevance', name: 'relevance' }
query.value = ''
loading.value = true
await clearFilters()
loading.value = false
}
)
const handleInstanceSwitch = async (value) => {
searchStore.ignoreInstance = value
await switchPage(1)
}
const [categories, loaders, availableGameVersions] = await Promise.all([
get_categories().catch(handleError).then(ref),
get_loaders().catch(handleError).then(ref),
get_game_versions().catch(handleError).then(ref),
refreshSearch(),
])
const selectableProjectTypes = computed(() => {
const values = [
{ label: 'Data Packs', href: `/browse/datapack` },
{ label: 'Shaders', href: `/browse/shader` },
{ label: 'Resource Packs', href: `/browse/resourcepack` },
]
if (searchStore.instanceContext) {
if (searchStore.instanceContext.metadata.loader !== 'vanilla') {
if (instanceContext.value) {
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' })
}
} else {
values.unshift({ label: 'Data Packs', href: `/browse/datapack` })
values.unshift({ label: 'Mods', href: '/browse/mod' })
values.unshift({ label: 'Modpacks', href: '/browse/modpack' })
}
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>
<template>
<div class="search-container">
<aside class="filter-panel">
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small>
<template #content>
<Checkbox
:model-value="searchStore.ignoreInstance"
:checked="searchStore.ignoreInstance"
label="Show unsupported content"
class="filter-checkbox"
@update:model-value="(value) => handleInstanceSwitch(value)"
<div v-if="instanceContext" class="small-instance">
<div class="instance">
<Avatar
:src="
!instanceContext.metadata.icon ||
(instanceContext.metadata.icon && instanceContext.metadata.icon.startsWith('http'))
? instanceContext.metadata.icon
: convertFileSrc(instanceContext.metadata.icon)
"
:alt="instanceContext.metadata.name"
size="sm"
/>
</template>
</Instance>
<div class="small-instance_info">
<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">
<Button
role="button"
:disabled="
!(
searchStore.facets.length > 0 ||
searchStore.orFacets.length > 0 ||
searchStore.environments.server === true ||
searchStore.environments.client === true ||
searchStore.openSource === true ||
searchStore.activeVersions.length > 0
)
onlyOpenSource === false &&
selectedEnvironments.length === 0 &&
selectedVersions.length === 0 &&
facets.length === 0 &&
orFacets.length === 0
"
@click="handleReset"
><ClearIcon />Clear Filters</Button
@click="clearFilters"
>
<ClearIcon /> Clear Filters
</Button>
<div v-if="showLoaders" class="loaders">
<h2>Loaders</h2>
<div
v-for="loader in loaders.filter(
(l) =>
(searchStore.projectType !== 'mod' &&
l.supported_project_types?.includes(searchStore.projectType)) ||
(searchStore.projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
(projectType !== 'mod' && l.supported_project_types?.includes(projectType)) ||
(projectType === 'mod' && ['fabric', 'forge', 'quilt'].includes(l.name))
)"
:key="loader"
>
<SearchFilter
:active-filters="searchStore.orFacets"
:active-filters="orFacets"
:icon="loader.icon"
:display-name="formatCategory(loader.name)"
:facet-name="`categories:${encodeURIComponent(loader.name)}`"
@@ -222,49 +544,11 @@ const selectableProjectTypes = computed(() => {
/>
</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">
<h2>Minecraft versions</h2>
<Checkbox v-model="showSnapshots" class="filter-checkbox" label="Include snapshots" />
<multiselect
v-model="searchStore.activeVersions"
v-model="selectedVersions"
:options="
showSnapshots
? availableGameVersions.map((x) => x.version)
@@ -279,21 +563,59 @@ const selectableProjectTypes = computed(() => {
:clear-search-on-select="false"
:show-labels="false"
placeholder="Choose versions..."
@update:model-value="getSearchResults"
@update:model-value="onSearchChange(1)"
/>
</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">
<h2>Open source</h2>
<Checkbox
v-model="searchStore.openSource"
v-model="onlyOpenSource"
label="Open source only"
class="filter-checkbox"
label="Open source"
@click="getSearchResults"
@update:model-value="onSearchChange(1)"
/>
</div>
</Card>
</aside>
<div class="search">
<div ref="searchWrapper" class="search">
<Promotion class="promotion" />
<Card class="project-type-container">
<NavRow :links="selectableProjectTypes" />
@@ -302,64 +624,59 @@ const selectableProjectTypes = computed(() => {
<div class="iconified-input">
<SearchIcon aria-hidden="true" />
<input
v-model="searchStore.searchInput"
v-model="query"
autocomplete="off"
type="text"
:placeholder="`Search ${searchStore.projectType}s...`"
@input="getSearchResults"
:placeholder="`Search ${projectType}s...`"
@input="onSearchChange(1)"
/>
</div>
<div class="inline-option">
<span>Sort by</span>
<DropdownSelect
v-model="searchStore.filter"
name="Sort dropdown"
:options="[
'Relevance',
'Download count',
'Follow count',
'Recently published',
'Recently updated',
]"
class="sort-dropdown"
@change="getSearchResults"
v-model="sortType"
name="Sort by"
:options="sortTypes"
:display-name="(option) => option?.display"
@change="onSearchChange(1)"
/>
</div>
<div class="inline-option">
<span>Show per page</span>
<DropdownSelect
v-model="searchStore.limit"
name="Limit dropdown"
v-model="maxResults"
name="Max results"
:options="[5, 10, 15, 20, 50, 100]"
:default-value="searchStore.limit.toString()"
:model-value="searchStore.limit.toString()"
:default-value="maxResults"
:model-value="maxResults"
class="limit-dropdown"
@change="getSearchResults"
@change="onSearchChange(1)"
/>
</div>
</Card>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-before"
@switch-page="onSearchChange"
/>
<SplashScreen v-if="loading" />
<section v-else class="project-list display-mode--list instance-results" role="list">
<SearchCard
v-for="result in searchStore.searchResults"
v-for="result in results.hits"
:key="result?.project_id"
:project="result"
:instance="searchStore.instanceContext"
:instance="instanceContext"
:categories="[
...categories.filter(
(cat) =>
result?.display_categories.includes(cat.name) &&
cat.project_type === searchStore.projectType
result?.display_categories.includes(cat.name) && cat.project_type === projectType
),
...loaders.filter(
(loader) =>
result?.display_categories.includes(loader.name) &&
loader.supported_project_types?.includes(searchStore.projectType)
loader.supported_project_types?.includes(projectType)
),
]"
:confirm-modal="confirmModal"
@@ -367,10 +684,12 @@ const selectableProjectTypes = computed(() => {
:incompatibility-warning-modal="incompatibilityWarningModal"
/>
</section>
<Pagination
:page="searchStore.currentPage"
:count="searchStore.pageCount"
@switch-page="switchPage"
<pagination
:page="currentPage"
:count="pageCount"
:link-function="(x) => getSearchUrl(x <= 1 ? 0 : (x - 1) * maxResults)"
class="pagination-after"
@switch-page="onSearchChangeToTop"
/>
</div>
</div>
@@ -381,6 +700,32 @@ const selectableProjectTypes = computed(() => {
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
<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 {
margin-bottom: 0.3rem;
font-size: 1rem;
@@ -492,7 +837,8 @@ const selectableProjectTypes = computed(() => {
}
.search {
margin: 0 1rem 0 21rem;
scroll-behavior: smooth;
margin: 0 1rem 0.5rem 21rem;
width: calc(100% - 22rem);
.loading {

View File

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

View File

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

View File

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

View File

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

View File

@@ -30,6 +30,7 @@
@click="
router.push({
path: `/browse/${props.instance.metadata.loader === 'vanilla' ? 'datapack' : 'mod'}`,
query: { i: $route.params.id },
})
"
>
@@ -80,7 +81,11 @@
</Button>
</div>
<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" />
{{ mod.name }}
</router-link>
@@ -305,7 +310,6 @@ async function updateProject(mod) {
async function toggleDisableMod(mod) {
mod.path = await toggle_disable_project(props.instance.path, mod.path).catch(handleError)
console.log(mod.disabled)
mod.disabled = !mod.disabled
}

View File

@@ -1,4 +1,12 @@
<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">
<div class="change-versions-modal universal-body">
<div class="input-row">
@@ -105,8 +113,8 @@
placeholder="Select categories..."
@tag="
(newTag) => {
groups.push(newTag)
availableGroups.push(newTag)
groups.push(newTag.trim().substring(0, 32))
availableGroups.push(newTag.trim().substring(0, 32))
}
"
/>
@@ -288,7 +296,7 @@
id="delete-profile"
class="btn btn-danger"
:disabled="removing"
@click="removeProfile"
@click="$refs.modal_confirm.show()"
>
<TrashIcon /> Delete
</button>
@@ -311,6 +319,7 @@ import {
XIcon,
SaveIcon,
HammerIcon,
ModalConfirm,
} from 'omorphia'
import { Multiselect } from 'vue-multiselect'
import { useRouter } from 'vue-router'
@@ -398,7 +407,8 @@ const hooks = ref(props.instance.hooks ?? globalSettings.hooks)
watch(
[
title,
groups.value,
groups,
groups,
overrideJavaInstall,
javaInstall,
overrideJavaArgs,
@@ -406,17 +416,17 @@ watch(
overrideEnvVars,
envVars,
overrideMemorySettings,
memory.value,
memory,
overrideWindowSettings,
resolution.value,
resolution,
overrideHooks,
hooks.value,
hooks,
],
async () => {
const editProfile = {
metadata: {
name: title.value,
groups: groups.value,
name: title.value.trim().substring(0, 16) ?? 'Instance',
groups: groups.value.map((x) => x.trim().substring(0, 32)).filter((x) => x.length > 0),
},
java: {},
}
@@ -424,6 +434,10 @@ watch(
if (overrideJavaInstall.value) {
if (javaInstall.value.path !== '') {
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)
}
},
{ deep: true }
)
const repairing = ref(false)

View File

@@ -1,7 +1,29 @@
<template>
<div class="root-container">
<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">
<Avatar size="lg" :src="data.icon_url" />
<div class="instance-info">
@@ -119,8 +141,8 @@
<span>Wiki</span>
</a>
<a
v-if="data.wiki_url"
:href="data.wiki_url"
v-if="data.discord_url"
:href="data.discord_url"
class="title"
rel="noopener nofollow ugc external"
>
@@ -238,7 +260,12 @@ import {
} from '@/assets/external'
import { get_categories, get_loaders } from '@/helpers/tags'
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 relativeTime from 'dayjs/plugin/relativeTime'
import { useRoute, useRouter } from 'vue-router'
@@ -246,16 +273,13 @@ import { ref, shallowRef, watch } from 'vue'
import { installVersionDependencies } from '@/helpers/utils'
import InstallConfirmModal from '@/components/ui/InstallConfirmModal.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 IncompatibilityWarningModal from '@/components/ui/IncompatibilityWarningModal.vue'
import { useFetch } from '@/helpers/fetch.js'
import { handleError } from '@/store/notifications.js'
import { convertFileSrc } from '@tauri-apps/api/tauri'
import ContextMenu from '@/components/ui/ContextMenu.vue'
const searchStore = useSearch()
const route = useRoute()
const router = useRouter()
const breadcrumbs = useBreadcrumbs()
@@ -263,11 +287,11 @@ const breadcrumbs = useBreadcrumbs()
const confirmModal = ref(null)
const modInstallModal = ref(null)
const incompatibilityWarning = ref(null)
const options = ref(null)
const instance = ref(searchStore.instanceContext)
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}/version`, 'project').then(
shallowRef
@@ -280,6 +304,7 @@ const [data, versions, members, dependencies, categories, loaders] = await Promi
),
get_loaders().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(
@@ -587,4 +612,29 @@ const handleOptionsClick = (args) => {
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>

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 { useBreadcrumbs } from './breadcrumbs'
import { useLoading } from './loading'
import { useNotifications, handleError } from './notifications'
export { useSearch, useTheming, useBreadcrumbs, useLoading, useNotifications, handleError }
export { useTheming, useBreadcrumbs, useLoading, useNotifications, handleError }