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:
Adrian O.V
2023-05-08 19:27:27 -04:00
committed by GitHub
parent 65c1942037
commit b094a30677
14 changed files with 675 additions and 64 deletions

View File

@@ -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;

View File

@@ -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'

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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>

View File

@@ -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))

View File

@@ -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>