You've already forked AstralRinth
forked from didirus/AstralRinth
refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs * fix: prettier frontend * feat: config changes * fix: lint issues * fix: lint * fix: type imports * fix: cyclical import issue * fix: lockfile * fix: missing dep * fix: switch to tabs * fix: continue switch to tabs * fix: rustfmt parity * fix: moderation lint issue * fix: lint issues * fix: ui intl * fix: lint issues * Revert "fix: rustfmt parity" This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711. * feat: revert last rs
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Changelog',
|
||||
name: 'Changelog',
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
<template>
|
||||
<Card>
|
||||
<ProjectPageDescription :description="project.body" />
|
||||
</Card>
|
||||
<Card>
|
||||
<ProjectPageDescription :description="project.body" />
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { Card, ProjectPageDescription } from '@modrinth/ui'
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Description',
|
||||
name: 'Description',
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,106 +1,107 @@
|
||||
<template>
|
||||
<div class="gallery">
|
||||
<Card v-for="(image, index) in project.gallery" :key="image.url" class="gallery-item">
|
||||
<a @click="expandImage(image, index)">
|
||||
<img :src="image.url" :alt="image.title" class="gallery-image" />
|
||||
</a>
|
||||
<div class="gallery-body">
|
||||
<h3>{{ image.title }}</h3>
|
||||
{{ image.description }}
|
||||
</div>
|
||||
<span class="gallery-time">
|
||||
<CalendarIcon />
|
||||
{{
|
||||
new Date(image.created).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
|
||||
<div class="content">
|
||||
<img
|
||||
class="image"
|
||||
:class="{ 'zoomed-in': zoomedIn }"
|
||||
:src="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||
@click.stop="() => {}"
|
||||
/>
|
||||
<div class="gallery">
|
||||
<Card v-for="(image, index) in project.gallery" :key="image.url" class="gallery-item">
|
||||
<a @click="expandImage(image, index)">
|
||||
<img :src="image.url" :alt="image.title" class="gallery-image" />
|
||||
</a>
|
||||
<div class="gallery-body">
|
||||
<h3>{{ image.title }}</h3>
|
||||
{{ image.description }}
|
||||
</div>
|
||||
<span class="gallery-time">
|
||||
<CalendarIcon />
|
||||
{{
|
||||
new Date(image.created).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</Card>
|
||||
</div>
|
||||
<div v-if="expandedGalleryItem" class="expanded-image-modal" @click="hideImage">
|
||||
<div class="content">
|
||||
<img
|
||||
class="image"
|
||||
:class="{ 'zoomed-in': zoomedIn }"
|
||||
:src="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
:alt="expandedGalleryItem.title ? expandedGalleryItem.title : 'gallery-image'"
|
||||
@click.stop="() => {}"
|
||||
/>
|
||||
|
||||
<div class="floating" @click.stop="() => {}">
|
||||
<div class="text">
|
||||
<h2 v-if="expandedGalleryItem.title">
|
||||
{{ expandedGalleryItem.title }}
|
||||
</h2>
|
||||
<p v-if="expandedGalleryItem.description">
|
||||
{{ expandedGalleryItem.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<Button class="close" icon-only @click="hideImage">
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
<Button icon-only @click="zoomedIn = !zoomedIn">
|
||||
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||
<ContractIcon v-else aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="project.gallery.length > 1"
|
||||
class="previous"
|
||||
icon-only
|
||||
@click="previousImage()"
|
||||
>
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="project.gallery.length > 1" class="next" icon-only @click="nextImage()">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="floating" @click.stop="() => {}">
|
||||
<div class="text">
|
||||
<h2 v-if="expandedGalleryItem.title">
|
||||
{{ expandedGalleryItem.title }}
|
||||
</h2>
|
||||
<p v-if="expandedGalleryItem.description">
|
||||
{{ expandedGalleryItem.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="controls">
|
||||
<div class="buttons">
|
||||
<Button class="close" icon-only @click="hideImage">
|
||||
<XIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<a
|
||||
class="open btn icon-only"
|
||||
target="_blank"
|
||||
:href="
|
||||
expandedGalleryItem.raw_url
|
||||
? expandedGalleryItem.raw_url
|
||||
: 'https://cdn.modrinth.com/placeholder-banner.svg'
|
||||
"
|
||||
>
|
||||
<ExternalIcon aria-hidden="true" />
|
||||
</a>
|
||||
<Button icon-only @click="zoomedIn = !zoomedIn">
|
||||
<ExpandIcon v-if="!zoomedIn" aria-hidden="true" />
|
||||
<ContractIcon v-else aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="project.gallery.length > 1"
|
||||
class="previous"
|
||||
icon-only
|
||||
@click="previousImage()"
|
||||
>
|
||||
<LeftArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
<Button v-if="project.gallery.length > 1" class="next" icon-only @click="nextImage()">
|
||||
<RightArrowIcon aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
ExpandIcon,
|
||||
RightArrowIcon,
|
||||
LeftArrowIcon,
|
||||
ExternalIcon,
|
||||
ContractIcon,
|
||||
XIcon,
|
||||
CalendarIcon,
|
||||
CalendarIcon,
|
||||
ContractIcon,
|
||||
ExpandIcon,
|
||||
ExternalIcon,
|
||||
LeftArrowIcon,
|
||||
RightArrowIcon,
|
||||
XIcon,
|
||||
} from '@modrinth/assets'
|
||||
import { Button, Card } from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { hide_ads_window, show_ads_window } from '@/helpers/ads.js'
|
||||
import { trackEvent } from '@/helpers/analytics'
|
||||
import { show_ads_window, hide_ads_window } from '@/helpers/ads.js'
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
})
|
||||
|
||||
const expandedGalleryItem = ref(null)
|
||||
@@ -108,227 +109,227 @@ const expandedGalleryIndex = ref(0)
|
||||
const zoomedIn = ref(false)
|
||||
|
||||
const hideImage = () => {
|
||||
expandedGalleryItem.value = null
|
||||
show_ads_window()
|
||||
expandedGalleryItem.value = null
|
||||
show_ads_window()
|
||||
}
|
||||
|
||||
const nextImage = () => {
|
||||
expandedGalleryIndex.value++
|
||||
if (expandedGalleryIndex.value >= props.project.gallery.length) {
|
||||
expandedGalleryIndex.value = 0
|
||||
}
|
||||
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
|
||||
trackEvent('GalleryImageNext', {
|
||||
project_id: props.project.id,
|
||||
url: expandedGalleryItem.value.url,
|
||||
})
|
||||
expandedGalleryIndex.value++
|
||||
if (expandedGalleryIndex.value >= props.project.gallery.length) {
|
||||
expandedGalleryIndex.value = 0
|
||||
}
|
||||
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
|
||||
trackEvent('GalleryImageNext', {
|
||||
project_id: props.project.id,
|
||||
url: expandedGalleryItem.value.url,
|
||||
})
|
||||
}
|
||||
|
||||
const previousImage = () => {
|
||||
expandedGalleryIndex.value--
|
||||
if (expandedGalleryIndex.value < 0) {
|
||||
expandedGalleryIndex.value = props.project.gallery.length - 1
|
||||
}
|
||||
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
|
||||
trackEvent('GalleryImagePrevious', {
|
||||
project_id: props.project.id,
|
||||
url: expandedGalleryItem.value,
|
||||
})
|
||||
expandedGalleryIndex.value--
|
||||
if (expandedGalleryIndex.value < 0) {
|
||||
expandedGalleryIndex.value = props.project.gallery.length - 1
|
||||
}
|
||||
expandedGalleryItem.value = props.project.gallery[expandedGalleryIndex.value]
|
||||
trackEvent('GalleryImagePrevious', {
|
||||
project_id: props.project.id,
|
||||
url: expandedGalleryItem.value,
|
||||
})
|
||||
}
|
||||
|
||||
const expandImage = (item, index) => {
|
||||
hide_ads_window()
|
||||
expandedGalleryItem.value = item
|
||||
expandedGalleryIndex.value = index
|
||||
zoomedIn.value = false
|
||||
hide_ads_window()
|
||||
expandedGalleryItem.value = item
|
||||
expandedGalleryIndex.value = index
|
||||
zoomedIn.value = false
|
||||
|
||||
trackEvent('GalleryImageExpand', {
|
||||
project_id: props.project.id,
|
||||
url: item.url,
|
||||
})
|
||||
trackEvent('GalleryImageExpand', {
|
||||
project_id: props.project.id,
|
||||
url: item.url,
|
||||
})
|
||||
}
|
||||
|
||||
function keyListener(e) {
|
||||
if (expandedGalleryItem.value) {
|
||||
e.preventDefault()
|
||||
if (e.key === 'Escape') {
|
||||
hideImage()
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
previousImage()
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextImage()
|
||||
}
|
||||
}
|
||||
if (expandedGalleryItem.value) {
|
||||
e.preventDefault()
|
||||
if (e.key === 'Escape') {
|
||||
hideImage()
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
previousImage()
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
nextImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
document.addEventListener('keypress', keyListener)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(20rem, 1fr));
|
||||
width: 100%;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.gallery-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 2/1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
.gallery-image {
|
||||
width: 100%;
|
||||
aspect-ratio: 2/1;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.gallery-body {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
.gallery-body {
|
||||
flex-grow: 1;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.gallery-time {
|
||||
padding: 0 1rem 1rem;
|
||||
vertical-align: center;
|
||||
}
|
||||
.gallery-time {
|
||||
padding: 0 1rem 1rem;
|
||||
vertical-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.expanded-image-modal {
|
||||
position: fixed;
|
||||
z-index: 11;
|
||||
overflow: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000000;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: fixed;
|
||||
z-index: 11;
|
||||
overflow: auto;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000000;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
width: calc(100vw - 2 * var(--gap-lg));
|
||||
height: calc(100vh - 2 * var(--gap-lg));
|
||||
.content {
|
||||
position: relative;
|
||||
width: calc(100vw - 2 * var(--gap-lg));
|
||||
height: calc(100vh - 2 * var(--gap-lg));
|
||||
|
||||
.circle-button {
|
||||
padding: 0.5rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
max-width: 2rem;
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-max);
|
||||
margin: 0;
|
||||
box-shadow: inset 0px -1px 1px rgb(17 24 39 / 10%);
|
||||
.circle-button {
|
||||
padding: 0.5rem;
|
||||
line-height: 1;
|
||||
display: flex;
|
||||
max-width: 2rem;
|
||||
color: var(--color-button-text);
|
||||
background-color: var(--color-button-bg);
|
||||
border-radius: var(--size-rounded-max);
|
||||
margin: 0;
|
||||
box-shadow: inset 0px -1px 1px rgb(17 24 39 / 10%);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover) !important;
|
||||
&:hover {
|
||||
background-color: var(--color-button-bg-hover) !important;
|
||||
|
||||
svg {
|
||||
color: var(--color-button-text-hover) !important;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
color: var(--color-button-text-hover) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active) !important;
|
||||
&:active {
|
||||
background-color: var(--color-button-bg-active) !important;
|
||||
|
||||
svg {
|
||||
color: var(--color-button-text-active) !important;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
color: var(--color-button-text-active) !important;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
svg {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.image {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: calc(100vw - 2 * var(--gap-lg));
|
||||
max-height: calc(100vh - 2 * var(--gap-lg));
|
||||
border-radius: var(--radius-lg);
|
||||
.image {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
max-width: calc(100vw - 2 * var(--gap-lg));
|
||||
max-height: calc(100vh - 2 * var(--gap-lg));
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
&.zoomed-in {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
height: calc(100vh - 2 * var(--gap-lg));
|
||||
max-width: calc(100vw - 2 * var(--gap-lg));
|
||||
}
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: var(--gap-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
opacity: 1;
|
||||
padding: 2rem 2rem 0 2rem;
|
||||
&.zoomed-in {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
height: calc(100vh - 2 * var(--gap-lg));
|
||||
max-width: calc(100vw - 2 * var(--gap-lg));
|
||||
}
|
||||
}
|
||||
.floating {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
bottom: var(--gap-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--gap-md);
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
opacity: 1;
|
||||
padding: 2rem 2rem 0 2rem;
|
||||
|
||||
&:not(&:hover) {
|
||||
opacity: 0.4;
|
||||
.text {
|
||||
transform: translateY(2.5rem) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
.controls {
|
||||
transform: translateY(0.25rem) scale(0.9);
|
||||
}
|
||||
}
|
||||
&:not(&:hover) {
|
||||
opacity: 0.4;
|
||||
.text {
|
||||
transform: translateY(2.5rem) scale(0.8);
|
||||
opacity: 0;
|
||||
}
|
||||
.controls {
|
||||
transform: translateY(0.25rem) scale(0.9);
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 40rem;
|
||||
transition:
|
||||
opacity 0.25s ease-in-out,
|
||||
transform 0.25s ease-in-out;
|
||||
text-shadow: 1px 1px 10px #000000d4;
|
||||
margin-bottom: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
.text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 40rem;
|
||||
transition:
|
||||
opacity 0.25s ease-in-out,
|
||||
transform 0.25s ease-in-out;
|
||||
text-shadow: 1px 1px 10px #000000d4;
|
||||
margin-bottom: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
h2 {
|
||||
color: var(--dark-color-base);
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
h2 {
|
||||
color: var(--dark-color-base);
|
||||
font-size: 1.25rem;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--dark-color-base);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.controls {
|
||||
background-color: var(--color-raised-bg);
|
||||
padding: var(--gap-md);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
opacity 0.25s ease-in-out,
|
||||
transform 0.25s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
color: var(--dark-color-base);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
.controls {
|
||||
background-color: var(--color-raised-bg);
|
||||
padding: var(--gap-md);
|
||||
border-radius: var(--radius-md);
|
||||
transition:
|
||||
opacity 0.25s ease-in-out,
|
||||
transform 0.25s ease-in-out;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,134 +1,162 @@
|
||||
<template>
|
||||
<div>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<ProjectSidebarCompatibility
|
||||
:project="data"
|
||||
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
link-target="_blank"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
:project="data"
|
||||
:has-versions="versions.length > 0"
|
||||
:link-target="`_blank`"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
to="#background-teleport-target"
|
||||
>
|
||||
<ProjectBackgroundGradient :project="data" />
|
||||
</Teleport>
|
||||
<ProjectHeader :project="data" @contextmenu.prevent.stop="handleRightClick">
|
||||
<template #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installed ? `This project is already installed` : null"
|
||||
:disabled="installed || installing"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: {
|
||||
path: `/project/${$route.params.id}/versions`,
|
||||
query: { l: instance?.loader, g: instance?.game_version },
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> Project data couldn't not be loaded. </template>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
<div>
|
||||
<Teleport to="#sidebar-teleport-target">
|
||||
<ProjectSidebarCompatibility
|
||||
:project="data"
|
||||
:tags="{ loaders: allLoaders, gameVersions: allGameVersions }"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarLinks link-target="_blank" :project="data" class="project-sidebar-section" />
|
||||
<ProjectSidebarCreators
|
||||
:organization="null"
|
||||
:members="members"
|
||||
:org-link="(slug) => `https://modrinth.com/organization/${slug}`"
|
||||
:user-link="(username) => `https://modrinth.com/user/${username}`"
|
||||
link-target="_blank"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
<ProjectSidebarDetails
|
||||
:project="data"
|
||||
:has-versions="versions.length > 0"
|
||||
:link-target="`_blank`"
|
||||
class="project-sidebar-section"
|
||||
/>
|
||||
</Teleport>
|
||||
<div class="flex flex-col gap-4 p-6">
|
||||
<InstanceIndicator v-if="instance" :instance="instance" />
|
||||
<template v-if="data">
|
||||
<Teleport
|
||||
v-if="themeStore.featureFlags.project_background"
|
||||
to="#background-teleport-target"
|
||||
>
|
||||
<ProjectBackgroundGradient :project="data" />
|
||||
</Teleport>
|
||||
<ProjectHeader :project="data" @contextmenu.prevent.stop="handleRightClick">
|
||||
<template #actions>
|
||||
<ButtonStyled size="large" color="brand">
|
||||
<button
|
||||
v-tooltip="installed ? `This project is already installed` : null"
|
||||
:disabled="installed || installing"
|
||||
@click="install(null)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed && !installing" />
|
||||
<CheckIcon v-else-if="installed" />
|
||||
{{ installing ? 'Installing...' : installed ? 'Installed' : 'Install' }}
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled size="large" circular type="transparent">
|
||||
<OverflowMenu
|
||||
:tooltip="`More options`"
|
||||
:options="[
|
||||
{
|
||||
id: 'follow',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'save',
|
||||
disabled: true,
|
||||
tooltip: 'Coming soon',
|
||||
action: () => {},
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${data.project_type}/${data.slug}`,
|
||||
external: true,
|
||||
},
|
||||
{
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
id: 'report',
|
||||
color: 'red',
|
||||
hoverFilled: true,
|
||||
link: `https://modrinth.com/report?item=project&itemID=${data.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
<template #follow> <HeartIcon /> Follow </template>
|
||||
<template #save> <BookmarkIcon /> Save </template>
|
||||
<template #report> <ReportIcon /> Report </template>
|
||||
</OverflowMenu>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectHeader>
|
||||
<NavTabs
|
||||
:links="[
|
||||
{
|
||||
label: 'Description',
|
||||
href: `/project/${$route.params.id}`,
|
||||
},
|
||||
{
|
||||
label: 'Versions',
|
||||
href: {
|
||||
path: `/project/${$route.params.id}/versions`,
|
||||
query: { l: instance?.loader, g: instance?.game_version },
|
||||
},
|
||||
subpages: ['version'],
|
||||
},
|
||||
{
|
||||
label: 'Gallery',
|
||||
href: `/project/${$route.params.id}/gallery`,
|
||||
shown: data.gallery.length > 0,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<RouterView
|
||||
:project="data"
|
||||
:versions="versions"
|
||||
:members="members"
|
||||
:instance="instance"
|
||||
:install="install"
|
||||
:installed="installed"
|
||||
:installing="installing"
|
||||
:installed-version="installedVersion"
|
||||
/>
|
||||
</template>
|
||||
<template v-else> Project data couldn't not be loaded. </template>
|
||||
</div>
|
||||
<ContextMenu ref="options" @option-clicked="handleOptionsClick">
|
||||
<template #install> <DownloadIcon /> Install </template>
|
||||
<template #open_link> <GlobeIcon /> Open in Modrinth <ExternalIcon /> </template>
|
||||
<template #copy_link> <ClipboardCopyIcon /> Copy link </template>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
GlobeIcon,
|
||||
HeartIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectSidebarLinks,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import ContextMenu from '@/components/ui/ContextMenu.vue'
|
||||
import InstanceIndicator from '@/components/ui/InstanceIndicator.vue'
|
||||
import NavTabs from '@/components/ui/NavTabs.vue'
|
||||
@@ -138,33 +166,6 @@ import { get_categories, get_game_versions, get_loaders } from '@/helpers/tags'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
import { install as installVersion } from '@/store/install.js'
|
||||
import { useTheming } from '@/store/state.js'
|
||||
import {
|
||||
BookmarkIcon,
|
||||
CheckIcon,
|
||||
ClipboardCopyIcon,
|
||||
DownloadIcon,
|
||||
ExternalIcon,
|
||||
GlobeIcon,
|
||||
HeartIcon,
|
||||
MoreVerticalIcon,
|
||||
ReportIcon,
|
||||
} from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
ProjectBackgroundGradient,
|
||||
ProjectHeader,
|
||||
ProjectSidebarCompatibility,
|
||||
ProjectSidebarCreators,
|
||||
ProjectSidebarDetails,
|
||||
ProjectSidebarLinks,
|
||||
injectNotificationManager,
|
||||
} from '@modrinth/ui'
|
||||
import { openUrl } from '@tauri-apps/plugin-opener'
|
||||
import dayjs from 'dayjs'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
@@ -186,266 +187,266 @@ const installed = ref(false)
|
||||
const installedVersion = ref(null)
|
||||
|
||||
const [allLoaders, allGameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
|
||||
async function fetchProjectData() {
|
||||
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
||||
const project = await get_project(route.params.id, 'must_revalidate').catch(handleError)
|
||||
|
||||
if (!project) {
|
||||
handleError('Error loading project')
|
||||
return
|
||||
}
|
||||
if (!project) {
|
||||
handleError('Error loading project')
|
||||
return
|
||||
}
|
||||
|
||||
data.value = project
|
||||
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
|
||||
await Promise.all([
|
||||
get_version_many(project.versions, 'must_revalidate').catch(handleError),
|
||||
get_team(project.team).catch(handleError),
|
||||
get_categories().catch(handleError),
|
||||
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
|
||||
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
|
||||
])
|
||||
data.value = project
|
||||
;[versions.value, members.value, categories.value, instance.value, instanceProjects.value] =
|
||||
await Promise.all([
|
||||
get_version_many(project.versions, 'must_revalidate').catch(handleError),
|
||||
get_team(project.team).catch(handleError),
|
||||
get_categories().catch(handleError),
|
||||
route.query.i ? getInstance(route.query.i).catch(handleError) : Promise.resolve(),
|
||||
route.query.i ? getInstanceProjects(route.query.i).catch(handleError) : Promise.resolve(),
|
||||
])
|
||||
|
||||
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
|
||||
versions.value = versions.value.sort((a, b) => dayjs(b.date_published) - dayjs(a.date_published))
|
||||
|
||||
if (instanceProjects.value) {
|
||||
const installedFile = Object.values(instanceProjects.value).find(
|
||||
(x) => x.metadata && x.metadata.project_id === data.value.id,
|
||||
)
|
||||
if (installedFile) {
|
||||
installed.value = true
|
||||
installedVersion.value = installedFile.metadata.version_id
|
||||
}
|
||||
}
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
if (instanceProjects.value) {
|
||||
const installedFile = Object.values(instanceProjects.value).find(
|
||||
(x) => x.metadata && x.metadata.project_id === data.value.id,
|
||||
)
|
||||
if (installedFile) {
|
||||
installed.value = true
|
||||
installedVersion.value = installedFile.metadata.version_id
|
||||
}
|
||||
}
|
||||
breadcrumbs.setName('Project', data.value.title)
|
||||
}
|
||||
|
||||
await fetchProjectData()
|
||||
|
||||
watch(
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
}
|
||||
},
|
||||
() => route.params.id,
|
||||
async () => {
|
||||
if (route.params.id && route.path.startsWith('/project')) {
|
||||
await fetchProjectData()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
async function install(version) {
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
data.value.id,
|
||||
version,
|
||||
instance.value ? instance.value.path : null,
|
||||
'ProjectPage',
|
||||
(version) => {
|
||||
installing.value = false
|
||||
installing.value = true
|
||||
await installVersion(
|
||||
data.value.id,
|
||||
version,
|
||||
instance.value ? instance.value.path : null,
|
||||
'ProjectPage',
|
||||
(version) => {
|
||||
installing.value = false
|
||||
|
||||
if (instance.value && version) {
|
||||
installed.value = true
|
||||
installedVersion.value = version
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
if (instance.value && version) {
|
||||
installed.value = true
|
||||
installedVersion.value = version
|
||||
}
|
||||
},
|
||||
(profile) => {
|
||||
router.push(`/instance/${profile}`)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const options = ref(null)
|
||||
const handleRightClick = (event) => {
|
||||
options.value.showMenu(event, data.value, [
|
||||
{
|
||||
name: 'install',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
options.value.showMenu(event, data.value, [
|
||||
{
|
||||
name: 'install',
|
||||
},
|
||||
{
|
||||
type: 'divider',
|
||||
},
|
||||
{
|
||||
name: 'open_link',
|
||||
},
|
||||
{
|
||||
name: 'copy_link',
|
||||
},
|
||||
])
|
||||
}
|
||||
const handleOptionsClick = (args) => {
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
switch (args.option) {
|
||||
case 'install':
|
||||
install(null)
|
||||
break
|
||||
case 'open_link':
|
||||
openUrl(`https://modrinth.com/${args.item.project_type}/${args.item.slug}`)
|
||||
break
|
||||
case 'copy_link':
|
||||
navigator.clipboard.writeText(
|
||||
`https://modrinth.com/${args.item.project_type}/${args.item.slug}`,
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.root-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.project-sidebar {
|
||||
position: fixed;
|
||||
width: calc(300px + 1.5rem);
|
||||
min-height: calc(100vh - 3.25rem);
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 3.25rem);
|
||||
padding: 1rem 0.5rem 1rem 1rem;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
position: fixed;
|
||||
width: calc(300px + 1.5rem);
|
||||
min-height: calc(100vh - 3.25rem);
|
||||
height: fit-content;
|
||||
max-height: calc(100vh - 3.25rem);
|
||||
padding: 1rem 0.5rem 1rem 1rem;
|
||||
overflow-y: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
background: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-left: calc(300px + 1rem);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 1rem;
|
||||
margin-left: calc(300px + 1rem);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-md);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-md);
|
||||
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
gap: var(--gap-xs);
|
||||
--stat-strong-size: 1.25rem;
|
||||
.stat {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
width: fit-content;
|
||||
gap: var(--gap-xs);
|
||||
--stat-strong-size: 1.25rem;
|
||||
|
||||
strong {
|
||||
font-size: var(--stat-strong-size);
|
||||
}
|
||||
strong {
|
||||
font-size: var(--stat-strong-size);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
min-height: var(--stat-strong-size);
|
||||
min-width: var(--stat-strong-size);
|
||||
}
|
||||
}
|
||||
svg {
|
||||
min-height: var(--stat-strong-size);
|
||||
min-width: var(--stat-strong-size);
|
||||
}
|
||||
}
|
||||
|
||||
.date {
|
||||
margin-top: auto;
|
||||
}
|
||||
.date {
|
||||
margin-top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
margin-bottom: var(--gap-md);
|
||||
justify-content: space-between;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
margin-bottom: var(--gap-md);
|
||||
justify-content: space-between;
|
||||
|
||||
.tab {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
.tab {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
&.router-view-active {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
&.router-view-active {
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.links {
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 1rem;
|
||||
color: var(--color-text);
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border-radius: 1rem;
|
||||
color: var(--color-text);
|
||||
|
||||
svg,
|
||||
img {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
svg,
|
||||
img {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
}
|
||||
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
text-decoration: underline;
|
||||
line-height: 2rem;
|
||||
}
|
||||
span {
|
||||
margin-left: 0.25rem;
|
||||
text-decoration: underline;
|
||||
line-height: 2rem;
|
||||
}
|
||||
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
svg,
|
||||
img,
|
||||
span {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
}
|
||||
&:focus-visible,
|
||||
&:hover {
|
||||
svg,
|
||||
img,
|
||||
span {
|
||||
color: var(--color-heading);
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
svg,
|
||||
img,
|
||||
span {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
&:active {
|
||||
svg,
|
||||
img,
|
||||
span {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
}
|
||||
&:not(:last-child)::after {
|
||||
content: '•';
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.install-loading {
|
||||
scale: 0.2;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: -1rem;
|
||||
scale: 0.2;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
margin-right: -1rem;
|
||||
|
||||
:deep(svg) {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
:deep(svg) {
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
}
|
||||
|
||||
.project-sidebar-section {
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||
@apply p-4 flex flex-col gap-2 border-0 border-b-[1px] border-[--brand-gradient-border] border-solid;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,468 +1,469 @@
|
||||
<template>
|
||||
<div>
|
||||
<Card>
|
||||
<Breadcrumbs
|
||||
:current-title="version.name"
|
||||
:link-stack="[
|
||||
{
|
||||
href: `/project/${route.params.id}/versions`,
|
||||
label: 'Versions',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="version-title">
|
||||
<h2>{{ version.name }}</h2>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button
|
||||
color="primary"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
<Button>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
|
||||
rel="external"
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Modrinth website
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="version-container">
|
||||
<div class="description-cards">
|
||||
<Card>
|
||||
<h3 class="card-title">Changelog</h3>
|
||||
<div class="markdown-body" v-html="renderString(version.changelog ?? '')" />
|
||||
</Card>
|
||||
<Card>
|
||||
<h3 class="card-title">Files</h3>
|
||||
<Card
|
||||
v-for="file in version.files"
|
||||
:key="file.id"
|
||||
:class="{ primary: file.primary }"
|
||||
class="file"
|
||||
>
|
||||
<span class="label">
|
||||
<FileIcon />
|
||||
<span>
|
||||
<span class="title">
|
||||
{{ file.filename }}
|
||||
</span>
|
||||
({{ formatBytes(file.size) }})
|
||||
<span v-if="file.primary" class="primary-label"> Primary </span>
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
v-if="project.project_type !== 'modpack' || file.primary"
|
||||
class="download"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installed"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
</Card>
|
||||
</Card>
|
||||
<Card v-if="displayDependencies.length > 0">
|
||||
<h2>Dependencies</h2>
|
||||
<div v-for="dependency in displayDependencies" :key="dependency.title">
|
||||
<router-link v-if="dependency.link" class="btn dependency" :to="dependency.link">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div>
|
||||
<span class="title"> {{ dependency.title }} </span> <br />
|
||||
<span> {{ dependency.subtitle }} </span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-else class="dependency disabled" disabled="">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div class="text">
|
||||
<div class="title">{{ dependency.title }}</div>
|
||||
<div>{{ dependency.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Card class="metadata-card">
|
||||
<h3 class="card-title">Metadata</h3>
|
||||
<div class="metadata">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Release Channel</span>
|
||||
<span class="metadata-value"
|
||||
><Badge
|
||||
:color="releaseColor(version.version_type)"
|
||||
:type="
|
||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
||||
"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version Number</span>
|
||||
<span class="metadata-value">{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Loaders</span>
|
||||
<span class="metadata-value">{{
|
||||
version.loaders
|
||||
.map((loader) => loader.charAt(0).toUpperCase() + loader.slice(1))
|
||||
.join(', ')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Game Versions</span>
|
||||
<span class="metadata-value"> {{ version.game_versions.join(', ') }} </span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Downloads</span>
|
||||
<span class="metadata-value">{{ version.downloads }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Publication Date</span>
|
||||
<span class="metadata-value">
|
||||
{{
|
||||
new Date(version.date_published).toLocaleString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}}
|
||||
at
|
||||
{{
|
||||
new Date(version.date_published).toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: true,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="author" class="metadata-item">
|
||||
<span class="metadata-label">Author</span>
|
||||
<a
|
||||
:href="`https://modrinth.com/user/${author.user.username}`"
|
||||
rel="external"
|
||||
class="metadata-value btn author"
|
||||
>
|
||||
<Avatar size="sm" :src="author.user.avatar_url" circle />
|
||||
<span>
|
||||
<strong>
|
||||
{{ author.user.username }}
|
||||
</strong>
|
||||
<br />
|
||||
{{ author.role }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version ID</span>
|
||||
<span class="metadata-value"><CopyCode class="copycode" :text="version.id" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Card>
|
||||
<Breadcrumbs
|
||||
:current-title="version.name"
|
||||
:link-stack="[
|
||||
{
|
||||
href: `/project/${route.params.id}/versions`,
|
||||
label: 'Versions',
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div class="version-title">
|
||||
<h2>{{ version.name }}</h2>
|
||||
</div>
|
||||
<div class="button-group">
|
||||
<Button
|
||||
color="primary"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installing || (installed && installedVersion === version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installedVersion !== version.id" />
|
||||
<CheckIcon v-else />
|
||||
{{
|
||||
installing
|
||||
? 'Installing...'
|
||||
: installed && installedVersion === version.id
|
||||
? 'Installed'
|
||||
: 'Install'
|
||||
}}
|
||||
</Button>
|
||||
<Button>
|
||||
<ReportIcon />
|
||||
Report
|
||||
</Button>
|
||||
<a
|
||||
:href="`https://modrinth.com/mod/${route.params.id}/version/${route.params.version}`"
|
||||
rel="external"
|
||||
class="btn"
|
||||
>
|
||||
<ExternalIcon />
|
||||
Modrinth website
|
||||
</a>
|
||||
</div>
|
||||
</Card>
|
||||
<div class="version-container">
|
||||
<div class="description-cards">
|
||||
<Card>
|
||||
<h3 class="card-title">Changelog</h3>
|
||||
<div class="markdown-body" v-html="renderString(version.changelog ?? '')" />
|
||||
</Card>
|
||||
<Card>
|
||||
<h3 class="card-title">Files</h3>
|
||||
<Card
|
||||
v-for="file in version.files"
|
||||
:key="file.id"
|
||||
:class="{ primary: file.primary }"
|
||||
class="file"
|
||||
>
|
||||
<span class="label">
|
||||
<FileIcon />
|
||||
<span>
|
||||
<span class="title">
|
||||
{{ file.filename }}
|
||||
</span>
|
||||
({{ formatBytes(file.size) }})
|
||||
<span v-if="file.primary" class="primary-label"> Primary </span>
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
v-if="project.project_type !== 'modpack' || file.primary"
|
||||
class="download"
|
||||
:action="() => install(version.id)"
|
||||
:disabled="installed"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<CheckIcon v-else />
|
||||
{{ installed ? 'Installed' : 'Install' }}
|
||||
</Button>
|
||||
</Card>
|
||||
</Card>
|
||||
<Card v-if="displayDependencies.length > 0">
|
||||
<h2>Dependencies</h2>
|
||||
<div v-for="dependency in displayDependencies" :key="dependency.title">
|
||||
<router-link v-if="dependency.link" class="btn dependency" :to="dependency.link">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div>
|
||||
<span class="title"> {{ dependency.title }} </span> <br />
|
||||
<span> {{ dependency.subtitle }} </span>
|
||||
</div>
|
||||
</router-link>
|
||||
<div v-else class="dependency disabled" disabled="">
|
||||
<Avatar size="sm" :src="dependency.icon" />
|
||||
<div class="text">
|
||||
<div class="title">{{ dependency.title }}</div>
|
||||
<div>{{ dependency.subtitle }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
<Card class="metadata-card">
|
||||
<h3 class="card-title">Metadata</h3>
|
||||
<div class="metadata">
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Release Channel</span>
|
||||
<span class="metadata-value"
|
||||
><Badge
|
||||
:color="releaseColor(version.version_type)"
|
||||
:type="
|
||||
version.version_type.charAt(0).toUpperCase() + version.version_type.slice(1)
|
||||
"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version Number</span>
|
||||
<span class="metadata-value">{{ version.version_number }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Loaders</span>
|
||||
<span class="metadata-value">{{
|
||||
version.loaders
|
||||
.map((loader) => loader.charAt(0).toUpperCase() + loader.slice(1))
|
||||
.join(', ')
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Game Versions</span>
|
||||
<span class="metadata-value"> {{ version.game_versions.join(', ') }} </span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Downloads</span>
|
||||
<span class="metadata-value">{{ version.downloads }}</span>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Publication Date</span>
|
||||
<span class="metadata-value">
|
||||
{{
|
||||
new Date(version.date_published).toLocaleString('en-US', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
})
|
||||
}}
|
||||
at
|
||||
{{
|
||||
new Date(version.date_published).toLocaleString('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
hour12: true,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="author" class="metadata-item">
|
||||
<span class="metadata-label">Author</span>
|
||||
<a
|
||||
:href="`https://modrinth.com/user/${author.user.username}`"
|
||||
rel="external"
|
||||
class="metadata-value btn author"
|
||||
>
|
||||
<Avatar size="sm" :src="author.user.avatar_url" circle />
|
||||
<span>
|
||||
<strong>
|
||||
{{ author.user.username }}
|
||||
</strong>
|
||||
<br />
|
||||
{{ author.role }}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="metadata-item">
|
||||
<span class="metadata-label">Version ID</span>
|
||||
<span class="metadata-value"><CopyCode class="copycode" :text="version.id" /></span>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { DownloadIcon, FileIcon, ReportIcon, ExternalIcon, CheckIcon } from '@modrinth/assets'
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, FileIcon, ReportIcon } from '@modrinth/assets'
|
||||
import { Avatar, Badge, Breadcrumbs, Button, Card, CopyCode } from '@modrinth/ui'
|
||||
import { formatBytes, renderString } from '@modrinth/utils'
|
||||
import { Breadcrumbs, Badge, Avatar, Card, Button, CopyCode } from '@modrinth/ui'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { ref, watch, computed } from 'vue'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons'
|
||||
import { get_project_many, get_version_many } from '@/helpers/cache.js'
|
||||
import { releaseColor } from '@/helpers/utils'
|
||||
import { useBreadcrumbs } from '@/store/breadcrumbs'
|
||||
|
||||
const breadcrumbs = useBreadcrumbs()
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
install: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
members: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
install: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const version = ref(props.versions.find((version) => version.id === route.params.version))
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
|
||||
watch(
|
||||
() => props.versions,
|
||||
async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((version) => version.id === route.params.version)
|
||||
await refreshDisplayDependencies()
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
},
|
||||
() => props.versions,
|
||||
async () => {
|
||||
if (route.params.version) {
|
||||
version.value = props.versions.find((version) => version.id === route.params.version)
|
||||
await refreshDisplayDependencies()
|
||||
breadcrumbs.setName('Version', version.value.name)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const author = computed(() =>
|
||||
props.members ? props.members.find((member) => member.user.id === version.value.author_id) : null,
|
||||
props.members ? props.members.find((member) => member.user.id === version.value.author_id) : null,
|
||||
)
|
||||
|
||||
const displayDependencies = ref({})
|
||||
|
||||
async function refreshDisplayDependencies() {
|
||||
const projectIds = new Set()
|
||||
const versionIds = new Set()
|
||||
if (version.value.dependencies) {
|
||||
for (const dependency of version.value.dependencies) {
|
||||
if (dependency.project_id) {
|
||||
projectIds.add(dependency.project_id)
|
||||
}
|
||||
if (dependency.version_id) {
|
||||
versionIds.add(dependency.version_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
const [projectDeps, versionDeps] = await Promise.all([
|
||||
get_project_many([...projectIds]),
|
||||
get_version_many([...versionIds]),
|
||||
])
|
||||
const projectIds = new Set()
|
||||
const versionIds = new Set()
|
||||
if (version.value.dependencies) {
|
||||
for (const dependency of version.value.dependencies) {
|
||||
if (dependency.project_id) {
|
||||
projectIds.add(dependency.project_id)
|
||||
}
|
||||
if (dependency.version_id) {
|
||||
versionIds.add(dependency.version_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
const [projectDeps, versionDeps] = await Promise.all([
|
||||
get_project_many([...projectIds]),
|
||||
get_version_many([...versionIds]),
|
||||
])
|
||||
|
||||
const dependencies = {
|
||||
projects: projectDeps,
|
||||
versions: versionDeps,
|
||||
}
|
||||
const dependencies = {
|
||||
projects: projectDeps,
|
||||
versions: versionDeps,
|
||||
}
|
||||
|
||||
displayDependencies.value = version.value.dependencies.map((dependency) => {
|
||||
const version = dependencies.versions.find((obj) => obj.id === dependency.version_id)
|
||||
if (version) {
|
||||
const project = dependencies.projects.find(
|
||||
(obj) => obj.id === version.project_id || obj.id === dependency.project_id,
|
||||
)
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `Version ${version.version_number} is ${dependency.dependency_type}`,
|
||||
link: `/project/${project.slug}/version/${version.id}`,
|
||||
}
|
||||
} else {
|
||||
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
|
||||
displayDependencies.value = version.value.dependencies.map((dependency) => {
|
||||
const version = dependencies.versions.find((obj) => obj.id === dependency.version_id)
|
||||
if (version) {
|
||||
const project = dependencies.projects.find(
|
||||
(obj) => obj.id === version.project_id || obj.id === dependency.project_id,
|
||||
)
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `Version ${version.version_number} is ${dependency.dependency_type}`,
|
||||
link: `/project/${project.slug}/version/${version.id}`,
|
||||
}
|
||||
} else {
|
||||
const project = dependencies.projects.find((obj) => obj.id === dependency.project_id)
|
||||
|
||||
if (project) {
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `${dependency.dependency_type}`,
|
||||
link: `/project/${project.slug}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
icon: null,
|
||||
title: dependency.file_name,
|
||||
subtitle: `Added via overrides`,
|
||||
link: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
if (project) {
|
||||
return {
|
||||
icon: project?.icon_url,
|
||||
title: project?.title || project?.name,
|
||||
subtitle: `${dependency.dependency_type}`,
|
||||
link: `/project/${project.slug}`,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
icon: null,
|
||||
title: dependency.file_name,
|
||||
subtitle: `Added via overrides`,
|
||||
link: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
await refreshDisplayDependencies()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.version-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.version-title {
|
||||
margin-bottom: 1rem;
|
||||
h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-contrast);
|
||||
margin: 0;
|
||||
}
|
||||
margin-bottom: 1rem;
|
||||
h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-contrast);
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dependency {
|
||||
display: flex;
|
||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
padding: 0.5rem 1rem 0.5rem 0.5rem;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-raised-bg);
|
||||
color: var(--color-base);
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
:deep(svg) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
:deep(svg) {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.file {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
padding: 0.5rem 1rem;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
background: var(--color-button-bg);
|
||||
color: var(--color-base);
|
||||
padding: 0.5rem 1rem;
|
||||
|
||||
.download {
|
||||
margin-left: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
.download {
|
||||
margin-left: auto;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
margin: auto 0 auto;
|
||||
gap: 0.5rem;
|
||||
.label {
|
||||
display: flex;
|
||||
margin: auto 0 auto;
|
||||
gap: 0.5rem;
|
||||
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
word-break: break-all;
|
||||
}
|
||||
.title {
|
||||
font-weight: bolder;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
svg {
|
||||
min-width: 1.1rem;
|
||||
min-height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
margin: auto 0;
|
||||
}
|
||||
svg {
|
||||
min-width: 1.1rem;
|
||||
min-height: 1.1rem;
|
||||
width: 1.1rem;
|
||||
height: 1.1rem;
|
||||
margin: auto 0;
|
||||
}
|
||||
|
||||
.primary-label {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
.primary-label {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.primary {
|
||||
background: var(--color-brand-highlight);
|
||||
color: var(--color-contrast);
|
||||
background: var(--color-brand-highlight);
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-contrast);
|
||||
margin: 0 0 0.5rem;
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--color-contrast);
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.description-cards {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.metadata-card {
|
||||
width: 20rem;
|
||||
height: min-content;
|
||||
width: 20rem;
|
||||
height: min-content;
|
||||
}
|
||||
|
||||
.metadata {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
.metadata-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
.metadata-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
.metadata-label {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.author {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color-base);
|
||||
background: var(--color-raised-bg);
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
color: var(--color-base);
|
||||
background: var(--color-raised-bg);
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.markdown-body {
|
||||
:deep(hr),
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
img {
|
||||
max-width: max(60rem, 90%) !important;
|
||||
}
|
||||
:deep(hr),
|
||||
:deep(h1),
|
||||
:deep(h2),
|
||||
img {
|
||||
max-width: max(60rem, 90%) !important;
|
||||
}
|
||||
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
:deep(ul),
|
||||
:deep(ol) {
|
||||
margin-left: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.copycode {
|
||||
border: 0;
|
||||
color: var(--color-contrast);
|
||||
border: 0;
|
||||
color: var(--color-contrast);
|
||||
}
|
||||
|
||||
.disabled {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
vertical-align: center;
|
||||
align-items: center;
|
||||
cursor: not-allowed;
|
||||
border-radius: var(--radius-lg);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
vertical-align: center;
|
||||
align-items: center;
|
||||
cursor: not-allowed;
|
||||
border-radius: var(--radius-lg);
|
||||
|
||||
.text {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
.text {
|
||||
filter: brightness(0.5);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,201 +1,202 @@
|
||||
<template>
|
||||
<div>
|
||||
<ProjectPageVersions
|
||||
:loaders="loaders"
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
|
||||
>
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
action: () => {},
|
||||
shown: false && !!instance,
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #install-elsewhere>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Add to another instance
|
||||
</template>
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
</OverflowMenu>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectPageVersions>
|
||||
</div>
|
||||
<div>
|
||||
<ProjectPageVersions
|
||||
:loaders="loaders"
|
||||
:game-versions="gameVersions"
|
||||
:versions="versions"
|
||||
:project="project"
|
||||
:version-link="(version) => `/project/${project.id}/version/${version.id}`"
|
||||
>
|
||||
<template #actions="{ version }">
|
||||
<ButtonStyled circular type="transparent">
|
||||
<button
|
||||
v-tooltip="`Install`"
|
||||
:class="{
|
||||
'group-hover:!bg-brand group-hover:[&>svg]:!text-brand-inverted':
|
||||
!installed || version.id !== installedVersion,
|
||||
}"
|
||||
:disabled="installing || (installed && version.id === installedVersion)"
|
||||
@click.stop="() => install(version.id)"
|
||||
>
|
||||
<DownloadIcon v-if="!installed" />
|
||||
<SwapIcon v-else-if="installed && version.id !== installedVersion" />
|
||||
<CheckIcon v-else />
|
||||
</button>
|
||||
</ButtonStyled>
|
||||
<ButtonStyled circular type="transparent">
|
||||
<OverflowMenu
|
||||
v-if="false"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:options="[
|
||||
{
|
||||
id: 'install-elsewhere',
|
||||
action: () => {},
|
||||
shown: false && !!instance,
|
||||
color: 'primary',
|
||||
hoverFilled: true,
|
||||
},
|
||||
{
|
||||
id: 'open-in-browser',
|
||||
link: `https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`,
|
||||
},
|
||||
]"
|
||||
aria-label="More options"
|
||||
>
|
||||
<MoreVerticalIcon aria-hidden="true" />
|
||||
<template #install-elsewhere>
|
||||
<DownloadIcon aria-hidden="true" />
|
||||
Add to another instance
|
||||
</template>
|
||||
<template #open-in-browser> <ExternalIcon /> Open in browser </template>
|
||||
</OverflowMenu>
|
||||
<a
|
||||
v-else
|
||||
v-tooltip="`Open in browser`"
|
||||
class="group-hover:!bg-button-bg"
|
||||
:href="`https://modrinth.com/${project.project_type}/${project.slug}/version/${version.id}`"
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalIcon />
|
||||
</a>
|
||||
</ButtonStyled>
|
||||
</template>
|
||||
</ProjectPageVersions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
import { CheckIcon, DownloadIcon, ExternalIcon, MoreVerticalIcon } from '@modrinth/assets'
|
||||
import {
|
||||
ButtonStyled,
|
||||
OverflowMenu,
|
||||
ProjectPageVersions,
|
||||
injectNotificationManager,
|
||||
ButtonStyled,
|
||||
injectNotificationManager,
|
||||
OverflowMenu,
|
||||
ProjectPageVersions,
|
||||
} from '@modrinth/ui'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { SwapIcon } from '@/assets/icons/index.js'
|
||||
import { get_game_versions, get_loaders } from '@/helpers/tags.js'
|
||||
|
||||
defineProps({
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
install: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
project: {
|
||||
type: Object,
|
||||
default: () => {},
|
||||
},
|
||||
versions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
install: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
installed: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
installing: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
instance: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
installedVersion: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
})
|
||||
|
||||
const { handleError } = injectNotificationManager()
|
||||
|
||||
const [loaders, gameVersions] = await Promise.all([
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
get_loaders().catch(handleError).then(ref),
|
||||
get_game_versions().catch(handleError).then(ref),
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.filter-header {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
grid-template-columns: min-content 1fr 1fr 1.5fr;
|
||||
grid-template-columns: min-content 1fr 1fr 1.5fr;
|
||||
}
|
||||
|
||||
.manage {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-grow: 1;
|
||||
|
||||
.multiselect {
|
||||
flex-grow: 1;
|
||||
}
|
||||
.multiselect {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-raised-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--color-raised-bg);
|
||||
}
|
||||
|
||||
.mod-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-combo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.select {
|
||||
width: 100% !important;
|
||||
max-width: 20rem;
|
||||
width: 100% !important;
|
||||
max-width: 20rem;
|
||||
}
|
||||
|
||||
.version-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
text-wrap: wrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
text-wrap: wrap;
|
||||
|
||||
.version-badge {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
.version-badge {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.channel-indicator {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
.channel-indicator {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stacked-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.download-cell {
|
||||
width: 4rem;
|
||||
padding: 1rem;
|
||||
width: 4rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.filter-checkbox {
|
||||
:deep(.checkbox) {
|
||||
border: none;
|
||||
}
|
||||
:deep(.checkbox) {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Index from './Index.vue'
|
||||
import Description from './Description.vue'
|
||||
import Versions from './Versions.vue'
|
||||
import Gallery from './Gallery.vue'
|
||||
import Index from './Index.vue'
|
||||
import Version from './Version.vue'
|
||||
import Versions from './Versions.vue'
|
||||
|
||||
export { Index, Description, Versions, Gallery, Version }
|
||||
export { Description, Gallery, Index, Version, Versions }
|
||||
|
||||
Reference in New Issue
Block a user