You've already forked AstralRinth
forked from didirus/AstralRinth
Non modpack wireup & Project to profile install (#90)
* Base impl * Make project type selectable * Update Browse.vue * address changes * Quick create * Run linter * fix merge * Addressed changes * Installation improvements * Run lint * resourcepacks * automatic installation of dependencies * Fix bugs with search * Addressed changes * Run linter * Fixed direct install not working * Remove back to search * Update Index.vue * Addressed some changes * Shader fix * fix resetting * Update Browse.vue * fixed install not working properly * Update Index.vue --------- Co-authored-by: Geometrically <18202329+Geometrically@users.noreply.github.com> Co-authored-by: Jai A <jaiagr+gpg@pm.me>
This commit is contained in:
@@ -50,7 +50,7 @@ list().then(
|
||||
<span v-if="!themeStore.collapsedNavigation">Home</span>
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
to="/browse"
|
||||
to="/browse/modpack"
|
||||
class="btn"
|
||||
:class="{
|
||||
'icon-only': themeStore.collapsedNavigation,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
:root {
|
||||
font-family: var(--font-standard);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
* {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import { Card } from 'omorphia'
|
||||
import { Avatar, Card } from 'omorphia'
|
||||
import { PlayIcon } from '@/assets/icons'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
|
||||
@@ -11,13 +11,32 @@ const props = defineProps({
|
||||
return {}
|
||||
},
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RouterLink :to="`/instance/${encodeURIComponent(props.instance.path)}`">
|
||||
<Card class="instance-card-item">
|
||||
<Card v-if="props.small" class="instance-small-card button-base">
|
||||
<Avatar
|
||||
:src="convertFileSrc(props.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>
|
||||
</Card>
|
||||
<Card v-else class="instance-card-item">
|
||||
<img :src="convertFileSrc(props.instance.metadata.icon)" alt="Trending mod card" />
|
||||
<div class="project-info">
|
||||
<p class="title">{{ props.instance.metadata.name }}</p>
|
||||
@@ -32,6 +51,27 @@ const props = defineProps({
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.instance-small-card {
|
||||
background-color: var(--color-bg) !important;
|
||||
padding: 1rem !important;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: min-content !important;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
|
||||
.instance-small-card__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
color: var(--color-contrast);
|
||||
font-weight: bolder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.instance-card-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
297
theseus_gui/src/components/ui/InstanceInstallModal.vue
Normal file
297
theseus_gui/src/components/ui/InstanceInstallModal.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<script setup>
|
||||
import {
|
||||
Avatar,
|
||||
Modal,
|
||||
Button,
|
||||
DownloadIcon,
|
||||
PlusIcon,
|
||||
Card,
|
||||
UploadIcon,
|
||||
XIcon,
|
||||
RightArrowIcon,
|
||||
CheckIcon,
|
||||
} from 'omorphia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { add_project_from_version as installMod, list } from '@/helpers/profile'
|
||||
import { tauri } from '@tauri-apps/api'
|
||||
import { open } from '@tauri-apps/api/dialog'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { create } from '@/helpers/profile'
|
||||
import { checkInstalled, installVersionDependencies } from '@/helpers/utils'
|
||||
const router = useRouter()
|
||||
const versions = ref([])
|
||||
const project = ref('')
|
||||
const installModal = ref(null)
|
||||
const searchFilter = ref('')
|
||||
const showCreation = ref(false)
|
||||
const icon = ref(null)
|
||||
const name = ref(null)
|
||||
const display_icon = ref(null)
|
||||
const loader = ref(null)
|
||||
const gameVersion = ref(null)
|
||||
const creatingInstance = ref(false)
|
||||
|
||||
defineExpose({
|
||||
show: (projectId, selectedVersion) => {
|
||||
project.value = projectId
|
||||
versions.value = selectedVersion
|
||||
installModal.value.show()
|
||||
searchFilter.value = ''
|
||||
},
|
||||
})
|
||||
|
||||
const profiles = ref(await list().then(Object.values))
|
||||
|
||||
async function install(instance) {
|
||||
instance.installing = true
|
||||
const version = versions.value.find((v) => {
|
||||
return (
|
||||
v.game_versions.includes(instance.metadata.game_version) &&
|
||||
(v.loaders.includes(instance.metadata.loader) || v.loaders.includes('minecraft'))
|
||||
)
|
||||
})
|
||||
|
||||
await installMod(instance.path, version.id)
|
||||
await installVersionDependencies(instance, version)
|
||||
|
||||
instance.installed = true
|
||||
instance.installing = false
|
||||
}
|
||||
|
||||
const filteredVersions = computed(() => {
|
||||
const filtered = profiles.value
|
||||
.filter((profile) => {
|
||||
return profile.metadata.name.toLowerCase().includes(searchFilter.value.toLowerCase())
|
||||
})
|
||||
.filter((profile) => {
|
||||
return (
|
||||
versions.value.flatMap((v) => v.game_versions).includes(profile.metadata.game_version) &&
|
||||
versions.value
|
||||
.flatMap((v) => v.loaders)
|
||||
.some((value) => value === profile.metadata.loader || value === 'minecraft')
|
||||
)
|
||||
})
|
||||
|
||||
filtered.map((profile) => {
|
||||
profile.installing = false
|
||||
profile.installed = checkInstalled(profile, project.value)
|
||||
})
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const toggleCreation = () => {
|
||||
showCreation.value = !showCreation.value
|
||||
name.value = null
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
gameVersion.value = null
|
||||
loader.value = null
|
||||
}
|
||||
|
||||
const upload_icon = async () => {
|
||||
icon.value = await open({
|
||||
multiple: false,
|
||||
filters: [
|
||||
{
|
||||
name: 'Image',
|
||||
extensions: ['png', 'jpeg'],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
display_icon.value = tauri.convertFileSrc(icon.value)
|
||||
}
|
||||
|
||||
const reset_icon = () => {
|
||||
icon.value = null
|
||||
display_icon.value = null
|
||||
}
|
||||
|
||||
const createInstance = async () => {
|
||||
creatingInstance.value = true
|
||||
const id = await create(
|
||||
name.value,
|
||||
versions.value[0].game_versions[0],
|
||||
versions.value[0].loaders[0] !== 'forge' ||
|
||||
versions.value[0].loaders[0] !== 'fabric' ||
|
||||
versions.value[0].loaders[0] !== 'quilt'
|
||||
? versions.value[0].loaders[0]
|
||||
: 'vanilla',
|
||||
'latest',
|
||||
icon.value
|
||||
)
|
||||
|
||||
await installMod(id, versions.value[0].id)
|
||||
|
||||
await router.push({ path: `/instance/${encodeURIComponent(id)}` })
|
||||
installModal.value.hide()
|
||||
creatingInstance.value = false
|
||||
}
|
||||
|
||||
const check_valid = computed(() => {
|
||||
return name.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal ref="installModal" header="Install mod to instance">
|
||||
<div class="modal-body">
|
||||
<input v-model="searchFilter" type="text" class="search" placeholder="Search for a profile" />
|
||||
<div class="profiles">
|
||||
<div v-for="profile in filteredVersions" :key="profile.metadata.name" class="option">
|
||||
<Button
|
||||
color="raised"
|
||||
class="profile-button"
|
||||
@click="$router.push(`/instance/${encodeURIComponent(profile.path)}`)"
|
||||
>
|
||||
<Avatar :src="convertFileSrc(profile.metadata.icon)" class="profile-image" />
|
||||
{{ profile.metadata.name }}
|
||||
</Button>
|
||||
<Button :disabled="profile.installed || profile.installing" @click="install(profile)">
|
||||
<DownloadIcon v-if="!profile.installed && !profile.installing" />
|
||||
<CheckIcon v-else-if="profile.installed" />
|
||||
{{ profile.installing ? 'Installing...' : profile.installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Card v-if="showCreation" class="creation-card">
|
||||
<div class="creation-container">
|
||||
<div class="creation-icon">
|
||||
<Avatar size="md" class="icon" :src="display_icon" />
|
||||
<div class="creation-icon__description">
|
||||
<Button @click="upload_icon()">
|
||||
<UploadIcon />
|
||||
<span class="no-wrap"> Upload Icon </span>
|
||||
</Button>
|
||||
<Button @click="reset_icon()">
|
||||
<XIcon />
|
||||
<span class="no-wrap"> Remove Icon </span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="creation-settings">
|
||||
<input v-model="name" type="text" placeholder="Name" class="creation-input" />
|
||||
<Button :disabled="creatingInstance === true || !check_valid" @click="createInstance()">
|
||||
<RightArrowIcon />
|
||||
{{ creatingInstance ? 'Creating...' : 'Create' }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="footer">
|
||||
<Button :color="showCreation ? '' : 'primary'" @click="toggleCreation()">
|
||||
<PlusIcon />
|
||||
{{ showCreation ? 'Hide New Instance' : 'Create new instance' }}
|
||||
</Button>
|
||||
<Button @click="installModal.hide()">Cancel</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.creation-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.creation-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.creation-icon {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-grow: 1;
|
||||
|
||||
.creation-icon__description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.creation-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.creation-dropdown {
|
||||
width: min-content !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.creation-settings {
|
||||
width: 100%;
|
||||
margin-left: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.profiles {
|
||||
max-height: 12rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.option {
|
||||
width: calc(100%);
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 0.5rem;
|
||||
|
||||
img {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.name {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.profile-button {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.profile-image {
|
||||
--size: 2rem !important;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -1,3 +1,6 @@
|
||||
import { add_project_from_version as installMod } from '@/helpers/profile'
|
||||
import { ofetch } from 'ofetch'
|
||||
|
||||
export const releaseColor = (releaseType) => {
|
||||
switch (releaseType) {
|
||||
case 'release':
|
||||
@@ -10,3 +13,27 @@ export const releaseColor = (releaseType) => {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
export const checkInstalled = (profile, projectId) => {
|
||||
return Object.values(profile.projects).some((p) => p.metadata?.project?.id === projectId)
|
||||
}
|
||||
|
||||
export const installVersionDependencies = async (profile, version) => {
|
||||
for (const dep of version.dependencies) {
|
||||
if (dep.version_id) {
|
||||
if (checkInstalled(profile, dep.project_id)) continue
|
||||
await installMod(profile.path, dep.version_id)
|
||||
} else {
|
||||
if (checkInstalled(profile, dep.project_id)) continue
|
||||
const depVersions = await ofetch(
|
||||
`https://api.modrinth.com/v2/project/${dep.project_id}/version`
|
||||
)
|
||||
const latest = depVersions.find(
|
||||
(v) =>
|
||||
v.game_versions.includes(profile.metadata.game_version) &&
|
||||
v.loaders.includes(profile.metadata.loader)
|
||||
)
|
||||
await installMod(profile.path, latest.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { ofetch } from 'ofetch'
|
||||
import {
|
||||
Pagination,
|
||||
@@ -14,17 +14,24 @@ import {
|
||||
ClientIcon,
|
||||
ServerIcon,
|
||||
AnimatedLogo,
|
||||
NavRow,
|
||||
formatCategoryHeader,
|
||||
} from 'omorphia'
|
||||
import Multiselect from 'vue-multiselect'
|
||||
import { useSearch } 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 Instance from '@/components/ui/Instance.vue'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const searchStore = useSearch()
|
||||
searchStore.projectType = route.params.projectType
|
||||
const showVersions = ref(true)
|
||||
const showLoaders = ref(true)
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
const route = useRoute()
|
||||
breadcrumbs.setContext({ name: 'Browse', link: route.path })
|
||||
|
||||
const showSnapshots = ref(false)
|
||||
const loading = ref(true)
|
||||
@@ -35,8 +42,39 @@ const [categories, loaders, availableGameVersions] = await Promise.all([
|
||||
get_game_versions(),
|
||||
])
|
||||
|
||||
onMounted(() => {
|
||||
breadcrumbs.setContext({ name: 'Browse', link: route.path })
|
||||
if (searchStore.projectType === 'modpack') {
|
||||
searchStore.instanceContext = null
|
||||
}
|
||||
searchStore.searchInput = ''
|
||||
handleReset()
|
||||
switchPage(1)
|
||||
})
|
||||
|
||||
const sortedCategories = computed(() => {
|
||||
const values = new Map()
|
||||
for (const category of categories.filter(
|
||||
(cat) =>
|
||||
cat.project_type ===
|
||||
(searchStore.projectType === 'datapack' ? 'mod' : searchStore.projectType)
|
||||
)) {
|
||||
if (!values.has(category.header)) {
|
||||
values.set(category.header, [])
|
||||
}
|
||||
values.get(category.header).push(category)
|
||||
}
|
||||
return values
|
||||
})
|
||||
|
||||
const getSearchResults = async (shouldLoad = false) => {
|
||||
const queryString = searchStore.getQueryString()
|
||||
if (searchStore.instanceContext) {
|
||||
showVersions.value = false
|
||||
showLoaders.value = !(
|
||||
searchStore.projectType === 'mod' || searchStore.projectType === 'resourcepack'
|
||||
)
|
||||
}
|
||||
if (shouldLoad === true) {
|
||||
loading.value = true
|
||||
}
|
||||
@@ -47,13 +85,22 @@ const getSearchResults = async (shouldLoad = false) => {
|
||||
|
||||
getSearchResults(true)
|
||||
|
||||
const handleReset = async () => {
|
||||
searchStore.currentPage = 1
|
||||
searchStore.offset = 0
|
||||
searchStore.resetFilters()
|
||||
await getSearchResults()
|
||||
}
|
||||
|
||||
const toggleFacet = async (facet) => {
|
||||
searchStore.currentPage = 1
|
||||
searchStore.offset = 0
|
||||
const index = searchStore.facets.indexOf(facet)
|
||||
|
||||
if (index !== -1) searchStore.facets.splice(index, 1)
|
||||
else searchStore.facets.push(facet)
|
||||
|
||||
await getSearchResults()
|
||||
await switchPage(1)
|
||||
}
|
||||
|
||||
const toggleOrFacet = async (orFacet) => {
|
||||
@@ -62,7 +109,7 @@ const toggleOrFacet = async (orFacet) => {
|
||||
if (index !== -1) searchStore.orFacets.splice(index, 1)
|
||||
else searchStore.orFacets.push(orFacet)
|
||||
|
||||
await getSearchResults()
|
||||
await switchPage(1)
|
||||
}
|
||||
|
||||
const switchPage = async (page) => {
|
||||
@@ -72,15 +119,21 @@ const switchPage = async (page) => {
|
||||
await getSearchResults()
|
||||
}
|
||||
|
||||
const handleReset = async () => {
|
||||
searchStore.resetFilters()
|
||||
await getSearchResults()
|
||||
}
|
||||
watch(
|
||||
() => route.params.projectType,
|
||||
async (projectType) => {
|
||||
searchStore.projectType = projectType ?? 'modpack'
|
||||
breadcrumbs.setContext({ name: 'Browse', link: route.path })
|
||||
await handleReset()
|
||||
await switchPage(1)
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-container">
|
||||
<aside class="filter-panel">
|
||||
<Instance v-if="searchStore.instanceContext" :instance="searchStore.instanceContext" small />
|
||||
<Button
|
||||
role="button"
|
||||
:disabled="
|
||||
@@ -96,12 +149,13 @@ const handleReset = async () => {
|
||||
@click="handleReset"
|
||||
><ClearIcon />Clear Filters</Button
|
||||
>
|
||||
<div class="categories">
|
||||
<h2>Categories</h2>
|
||||
<div
|
||||
v-for="category in categories.filter((cat) => cat.project_type === 'modpack')"
|
||||
:key="category.name"
|
||||
>
|
||||
<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"
|
||||
@@ -112,10 +166,20 @@ const handleReset = async () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loaders">
|
||||
<div
|
||||
v-if="
|
||||
showLoaders &&
|
||||
searchStore.projectType !== 'datapack' &&
|
||||
searchStore.projectType !== 'resourcepack' &&
|
||||
searchStore.projectType !== 'shader'
|
||||
"
|
||||
class="loaders"
|
||||
>
|
||||
<h2>Loaders</h2>
|
||||
<div
|
||||
v-for="loader in loaders.filter((l) => l.supported_project_types?.includes('modpack'))"
|
||||
v-for="loader in loaders.filter((l) =>
|
||||
l.supported_project_types?.includes(searchStore.projectType)
|
||||
)"
|
||||
:key="loader"
|
||||
>
|
||||
<SearchFilter
|
||||
@@ -128,7 +192,7 @@ const handleReset = async () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="environment">
|
||||
<div v-if="searchStore.projectType !== 'datapack'" class="environment">
|
||||
<h2>Environments</h2>
|
||||
<SearchFilter
|
||||
v-model="searchStore.environments.client"
|
||||
@@ -149,7 +213,7 @@ const handleReset = async () => {
|
||||
<ServerIcon aria-hidden="true" />
|
||||
</SearchFilter>
|
||||
</div>
|
||||
<div class="versions">
|
||||
<div v-if="showVersions" class="versions">
|
||||
<h2>Minecraft versions</h2>
|
||||
<Checkbox v-model="showSnapshots" class="filter-checkbox">Show snapshots</Checkbox>
|
||||
<multiselect
|
||||
@@ -183,6 +247,26 @@ const handleReset = async () => {
|
||||
</div>
|
||||
</aside>
|
||||
<div class="search">
|
||||
<Card class="project-type-container">
|
||||
<NavRow
|
||||
:links="
|
||||
searchStore.instanceContext
|
||||
? [
|
||||
{ label: 'Mods', href: `/browse/mod` },
|
||||
{ label: 'Datapacks', href: `/browse/datapack` },
|
||||
{ label: 'Shaders', href: `/browse/shader` },
|
||||
{ label: 'Resource Packs', href: `/browse/resourcepack` },
|
||||
]
|
||||
: [
|
||||
{ label: 'Modpacks', href: '/browse/modpack' },
|
||||
{ label: 'Mods', href: '/browse/mod' },
|
||||
{ label: 'Datapacks', href: '/browse/datapack' },
|
||||
{ label: 'Shaders', href: '/browse/shader' },
|
||||
{ label: 'Resource Packs', href: '/browse/resourcepack' },
|
||||
]
|
||||
"
|
||||
/>
|
||||
</Card>
|
||||
<Card class="search-panel-container">
|
||||
<div class="search-panel">
|
||||
<div class="iconified-input">
|
||||
@@ -190,7 +274,7 @@ const handleReset = async () => {
|
||||
<input
|
||||
v-model="searchStore.searchInput"
|
||||
type="text"
|
||||
placeholder="Search modpacks..."
|
||||
:placeholder="`Search ${searchStore.projectType}s...`"
|
||||
@input="getSearchResults"
|
||||
/>
|
||||
</div>
|
||||
@@ -243,12 +327,13 @@ const handleReset = async () => {
|
||||
:categories="[
|
||||
...categories.filter(
|
||||
(cat) =>
|
||||
result?.display_categories.includes(cat.name) && cat.project_type === 'modpack'
|
||||
result?.display_categories.includes(cat.name) &&
|
||||
cat.project_type === searchStore.projectType
|
||||
),
|
||||
...loaders.filter(
|
||||
(loader) =>
|
||||
result?.display_categories.includes(loader.name) &&
|
||||
loader.supported_project_types?.includes('modpack')
|
||||
loader.supported_project_types?.includes(searchStore.projectType)
|
||||
),
|
||||
]"
|
||||
:project-type-display="result?.project_type"
|
||||
@@ -265,6 +350,17 @@ const handleReset = async () => {
|
||||
|
||||
<style src="vue-multiselect/dist/vue-multiselect.css"></style>
|
||||
<style lang="scss">
|
||||
.project-type-dropdown {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.project-type-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.search-panel-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -318,14 +414,13 @@ const handleReset = async () => {
|
||||
|
||||
.filter-panel {
|
||||
position: fixed;
|
||||
width: 16rem;
|
||||
width: 19rem;
|
||||
background: var(--color-raised-bg);
|
||||
padding: 1rem 1rem 3rem 1rem;
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
height: fit-content;
|
||||
max-height: 100%;
|
||||
height: 100%;
|
||||
max-height: calc(100vh - 3rem);
|
||||
overflow-y: auto;
|
||||
|
||||
h2 {
|
||||
@@ -353,8 +448,8 @@ const handleReset = async () => {
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 0 1rem 0 17rem;
|
||||
width: 100%;
|
||||
margin: 0 1rem 0 20rem;
|
||||
width: calc(100% - 21rem);
|
||||
|
||||
.loading {
|
||||
margin: 2rem;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup>
|
||||
import GridDisplay from '@/components/GridDisplay.vue'
|
||||
import { shallowRef } from 'vue'
|
||||
import { list } from '@/helpers/profile.js'
|
||||
import { list } from '@/helpers/profile'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
|
||||
@@ -47,12 +47,16 @@ import { get, run } from '@/helpers/profile'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { shallowRef } from 'vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useSearch } from '@/store/search'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const route = useRoute()
|
||||
const searchStore = useSearch()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const route = useRoute()
|
||||
const instance = shallowRef(await get(route.params.id))
|
||||
searchStore.instanceContext = instance.value
|
||||
|
||||
breadcrumbs.setName('Instance', instance.value.metadata.name)
|
||||
breadcrumbs.setContext({
|
||||
name: instance.value.metadata.name,
|
||||
@@ -66,7 +70,7 @@ breadcrumbs.setContext({
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
width: 15rem;
|
||||
width: 17rem;
|
||||
}
|
||||
|
||||
Button {
|
||||
@@ -117,7 +121,7 @@ Button {
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 18rem;
|
||||
margin-left: 20rem;
|
||||
}
|
||||
|
||||
.instance-info {
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
<div class="card-row">
|
||||
<div class="iconified-input">
|
||||
<SearchIcon />
|
||||
<input v-model="searchFilter" type="text" placeholder="Search Mods" />
|
||||
<input v-model="searchFilter" type="text" placeholder="Search Mods" class="text-input" />
|
||||
</div>
|
||||
<span class="manage">
|
||||
<span class="text-combo">
|
||||
Sort By
|
||||
<span class="no-wrap sort"> Sort By </span>
|
||||
<DropdownSelect
|
||||
v-model="sortFilter"
|
||||
name="sort-by"
|
||||
@@ -16,9 +16,9 @@
|
||||
class="dropdown"
|
||||
/>
|
||||
</span>
|
||||
<Button color="primary">
|
||||
<Button color="primary" @click="searchMod()">
|
||||
<PlusIcon />
|
||||
Add Mods
|
||||
<span class="no-wrap"> Add Content </span>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@
|
||||
<UpdatedIcon />
|
||||
</Button>
|
||||
<Button v-else disabled icon-only>
|
||||
<CheckCircleIcon />
|
||||
<CheckIcon />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="table-cell table-text name-cell">
|
||||
@@ -77,13 +77,17 @@ import {
|
||||
TrashIcon,
|
||||
PlusIcon,
|
||||
Card,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
SearchIcon,
|
||||
UpdatedIcon,
|
||||
DropdownSelect,
|
||||
} from 'omorphia'
|
||||
import { computed, ref, shallowRef } from 'vue'
|
||||
import { convertFileSrc } from '@tauri-apps/api/tauri'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
instance: {
|
||||
@@ -142,7 +146,7 @@ const search = computed(() => {
|
||||
return updateSort(filtered, sortFilter.value)
|
||||
})
|
||||
|
||||
function updateSort(projects, sort) {
|
||||
const updateSort = (projects, sort) => {
|
||||
switch (sort) {
|
||||
case 'Version':
|
||||
return projects.slice().sort((a, b) => {
|
||||
@@ -176,9 +180,17 @@ function updateSort(projects, sort) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const searchMod = () => {
|
||||
router.push({ path: '/browse/mod', query: { instance: route.params.id } })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.text-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.manage {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
@@ -219,4 +231,14 @@ function updateSort(projects, sort) {
|
||||
.dropdown {
|
||||
width: 7rem !important;
|
||||
}
|
||||
|
||||
.no-wrap {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&.sort {
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div class="root-container">
|
||||
<div v-if="data" class="project-sidebar">
|
||||
<Instance v-if="instance" :instance="instance" small />
|
||||
<Card class="sidebar-card">
|
||||
<Avatar size="lg" :src="data.icon_url" />
|
||||
<div class="instance-info">
|
||||
@@ -29,9 +30,15 @@
|
||||
</Categories>
|
||||
<hr class="card-divider" />
|
||||
<div class="button-group">
|
||||
<Button color="primary" class="instance-button" @click="install(versions[0].id)">
|
||||
<DownloadIcon />
|
||||
Install
|
||||
<Button
|
||||
color="primary"
|
||||
class="instance-button"
|
||||
:disabled="installed === true || installing === true"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/${data.project_type}/${data.slug}`"
|
||||
@@ -40,7 +47,7 @@
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Website
|
||||
Site
|
||||
</a>
|
||||
</div>
|
||||
<hr class="card-divider" />
|
||||
@@ -174,10 +181,12 @@
|
||||
:members="members"
|
||||
:dependencies="dependencies"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InstallConfirmModal ref="confirmModal" />
|
||||
<InstanceInstallModal ref="modInstallModal" />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -200,6 +209,7 @@ import {
|
||||
CodeIcon,
|
||||
formatNumber,
|
||||
ExternalIcon,
|
||||
CheckIcon,
|
||||
} from 'omorphia'
|
||||
import {
|
||||
BuyMeACoffeeIcon,
|
||||
@@ -210,23 +220,33 @@ import {
|
||||
OpenCollectiveIcon,
|
||||
} from '@/assets/external'
|
||||
import { get_categories, get_loaders } from '@/helpers/tags'
|
||||
import { install as pack_install } from '@/helpers/pack'
|
||||
import { list } from '@/helpers/profile'
|
||||
import { install as packInstall } from '@/helpers/pack'
|
||||
import { list, add_project_from_version as installMod } from '@/helpers/profile'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ofetch } from 'ofetch'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { checkInstalled, 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'
|
||||
|
||||
const searchStore = useSearch()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const confirmModal = ref(null)
|
||||
const modInstallModal = ref(null)
|
||||
const loaders = ref(await get_loaders())
|
||||
const categories = ref(await get_categories())
|
||||
const instance = ref(searchStore.instanceContext)
|
||||
const installing = ref(false)
|
||||
|
||||
const [data, versions, members, dependencies] = await Promise.all([
|
||||
ofetch(`https://api.modrinth.com/v2/project/${route.params.id}`).then(shallowRef),
|
||||
ofetch(`https://api.modrinth.com/v2/project/${route.params.id}/version`).then(shallowRef),
|
||||
@@ -234,6 +254,8 @@ const [data, versions, members, dependencies] = await Promise.all([
|
||||
ofetch(`https://api.modrinth.com/v2/project/${route.params.id}/dependencies`).then(shallowRef),
|
||||
])
|
||||
|
||||
const installed = ref(instance.value && checkInstalled(instance.value, data.value.id))
|
||||
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
|
||||
watch(
|
||||
@@ -246,6 +268,21 @@ watch(
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
async function install(version) {
|
||||
installing.value = true
|
||||
let queuedVersionData
|
||||
|
||||
if (version) {
|
||||
queuedVersionData = versions.value.find((v) => v.id === version)
|
||||
} else {
|
||||
if (data.value.project_type === 'modpack' || !instance.value) {
|
||||
queuedVersionData = versions.value[0]
|
||||
} else {
|
||||
queuedVersionData = versions.value.find((v) =>
|
||||
v.game_versions.includes(data.value.game_versions[0])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.value.project_type === 'modpack') {
|
||||
const packs = Object.values(await list())
|
||||
if (
|
||||
@@ -254,12 +291,48 @@ async function install(version) {
|
||||
.map((value) => value.metadata)
|
||||
.find((pack) => pack.linked_data?.project_id === data.value.id)
|
||||
) {
|
||||
let id = await pack_install(version)
|
||||
let id = await packInstall(queuedVersionData.id)
|
||||
await router.push({ path: `/instance/${encodeURIComponent(id)}` })
|
||||
} else {
|
||||
confirmModal.value.show(version)
|
||||
confirmModal.value.show(queuedVersionData.id)
|
||||
}
|
||||
} else {
|
||||
if (instance.value) {
|
||||
if (!version) {
|
||||
const gameVersion = instance.value.metadata.game_version
|
||||
const loader = instance.value.metadata.loader
|
||||
const selectedVersion = versions.value.find(
|
||||
(v) =>
|
||||
v.game_versions.includes(gameVersion) &&
|
||||
(data.value.project_type === 'mod'
|
||||
? v.loaders.includes(loader) || v.loaders.includes('minecraft')
|
||||
: true)
|
||||
)
|
||||
if (!selectedVersion) {
|
||||
installing.value = false
|
||||
return
|
||||
}
|
||||
queuedVersionData = selectedVersion
|
||||
await installMod(instance.value.path, selectedVersion.id)
|
||||
} else {
|
||||
await installMod(instance.value.path, queuedVersionData.id)
|
||||
}
|
||||
|
||||
installVersionDependencies(instance.value, queuedVersionData)
|
||||
|
||||
installed.value = true
|
||||
} else {
|
||||
if (version) {
|
||||
modInstallModal.value.show(data.value.id, [
|
||||
versions.value.find((v) => v.id === queuedVersionData.id),
|
||||
])
|
||||
} else {
|
||||
modInstallModal.value.show(data.value.id, versions.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
installing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -271,8 +344,12 @@ async function install(version) {
|
||||
}
|
||||
|
||||
.project-sidebar {
|
||||
position: fixed;
|
||||
width: 20rem;
|
||||
min-width: 20rem;
|
||||
min-height: 100vh;
|
||||
height: fit-content;
|
||||
max-height: 100vh;
|
||||
overflow-y: auto;
|
||||
background: var(--color-raised-bg);
|
||||
padding: 1rem;
|
||||
}
|
||||
@@ -289,6 +366,7 @@ async function install(version) {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-left: 20rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
@@ -398,4 +476,15 @@ async function install(version) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.install-loading {
|
||||
scale: 0.2;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: -1rem;
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,9 +6,10 @@
|
||||
<span v-if="version.featured">Auto-Featured</span>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button color="primary" :action="() => install(version.id)">
|
||||
<DownloadIcon />
|
||||
Install
|
||||
<Button color="primary" :action="() => install(version.id)" :disabled="installed">
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
<Button :link="`/project/${route.params.id}/versions`">
|
||||
<LeftArrowIcon />
|
||||
@@ -57,9 +58,11 @@
|
||||
v-if="project.project_type !== 'modpack' || file.primary"
|
||||
class="download"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installed"
|
||||
>
|
||||
<DownloadIcon />
|
||||
Install
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
</Card>
|
||||
</Card>
|
||||
@@ -177,6 +180,7 @@ import {
|
||||
Badge,
|
||||
ExternalIcon,
|
||||
CopyCode,
|
||||
CheckIcon,
|
||||
formatBytes,
|
||||
renderString,
|
||||
} from 'omorphia'
|
||||
@@ -210,6 +214,10 @@ const props = defineProps({
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const version = ref(props.versions.find((version) => version.id === route.params.version))
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
:disabled="!filterLoader && !filterVersions && !filterCompatible"
|
||||
:action="clearFilters"
|
||||
>
|
||||
<CheckCircleIcon />
|
||||
<ClearIcon />
|
||||
Clear Filters
|
||||
</Button>
|
||||
</div>
|
||||
@@ -65,8 +65,14 @@
|
||||
@click="$router.push(`/project/${$route.params.id}/version/${version.id}`)"
|
||||
>
|
||||
<div class="table-cell table-text">
|
||||
<Button color="primary" icon-only @click.stop="() => install(version.id)">
|
||||
<DownloadIcon />
|
||||
<Button
|
||||
color="primary"
|
||||
icon-only
|
||||
:disabled="installed"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
</Button>
|
||||
</div>
|
||||
<div class="name-cell table-cell table-text">
|
||||
@@ -126,7 +132,8 @@
|
||||
import {
|
||||
Card,
|
||||
Button,
|
||||
CheckCircleIcon,
|
||||
CheckIcon,
|
||||
ClearIcon,
|
||||
Badge,
|
||||
DownloadIcon,
|
||||
Checkbox,
|
||||
@@ -155,6 +162,10 @@ defineProps({
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export default new createRouter({
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/browse',
|
||||
path: '/browse/:projectType',
|
||||
name: 'Browse',
|
||||
component: Pages.Browse,
|
||||
meta: {
|
||||
|
||||
@@ -9,6 +9,7 @@ export const useSearch = defineStore('searchStore', {
|
||||
pageCount: 1,
|
||||
offset: 0,
|
||||
filter: 'Relevance',
|
||||
projectType: '',
|
||||
facets: [],
|
||||
orFacets: [],
|
||||
environments: {
|
||||
@@ -18,10 +19,15 @@ export const useSearch = defineStore('searchStore', {
|
||||
activeVersions: [],
|
||||
openSource: false,
|
||||
limit: 20,
|
||||
instanceContext: null,
|
||||
}),
|
||||
actions: {
|
||||
getQueryString() {
|
||||
let andFacets = ['project_type:modpack']
|
||||
let andFacets = [`project_type:${this.projectType === 'datapack' ? 'mod' : this.projectType}`]
|
||||
|
||||
if (this.instanceContext) {
|
||||
this.activeVersions = [this.instanceContext.metadata.game_version]
|
||||
}
|
||||
|
||||
// Iterate through possible andFacets
|
||||
this.facets.forEach((facet) => {
|
||||
@@ -32,7 +38,18 @@ export const useSearch = defineStore('searchStore', {
|
||||
|
||||
// Create andFacet string
|
||||
let formattedAndFacets = ''
|
||||
andFacets.forEach((f) => (formattedAndFacets += `["${f}"],`))
|
||||
if (this.projectType === 'datapack') {
|
||||
;[...andFacets, `categories:${encodeURIComponent('datapack')}`].forEach(
|
||||
(f) => (formattedAndFacets += `["${f}"],`)
|
||||
)
|
||||
} else if (this.instanceContext && 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 += ''
|
||||
|
||||
|
||||
Reference in New Issue
Block a user