New features (#592)

* New features

* Lots of bug fixes

* Fix respack creation

* Improve mobile nav with more project types

* Fix resolution sorting and remove icons

* Move cookie consent to top on small devices to get out of the way of navigation

* Move cookie consent + fix hydration

* Fix project editing + update search features

* Centralize hardcoding of loader/category names, fix cookie consent shadow, fix mobile navbar rounding

* Fix plugin platforms formatting

* Kitchen sink!

* Add support for display names

* LiteLoader formatting

* Fixed "show all loaders" toggle not resetting when changing pages

* Allow multiple loaders in version filter controls

* Fix clear filters button

* Revert "Add support for display names"

This reverts commit 370838763d86bcae51bf06c304248f7a1f8fc28f.

* Let's see how this goes. Upstream filters, attempt 1

* github? hello?

* No more "Server mod" on plugins

* Fix formatting of project types in project creation

* Move where project creation sets the resource pack loader

* Allow setting pixelated image-rendering

Allows to apply 'style' attribute to IMG tags with value
'image-rendering' set to 'pixelated', which can be useful for people who
use pixel art in their READMEs (to demonstrate items, for example).

* fix user page + hydration issue fix from Brawaru

* Rename to proxies

* Make categories use title case

* Always show project type on moderation page, improve project type display on project pages

* Remove invalid key

* Missed a check

* Fix browse menu animation

* Fix disabled button condition and minimum width for 2 lines

* Body -> Description in edit pages

* More casing consistency issues

* Fix duplicate version URLs

* Fix version creation

* Edit URLs, fix privacy page buttons

* Fix notifications popup overlaying

* Final merge fixes

Co-authored-by: Prospector <prospectordev@gmail.com>
Co-authored-by: Sasha Sorokin <10401817+Brawaru@users.noreply.github.com>
This commit is contained in:
Geometrically
2022-08-14 12:42:58 -07:00
committed by GitHub
parent b16475b8bd
commit 673f7a82d1
31 changed files with 1152 additions and 348 deletions

View File

@@ -1,5 +1,9 @@
export default function (to, from, savedPosition) { export default function (to, from, savedPosition) {
if (to.name.startsWith('type-id') && from.name.startsWith('type-id')) { if (
from == null ||
(to.name.startsWith('type-id') && from.name.startsWith('type-id')) ||
to.name === from.name
) {
return savedPosition return savedPosition
} else { } else {
return { x: 0, y: 0 } return { x: 0, y: 0 }

View File

@@ -263,6 +263,14 @@
> :last-child { > :last-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
@media screen and (max-width: 850px) {
iframe {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
}
} }
.tooltip { .tooltip {
@@ -826,12 +834,12 @@ label {
} }
.vue-notification { .vue-notification {
background: #44A4FC; background: #44a4fc;
border-left: 5px solid #44A4FC; border-left: 5px solid #44a4fc;
&.success { &.success {
background: #68CD86; background: #68cd86;
border-left-color: #68CD86; border-left-color: #68cd86;
} }
&.warn { &.warn {
@@ -840,25 +848,43 @@ label {
} }
&.error { &.error {
background: #E54D42; background: #e54d42;
border-left-color: #E54D42; border-left-color: #e54d42;
} }
} }
.vue-notification-group { .vue-notification-group {
right: 25px !important; right: 25px !important;
bottom: 25px !important;
.vue-notification-template { .vue-notification-wrapper {
border-radius: var(--size-rounded-card); margin-bottom: 10px;
margin: 0 0 25px 0;
.notification-title { .vue-notification-template {
font-size: var(--font-size-lg); border-radius: var(--size-rounded-card);
margin-right: auto; margin: 0;
.notification-title {
font-size: var(--font-size-lg);
margin-right: auto;
}
.notification-content {
font-size: var(--font-size-md);
}
} }
.notification-content { &:last-child {
font-size: var(--font-size-md); margin: 0;
}
}
@media screen and (max-width: 750px) {
transition: bottom 0.25s ease-in-out;
bottom: calc(var(--size-mobile-navbar-height) + 10px) !important;
&.browse-menu-open {
bottom: calc(var(--size-mobile-navbar-height-expanded) + 10px) !important;
} }
} }
} }

View File

@@ -208,7 +208,9 @@ body {
--size-rounded-tooltip: 0.25rem; --size-rounded-tooltip: 0.25rem;
--size-navbar-height: 3.5rem; --size-navbar-height: 3.5rem;
--size-mobile-navbar-height: 4rem; --size-mobile-navbar-height: 3.5rem;
// --size-mobile-navbar-height-expanded: 10rem;
--size-mobile-navbar-height-expanded: 7.5rem;
--spacing-card-lg: 1.5rem; --spacing-card-lg: 1.5rem;
--spacing-card-bg: 1rem; --spacing-card-bg: 1rem;
@@ -235,6 +237,10 @@ body {
--font-weight-text: var(--font-weight-medium); --font-weight-text: var(--font-weight-medium);
--font-weight-heading: var(--font-weight-extrabold); --font-weight-heading: var(--font-weight-extrabold);
--font-weight-title: var(--font-weight-extrabold); --font-weight-title: var(--font-weight-extrabold);
@media screen and (min-width: 501px) {
--size-mobile-navbar-height-expanded: 7rem;
}
} }
svg { svg {

View File

@@ -3,13 +3,15 @@
<div <div
ref="container" ref="container"
class="container" class="container"
:style="{ visibility: shown ? 'visible' : 'hidden' }" :class="{ 'mobile-menu-open': mobileMenuOpen }"
:style="{
visibility: shown ? 'visible' : 'hidden',
}"
> >
<div class="card banner"> <div class="card banner">
<span> <span>
Modrinth uses cookies for various purposes, including advertising.<br /> Modrinth uses cookies for various purposes. We encourage you to review
We encourage you to review your privacy settings by clicking on the your privacy settings by clicking on the button below:
button below:
</span> </span>
<div class="actions"> <div class="actions">
<button class="btn button" @click="review">Review</button> <button class="btn button" @click="review">Review</button>
@@ -24,6 +26,12 @@
import scopes from '~/privacy-toggles' import scopes from '~/privacy-toggles'
export default { export default {
name: 'CookieConsent', name: 'CookieConsent',
props: {
mobileMenuOpen: {
type: Boolean,
default: true,
},
},
data() { data() {
return { return {
shown: false, shown: false,
@@ -68,15 +76,18 @@ export default {
width: 100%; width: 100%;
text-align: center; text-align: center;
z-index: 20; z-index: 2;
position: fixed; position: fixed;
bottom: 4rem;
right: 0; right: 0;
bottom: 0;
.banner { .banner {
padding: 1rem;
font-size: 1.05rem; font-size: 1.05rem;
border-radius: 0; border-radius: 0;
margin-bottom: 0; margin-bottom: 0;
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3);
padding: 1rem 1rem calc(var(--size-mobile-navbar-height) + 1rem);
transition: padding-bottom 0.25s ease-in-out;
} }
.actions { .actions {
display: flex; display: flex;
@@ -89,11 +100,23 @@ export default {
} }
} }
@media screen and (min-width: 750px) { .banner {
bottom: 0; margin-bottom: 0;
}
&.mobile-menu-open {
.banner { .banner {
margin-bottom: 0; padding-bottom: calc(var(--size-mobile-navbar-height-expanded) + 1rem);
}
}
@media screen and (min-width: 750px) {
.banner {
padding-bottom: 1rem;
}
&.mobile-menu-open {
bottom: 0;
} }
} }
@@ -102,7 +125,9 @@ export default {
text-align: unset; text-align: unset;
.banner { .banner {
max-width: 18vw; border-radius: var(--size-rounded-card);
width: 18vw;
min-width: 16rem;
border-left: solid 5px var(--color-brand); border-left: solid 5px var(--color-brand);
margin: 0 2rem 2rem 0; margin: 0 2rem 2rem 0;
} }

View File

@@ -28,13 +28,16 @@
</nuxt-link> </nuxt-link>
</p> </p>
</div> </div>
<div class="side-type"> <div
v-if="type !== 'resourcepack' && projectTypeDisplay !== 'plugin'"
class="side-type"
>
<div <div
v-if="clientSide === 'optional' && serverSide === 'optional'" v-if="clientSide === 'optional' && serverSide === 'optional'"
class="side-descriptor" class="side-descriptor"
> >
<InfoIcon aria-hidden="true" /> <InfoIcon aria-hidden="true" />
Universal {{ type }} Universal {{ projectTypeDisplay }}
</div> </div>
<div <div
v-else-if=" v-else-if="
@@ -44,7 +47,7 @@
class="side-descriptor" class="side-descriptor"
> >
<InfoIcon aria-hidden="true" /> <InfoIcon aria-hidden="true" />
Client {{ type }} Client {{ projectTypeDisplay }}
</div> </div>
<div <div
v-else-if=" v-else-if="
@@ -54,8 +57,16 @@
class="side-descriptor" class="side-descriptor"
> >
<InfoIcon aria-hidden="true" /> <InfoIcon aria-hidden="true" />
Server {{ type }} Server {{ projectTypeDisplay }}
</div> </div>
<div v-else-if="moderation" class="side-descriptor">
<InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }}
</div>
</div>
<div v-else-if="moderation" class="side-descriptor">
<InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }}
</div> </div>
<p class="description"> <p class="description">
{{ description }} {{ description }}
@@ -217,6 +228,16 @@ export default {
required: false, required: false,
default: '', default: '',
}, },
moderation: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories)
},
}, },
} }
</script> </script>

View File

@@ -43,11 +43,15 @@ export default {
<style scoped> <style scoped>
button { button {
text-transform: capitalize;
margin: 0; margin: 0;
padding: 0; padding: 0;
text-transform: capitalize;
background-color: transparent; background-color: transparent;
border-radius: 0; border-radius: 0;
color: inherit; color: inherit;
} }
button span::first-letter {
text-transform: uppercase;
}
</style> </style>

View File

@@ -5,16 +5,16 @@
> >
<Multiselect <Multiselect
v-if="getValidLoaders().length > 1" v-if="getValidLoaders().length > 1"
v-model="selectedLoader" v-model="selectedLoaders"
:options="getValidLoaders()" :options="getValidLoaders()"
:custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)" :custom-label="(value) => value.charAt(0).toUpperCase() + value.slice(1)"
:multiple="false" :multiple="true"
:searchable="false" :searchable="false"
:show-no-results="false" :show-no-results="false"
:close-on-select="true" :close-on-select="true"
:clear-search-on-select="false" :clear-search-on-select="false"
:show-labels="false" :show-labels="false"
:allow-empty="false" :allow-empty="true"
:disabled="getValidLoaders().length === 1" :disabled="getValidLoaders().length === 1"
placeholder="Filter loader..." placeholder="Filter loader..."
@input="updateVersionFilters()" @input="updateVersionFilters()"
@@ -51,10 +51,12 @@
/> />
<button <button
title="Clear filters" title="Clear filters"
:disabled="selectedLoader === null && selectedGameVersions.length === 0" :disabled="
selectedLoaders.length === 0 && selectedGameVersions.length === 0
"
class="iconified-button" class="iconified-button"
@click=" @click="
selectedLoader = null selectedLoaders = []
selectedGameVersions = [] selectedGameVersions = []
updateVersionFilters() updateVersionFilters()
" "
@@ -90,7 +92,7 @@ export default {
cachedValidVersions: null, cachedValidVersions: null,
cachedValidLoaders: null, cachedValidLoaders: null,
selectedGameVersions: [], selectedGameVersions: [],
selectedLoader: null, selectedLoaders: [],
} }
}, },
methods: { methods: {
@@ -124,8 +126,10 @@ export default {
this.selectedGameVersions.some((gameVersion) => this.selectedGameVersions.some((gameVersion) =>
projectVersion.game_versions.includes(gameVersion) projectVersion.game_versions.includes(gameVersion)
)) && )) &&
(this.selectedLoader === null || (this.selectedLoaders.length === 0 ||
projectVersion.loaders.includes(this.selectedLoader)) this.selectedLoaders.some((loader) =>
projectVersion.loaders.includes(loader)
))
) )
this.$emit('updateVersions', temp) this.$emit('updateVersions', temp)
}, },

View File

@@ -3,10 +3,7 @@
<span <span
v-for="category in categoriesFiltered" v-for="category in categoriesFiltered"
:key="category.name" :key="category.name"
v-html=" v-html="category.icon + $formatCategory(category.name)"
category.icon +
(category.name === 'modloader' ? 'ModLoader' : category.name)
"
/> />
</div> </div>
</template> </template>
@@ -33,7 +30,8 @@ export default {
.filter( .filter(
(x) => (x) =>
this.categories.includes(x.name) && this.categories.includes(x.name) &&
(!x.project_type || x.project_type === this.type) (!x.project_type || x.project_type === this.type) &&
x.name !== 'minecraft'
) )
}, },
}, },
@@ -52,7 +50,6 @@ export default {
flex-direction: row; flex-direction: row;
color: var(--color-icon); color: var(--color-icon);
margin-right: 1em; margin-right: 1em;
text-transform: capitalize;
svg { svg {
width: 1rem; width: 1rem;

View File

@@ -69,7 +69,6 @@ export default {
} }
span { span {
text-transform: capitalize;
user-select: none; user-select: none;
} }
} }

View File

@@ -1,12 +1,14 @@
<template> <template>
<div class="layout"> <div ref="layout" class="layout">
<header class="site-header" role="presentation"> <header class="site-header" role="presentation">
<section class="navbar columns" role="navigation"> <section class="navbar columns" role="navigation">
<section class="skip column" role="presentation"> <section class="skip column" role="presentation">
<a href="#main">Skip to Main Content</a> <a href="#main">Skip to Main Content</a>
<a v-if="registeredSkipLink" :href="registeredSkipLink.id">{{ <a
registeredSkipLink.text v-show="!!registeredSkipLink"
}}</a> :href="(registeredSkipLink || {}).id"
>{{ (registeredSkipLink || {}).text }}</a
>
</section> </section>
<section class="logo column" role="presentation"> <section class="logo column" role="presentation">
<NuxtLink to="/" aria-label="Modrinth home page"> <NuxtLink to="/" aria-label="Modrinth home page">
@@ -19,7 +21,13 @@
<NuxtLink to="/mods" class="tab"> <NuxtLink to="/mods" class="tab">
<span>Mods</span> <span>Mods</span>
</NuxtLink> </NuxtLink>
<NuxtLink to="/modpacks" class="tab tab--alpha"> <!-- <NuxtLink to="/plugins" class="tab">-->
<!-- <span>Plugins</span>-->
<!-- </NuxtLink>-->
<!-- <NuxtLink to="/resourcepacks" class="tab">-->
<!-- <span>Resource Packs</span>-->
<!-- </NuxtLink>-->
<NuxtLink to="/modpacks" class="tab">
<span>Modpacks</span> <span>Modpacks</span>
</NuxtLink> </NuxtLink>
</div> </div>
@@ -162,23 +170,62 @@
</section> </section>
</section> </section>
</section> </section>
<section class="mobile-navbar"> <section ref="mobileNavBar" class="mobile-navbar">
<NuxtLink to="/" class="tab"> <div class="top-row">
<HomeIcon /> <NuxtLink to="/" class="tab" @click.native="closeBrowseMenu()">
<span>Home</span> <HomeIcon />
</NuxtLink> </NuxtLink>
<NuxtLink to="/mods" class="tab"> <div class="spacer"></div>
<ModIcon /> <button class="tab browse" @click="toggleBrowseMenu()">
<span>Mods</span> <DropdownIcon :class="{ closed: !isBrowseMenuOpen }" />
</NuxtLink> <span>Browse</span>
<NuxtLink to="/modpacks" class="tab"> </button>
<ModpackIcon /> <div class="spacer"></div>
<span>Modpacks</span> <button class="tab" @click="toggleMobileMenu()">
</NuxtLink> <HamburgerIcon v-if="!isMobileMenuOpen" />
<button class="tab" @click="toggleMobileMenu()"> <CrossIcon v-else />
<HamburgerIcon /> </button>
<span>{{ isMobileMenuOpen ? 'Less' : 'More' }}</span> </div>
</button> <div
:class="{ 'disable-childern': !isBrowseMenuOpen }"
class="project-types"
>
<NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1"
to="/mods"
class="tab"
@click.native="closeBrowseMenu()"
>
<span>Mods</span>
</NuxtLink>
<!-- <NuxtLink-->
<!-- :tabindex="isBrowseMenuOpen ? 0 : -1"-->
<!-- to="/plugins"-->
<!-- class="tab"-->
<!-- @click.native="closeBrowseMenu()"-->
<!-- >-->
<!-- <span>Plugins</span>-->
<!-- </NuxtLink>-->
<!-- <NuxtLink-->
<!-- :tabindex="isBrowseMenuOpen ? 0 : -1"-->
<!-- to="/resourcepacks"-->
<!-- class="tab"-->
<!-- @click.native="closeBrowseMenu()"-->
<!-- >-->
<!-- <span>Resource Packs</span>-->
<!-- </NuxtLink>-->
<NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1"
to="/modpacks"
class="tab"
@click.native="closeBrowseMenu()"
>
<span>Modpacks</span>
</NuxtLink>
</div>
</section> </section>
<section ref="mobileMenu" class="mobile-menu"> <section ref="mobileMenu" class="mobile-menu">
<div class="mobile-menu-wrapper"> <div class="mobile-menu-wrapper">
@@ -239,11 +286,12 @@
</section> </section>
</header> </header>
<main> <main>
<CookieConsent /> <CookieConsent :mobile-menu-open="isBrowseMenuOpen" />
<notifications <notifications
group="main" group="main"
position="bottom right" position="bottom right"
:max="5" :max="5"
:class="{ 'browse-menu-open': isBrowseMenuOpen }"
:ignore-duplicates="true" :ignore-duplicates="true"
:duration="10000" :duration="10000"
/> />
@@ -312,16 +360,15 @@ import ClickOutside from 'vue-click-outside'
import ModrinthLogo from '~/assets/images/text-logo.svg?inline' import ModrinthLogo from '~/assets/images/text-logo.svg?inline'
import HamburgerIcon from '~/assets/images/utils/hamburger.svg?inline' import HamburgerIcon from '~/assets/images/utils/hamburger.svg?inline'
import CrossIcon from '~/assets/images/utils/x.svg?inline'
import NotificationIcon from '~/assets/images/sidebar/notifications.svg?inline' import NotificationIcon from '~/assets/images/sidebar/notifications.svg?inline'
import SettingsIcon from '~/assets/images/sidebar/settings.svg?inline' import SettingsIcon from '~/assets/images/sidebar/settings.svg?inline'
import ShieldIcon from '~/assets/images/utils/shield.svg?inline' import ShieldIcon from '~/assets/images/utils/shield.svg?inline'
import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline' import ModerationIcon from '~/assets/images/sidebar/admin.svg?inline'
import HomeIcon from '~/assets/images/sidebar/home.svg?inline' import HomeIcon from '~/assets/images/sidebar/home.svg?inline'
import ModIcon from '~/assets/images/sidebar/mod.svg?inline'
import ModpackIcon from '~/assets/images/sidebar/modpack.svg?inline'
import MoonIcon from '~/assets/images/utils/moon.svg?inline'
import MoonIcon from '~/assets/images/utils/moon.svg?inline'
import SunIcon from '~/assets/images/utils/sun.svg?inline' import SunIcon from '~/assets/images/utils/sun.svg?inline'
import PlusIcon from '~/assets/images/utils/plus.svg?inline' import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline' import DropdownIcon from '~/assets/images/utils/dropdown.svg?inline'
@@ -343,8 +390,7 @@ export default {
GitHubIcon, GitHubIcon,
NotificationIcon, NotificationIcon,
HomeIcon, HomeIcon,
ModIcon, CrossIcon,
ModpackIcon,
HamburgerIcon, HamburgerIcon,
CookieConsent, CookieConsent,
SettingsIcon, SettingsIcon,
@@ -365,6 +411,7 @@ export default {
branch: process.env.branch || 'master', branch: process.env.branch || 'master',
hash: process.env.hash || 'unknown', hash: process.env.hash || 'unknown',
isMobileMenuOpen: false, isMobileMenuOpen: false,
isBrowseMenuOpen: false,
registeredSkipLink: null, registeredSkipLink: null,
moderationNotifications: 0, moderationNotifications: 0,
} }
@@ -429,6 +476,37 @@ export default {
document.body.style.overflowY !== 'hidden' ? 'hidden' : overflowStyle document.body.style.overflowY !== 'hidden' ? 'hidden' : overflowStyle
this.isMobileMenuOpen = !currentlyActive this.isMobileMenuOpen = !currentlyActive
if (this.isMobileMenuOpen) {
this.$refs.mobileNavBar.className = `mobile-navbar`
this.$refs.layout.className = `layout`
this.isBrowseMenuOpen = false
}
},
toggleBrowseMenu() {
const currentlyActive =
this.$refs.mobileNavBar.className === 'mobile-navbar expanded'
this.$refs.mobileNavBar.className = `mobile-navbar${
currentlyActive ? '' : ' expanded'
}`
this.$refs.layout.className = `layout${
currentlyActive ? '' : ' expanded-mobile-nav'
}`
this.isBrowseMenuOpen = !currentlyActive
if (this.isBrowseMenuOpen) {
this.$refs.mobileMenu.className = `mobile-menu`
this.isMobileMenuOpen = false
}
},
closeBrowseMenu() {
this.$refs.mobileNavBar.className = `mobile-navbar`
this.$refs.layout.className = `layout`
this.isBrowseMenuOpen = false
}, },
async logout() { async logout() {
this.$cookies.set('auth-token-reset', true) this.$cookies.set('auth-token-reset', true)
@@ -453,16 +531,6 @@ export default {
removeFocus() { removeFocus() {
document.activeElement.blur() // This doesn't work, sadly. Help document.activeElement.blur() // This doesn't work, sadly. Help
}, },
async getModerationCount() {
const [projects, reports] = (
await Promise.all([
this.$axios.get(`moderation/projects`, this.$defaultHeaders()),
this.$axios.get(`report`, this.$defaultHeaders()),
])
).map((it) => it.data)
return projects.length + reports.length
},
}, },
} }
</script> </script>
@@ -819,34 +887,36 @@ export default {
.mobile-navbar { .mobile-navbar {
display: none; display: none;
width: 100%; width: 100%;
transition: height 0.25s ease-in-out;
height: var(--size-mobile-navbar-height); height: var(--size-mobile-navbar-height);
position: fixed; position: fixed;
left: 0; left: 0;
bottom: 0; bottom: 0;
justify-content: center;
align-items: center;
background-color: var(--color-raised-bg); background-color: var(--color-raised-bg);
box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3); box-shadow: 0 0 20px 2px rgba(0, 0, 0, 0.3);
z-index: 6; z-index: 6;
flex-direction: column;
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
overflow: hidden;
.tab { .tab {
background: none; background: none;
display: flex; display: flex;
flex-grow: 1;
flex-basis: 0; flex-basis: 0;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
flex-direction: column; flex-direction: row;
gap: 0.25rem;
font-weight: bold; font-weight: bold;
padding: 0; padding: 0;
margin: auto;
transition: color ease-in-out 0.15s; transition: color ease-in-out 0.15s;
color: var(--color-text-inactive); color: var(--color-text-inactive);
text-align: center;
svg { svg {
height: 1.75rem; height: 1.75rem;
width: 1.75rem; width: 1.75rem;
margin-bottom: 0.25rem;
} }
&:hover, &:hover,
@@ -863,9 +933,72 @@ export default {
} }
} }
.top-row {
min-height: var(--size-mobile-navbar-height);
display: flex;
width: 100%;
.browse {
flex-grow: 10;
svg {
transition: transform 0.125s ease-in-out;
&.closed {
transform: rotate(180deg);
}
}
}
.tab {
&:first-child {
margin-left: 2rem;
}
&:last-child {
margin-right: 2rem;
}
}
.spacer {
flex-grow: 1;
}
}
.disable-childern {
a {
pointer-events: none;
}
}
.project-types {
margin-top: 0.5rem;
display: flex;
justify-content: center;
flex-wrap: wrap;
row-gap: 0.5rem;
.tab {
flex: 0 0 fit-content;
background-color: var(--color-button-bg);
padding: 0.5rem 1.25rem;
margin: 0 0.25rem;
border-radius: var(--size-rounded-max);
&.nuxt-link-exact-active {
background-color: var(--color-brand);
color: var(--color-brand-inverted);
}
}
}
@media screen and (max-width: 750px) { @media screen and (max-width: 750px) {
display: flex; display: flex;
} }
&.expanded {
height: var(--size-mobile-navbar-height-expanded);
}
} }
} }
@@ -874,12 +1007,13 @@ export default {
position: absolute; position: absolute;
top: 0; top: 0;
background-color: var(--color-bg); background-color: var(--color-bg);
height: calc(100% - var(--size-mobile-navbar-height)); height: 100%;
width: 100%; width: 100%;
z-index: 5; z-index: 5;
.mobile-menu-wrapper { .mobile-menu-wrapper {
max-height: calc(100vh - var(--size-mobile-navbar-height)); max-height: calc(100vh - var(--size-mobile-navbar-height));
margin-bottom: var(--size-mobile-navbar-height);
overflow-y: auto; overflow-y: auto;
margin-top: auto; margin-top: auto;
@@ -903,6 +1037,7 @@ export default {
&.nuxt-link-exact-active { &.nuxt-link-exact-active {
color: var(--color-button-text-active); color: var(--color-button-text-active);
svg { svg {
color: var(--color-brand); color: var(--color-brand);
} }

View File

@@ -155,6 +155,16 @@ export default {
component: resolve(__dirname, 'pages/search/modpacks.vue'), component: resolve(__dirname, 'pages/search/modpacks.vue'),
name: 'modpacks', name: 'modpacks',
}, },
{
path: '/plugins',
component: resolve(__dirname, 'pages/search/plugins.vue'),
name: 'plugins',
},
{
path: '/resourcepacks',
component: resolve(__dirname, 'pages/search/resourcepacks.vue'),
name: 'resourcepacks',
},
], ],
}) })
@@ -222,7 +232,7 @@ export default {
'/search/**', '/search/**',
'/create/**', '/create/**',
], ],
routes: ['mods', 'modpacks'], routes: ['mods', 'modpacks', 'resourcepacks', 'plugins'],
}, },
/* /*
** Axios module configuration ** Axios module configuration
@@ -309,7 +319,7 @@ export default {
}, },
hooks: { hooks: {
render: { render: {
routeDone(url) { routeDone(url, result, context) {
setTimeout(() => { setTimeout(() => {
axios axios
.post( .post(
@@ -323,6 +333,12 @@ export default {
{ {
headers: { headers: {
'Modrinth-Admin': process.env.ARIADNE_ADMIN_KEY || 'feedbeef', 'Modrinth-Admin': process.env.ARIADNE_ADMIN_KEY || 'feedbeef',
'User-Agent':
context.req.rawHeaders[
context.req.rawHeaders.findIndex(
(x) => x === 'User-Agent'
) + 1
],
}, },
} }
) )

View File

@@ -36,38 +36,46 @@
</nuxt-link> </nuxt-link>
<div <div
v-if=" v-if="
project.client_side === 'optional' && project.project_type !== 'resourcepack' &&
project.server_side === 'optional' projectTypeDisplay !== 'plugin'
" "
class="side-descriptor"
> >
<InfoIcon aria-hidden="true" /> <div
Universal {{ project.project_type }} v-if="
</div> project.client_side === 'optional' &&
<div project.server_side === 'optional'
v-else-if=" "
(project.client_side === 'optional' || class="side-descriptor"
project.client_side === 'required') && >
(project.server_side === 'optional' || <InfoIcon aria-hidden="true" />
project.server_side === 'unsupported') Universal {{ projectTypeDisplay }}
" </div>
class="side-descriptor" <div
> v-else-if="
<InfoIcon aria-hidden="true" /> (project.client_side === 'optional' ||
Client {{ project.project_type }} project.client_side === 'required') &&
</div> (project.server_side === 'optional' ||
<div project.server_side === 'unsupported')
v-else-if=" "
(project.server_side === 'optional' || class="side-descriptor"
project.server_side === 'required') && >
(project.client_side === 'optional' || <InfoIcon aria-hidden="true" />
project.client_side === 'unsupported') Client {{ projectTypeDisplay }}
" </div>
class="side-descriptor" <div
> v-else-if="
<InfoIcon aria-hidden="true" /> (project.server_side === 'optional' ||
Server {{ project.project_type }} project.server_side === 'required') &&
(project.client_side === 'optional' ||
project.client_side === 'unsupported')
"
class="side-descriptor"
>
<InfoIcon aria-hidden="true" />
Server {{ projectTypeDisplay }}
</div>
</div> </div>
<p class="description"> <p class="description">
{{ project.description }} {{ project.description }}
</p> </p>
@@ -389,7 +397,7 @@
<nuxt-link <nuxt-link
:to="`/${project.project_type}/${ :to="`/${project.project_type}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`" }/version/${encodeURI(version.displayUrlEnding)}`"
class="top title-link" class="top title-link"
> >
{{ version.name }} {{ version.name }}
@@ -398,15 +406,7 @@
v-if="version.game_versions.length > 0" v-if="version.game_versions.length > 0"
class="game-version item" class="game-version item"
> >
{{ {{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
{{ $formatVersion(version.game_versions) }} {{ $formatVersion(version.game_versions) }}
</div> </div>
<VersionBadge <VersionBadge
@@ -455,13 +455,25 @@
}}</a> }}</a>
</div> </div>
</div> </div>
<div class="info"> <div
v-if="
project.project_type !== 'resourcepack' &&
projectTypeDisplay !== 'plugin'
"
class="info"
>
<div class="key">Client side</div> <div class="key">Client side</div>
<div class="value"> <div class="value">
{{ project.client_side }} {{ project.client_side }}
</div> </div>
</div> </div>
<div class="info"> <div
v-if="
project.project_type !== 'resourcepack' &&
projectTypeDisplay !== 'plugin'
"
class="info"
>
<div class="key">Server side</div> <div class="key">Server side</div>
<div class="value"> <div class="value">
{{ project.server_side }} {{ project.server_side }}
@@ -647,7 +659,7 @@ export default {
Categories, Categories,
}, },
async asyncData(data) { async asyncData(data) {
const projectTypes = ['mod', 'modpack'] const projectTypes = ['mod', 'modpack', 'resourcepack']
try { try {
if ( if (
@@ -706,6 +718,16 @@ export default {
project.body = (await data.$axios.get(project.body_url)).data project.body = (await data.$axios.get(project.body_url)).data
} }
const loaders = []
versions.forEach((version) => {
version.loaders.forEach((loader) => {
if (!loaders.includes(loader)) {
loaders.push(loader)
}
})
})
return { return {
project, project,
versions, versions,
@@ -714,6 +736,7 @@ export default {
allMembers: members, allMembers: members,
currentMember, currentMember,
dependencies, dependencies,
loaders,
} }
} catch { } catch {
data.error({ data.error({
@@ -727,6 +750,10 @@ export default {
showKnownErrors: false, showKnownErrors: false,
} }
}, },
fetch() {
this.versions = this.$computeVersions(this.versions)
this.featuredVersions = this.$computeVersions(this.featuredVersions)
},
head() { head() {
return { return {
title: `${this.project.title} - ${ title: `${this.project.title} - ${
@@ -779,6 +806,14 @@ export default {
], ],
} }
}, },
computed: {
projectTypeDisplay() {
return this.$getProjectTypeForDisplay(
this.project.project_type,
this.loaders
)
},
},
methods: { methods: {
findPrimary(version) { findPrimary(version) {
let file = version.files.find((x) => x.primary) let file = version.files.find((x) => x.primary)
@@ -1119,7 +1154,10 @@ export default {
.value { .value {
width: 50%; width: 50%;
text-transform: capitalize;
&::first-letter {
text-transform: capitalize;
}
&.lowercase { &.lowercase {
text-transform: none; text-transform: none;

View File

@@ -14,7 +14,7 @@
<nuxt-link <nuxt-link
:to="`/${project.project_type}/${ :to="`/${project.project_type}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`" }/version/${encodeURI(version.displayUrlEnding)}`"
>{{ version.name }}</nuxt-link >{{ version.name }}</nuxt-link
> >
</h2> </h2>
@@ -69,12 +69,6 @@ export default {
DownloadIcon, DownloadIcon,
VersionFilterControl, VersionFilterControl,
}, },
data() {
return {
filteredVersions: this.versions,
}
},
auth: false,
props: { props: {
project: { project: {
type: Object, type: Object,
@@ -95,6 +89,12 @@ export default {
}, },
}, },
}, },
data() {
return {
filteredVersions: this.versions,
}
},
auth: false,
methods: { methods: {
updateVersions(updatedVersions) { updateVersions(updatedVersions) {
this.filteredVersions = updatedVersions this.filteredVersions = updatedVersions

View File

@@ -110,17 +110,13 @@
<Multiselect <Multiselect
id="categories" id="categories"
v-model="newProject.categories" v-model="newProject.categories"
:options=" :options="selectableCategories"
$tag.categories
.filter((x) => x.project_type === project.project_type)
.map((it) => it.name)
"
:custom-label=" :custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1) (value) => value.charAt(0).toUpperCase() + value.slice(1)
" "
:loading="$tag.categories.length === 0" :loading="$tag.categories.length === 0"
:multiple="true" :multiple="true"
:searchable="false" :searchable="true"
:show-no-results="false" :show-no-results="false"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="false"
@@ -132,13 +128,46 @@
:disabled=" :disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS (currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
" "
@input="setCategories"
/>
</label>
<label>
<span>
<h3>Additional Categories</h3>
<span class="no-padding">
Select up to 3 categories that will help others <br />
find your project.
</span>
</span>
<Multiselect
id="additional_categories"
v-model="newProject.additional_categories"
:options="selectableAdditionalCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="true"
:show-no-results="false"
:close-on-select="false"
:clear-on-select="false"
:show-labels="false"
:max="255"
:limit="6"
:hide-selected="true"
placeholder="Choose additional categories"
:disabled="
(currentMember.permissions & EDIT_DETAILS) !== EDIT_DETAILS
"
@input="setCategories"
/> />
</label> </label>
<label class="vertical-input"> <label class="vertical-input">
<span> <span>
<h3>Vanity URL (slug)<span class="required">*</span></h3> <h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description" <span class="slug-description"
>https://modrinth.com/{{ newProject.project_type.toLowerCase() }}/{{ >https://modrinth.com/{{ project.project_type.toLowerCase() }}/{{
newProject.slug ? newProject.slug : 'your-slug' newProject.slug ? newProject.slug : 'your-slug'
}} }}
</span> </span>
@@ -189,7 +218,10 @@
Reset Reset
</button> </button>
</section> </section>
<section class="card game-sides"> <section
v-if="project.project_type !== 'resourcepack'"
class="card game-sides"
>
<div class="columns"> <div class="columns">
<div> <div>
<h3>Supported environments</h3> <h3>Supported environments</h3>
@@ -241,7 +273,7 @@
for="body" for="body"
title="You can type an extended description of your project here." title="You can type an extended description of your project here."
> >
Body<span class="required">*</span> Description<span class="required">*</span>
</label> </label>
</h3> </h3>
<span> <span>
@@ -500,6 +532,9 @@ export default {
donationPlatforms: [], donationPlatforms: [],
donationLinks: [], donationLinks: [],
selectableCategories: [],
selectableAdditionalCategories: [],
isProcessing: false, isProcessing: false,
previewImage: null, previewImage: null,
compiledBody: '', compiledBody: '',
@@ -544,6 +579,8 @@ export default {
this.serverSideType = this.serverSideType =
this.newProject.server_side.charAt(0) + this.newProject.server_side.charAt(0) +
this.newProject.server_side.slice(1) this.newProject.server_side.slice(1)
this.setCategories()
}, },
watch: { watch: {
license(newValue, oldValue) { license(newValue, oldValue) {
@@ -584,6 +621,23 @@ export default {
this.DELETE_PROJECT = 1 << 7 this.DELETE_PROJECT = 1 << 7
}, },
methods: { methods: {
setCategories() {
this.selectableCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.project.project_type &&
!this.newProject.additional_categories.includes(x.name)
)
.map((it) => it.name)
this.selectableAdditionalCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.project.project_type &&
!this.newProject.categories.includes(x.name)
)
.map((it) => it.name)
},
checkFields() { checkFields() {
const reviewConditions = const reviewConditions =
this.newProject.body !== '' && this.newProject.versions.length > 0 this.newProject.body !== '' && this.newProject.versions.length > 0
@@ -626,6 +680,7 @@ export default {
description: this.newProject.description, description: this.newProject.description,
body: this.newProject.body, body: this.newProject.body,
categories: this.newProject.categories, categories: this.newProject.categories,
additional_categories: this.newProject.additional_categories,
issues_url: this.newProject.issues_url issues_url: this.newProject.issues_url
? this.newProject.issues_url ? this.newProject.issues_url
: null, : null,

View File

@@ -26,7 +26,7 @@
Back to list Back to list
</nuxt-link> </nuxt-link>
</div> </div>
<div> <div v-if="version">
<div v-if="mode === 'version'" class="version-header"> <div v-if="mode === 'version'" class="version-header">
<h2>{{ version.name }}</h2> <h2>{{ version.name }}</h2>
@@ -47,7 +47,7 @@
v-if="$auth.user" v-if="$auth.user"
:to="`/${project.project_type}/${ :to="`/${project.project_type}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`" }/version/${encodeURI(version.displayUrlEnding)}`"
class="iconified-button" class="iconified-button"
> >
<CrossIcon aria-hidden="true" /> <CrossIcon aria-hidden="true" />
@@ -113,7 +113,7 @@
class="action iconified-button" class="action iconified-button"
:to="`/${project.project_type}/${ :to="`/${project.project_type}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}/edit`" }/version/${encodeURI(version.displayUrlEnding)}/edit`"
@click.prevent="mode = 'edit'" @click.prevent="mode = 'edit'"
> >
<EditIcon aria-hidden="true" /> <EditIcon aria-hidden="true" />
@@ -209,8 +209,8 @@
color="red" color="red"
/> />
</div> </div>
<div class="data"> <div v-if="project.project_type !== 'resourcepack'" class="data">
<p class="title">Mod loaders</p> <p class="title">Loaders</p>
<multiselect <multiselect
v-if="mode === 'edit' || mode === 'create'" v-if="mode === 'edit' || mode === 'create'"
v-model="version.loaders" v-model="version.loaders"
@@ -223,12 +223,7 @@
) )
.map((it) => it.name) .map((it) => it.name)
" "
:custom-label=" :custom-label="(value) => $formatCategory(value)"
(value) =>
value === 'modloader'
? 'Risugami\'s ModLoader'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.loaders.length === 0" :loading="$tag.loaders.length === 0"
:multiple="true" :multiple="true"
:searchable="false" :searchable="false"
@@ -238,18 +233,10 @@
:show-labels="false" :show-labels="false"
:limit="6" :limit="6"
:hide-selected="true" :hide-selected="true"
placeholder="Choose mod loaders..." placeholder="Choose loaders..."
/> />
<p v-else class="value"> <p v-else class="value">
{{ {{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? "Risugami's ModLoader"
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
</p> </p>
</div> </div>
<div v-if="mode === 'version'" class="data"> <div v-if="mode === 'version'" class="data">
@@ -365,7 +352,7 @@
dependency.project.slug dependency.project.slug
? dependency.project.slug ? dependency.project.slug
: dependency.project.id : dependency.project.id
}/version/${encodeURIComponent( }/version/${encodeURI(
dependency.version.version_number dependency.version.version_number
)}` )}`
: `/${dependency.project.project_type}/${ : `/${dependency.project.project_type}/${
@@ -754,9 +741,18 @@ export default {
if (!this.version) if (!this.version)
this.version = this.versions.find( this.version = this.versions.find(
(x) => x.version_number === this.$route.params.version (x) => x.displayUrlEnding === this.$route.params.version
) )
if (!this.version) {
this.$nuxt.context.error({
statusCode: 404,
message: 'The page could not be found',
})
return
}
this.version = JSON.parse(JSON.stringify(this.version)) this.version = JSON.parse(JSON.stringify(this.version))
this.primaryFile = this.primaryFile =
this.version.files.find((file) => file.primary) ?? this.version.files[0] this.version.files.find((file) => file.primary) ?? this.version.files[0]
@@ -891,8 +887,9 @@ export default {
const index = this.versions.findIndex((x) => x.id === this.version.id) const index = this.versions.findIndex((x) => x.id === this.version.id)
editedVersions.splice(index, 1, version) editedVersions.splice(index, 1, version)
this.$emit('update:versions', editedVersions) const newEditedVersions = this.$computeVersions(editedVersions)
this.$emit('update:versions', newEditedVersions)
this.$emit('update:featuredVersions', featuredVersions) this.$emit('update:featuredVersions', featuredVersions)
this.newFiles = [] this.newFiles = []
@@ -901,7 +898,7 @@ export default {
await this.$router.replace( await this.$router.replace(
`/${this.project.project_type}/${ `/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.id this.project.slug ? this.project.slug : this.project.id
}/version/${encodeURIComponent(this.version.version_number)}` }/version/${encodeURI(newEditedVersions[index].displayUrlEnding)}`
) )
} catch (err) { } catch (err) {
this.$notify({ this.$notify({
@@ -921,6 +918,10 @@ export default {
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`) const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
if (this.project.project_type === 'resourcepack') {
this.version.loaders = ['minecraft']
}
const newVersion = { const newVersion = {
project_id: this.version.project_id, project_id: this.version.project_id,
file_parts: fileParts, file_parts: fileParts,
@@ -958,15 +959,19 @@ export default {
).data ).data
const newProject = JSON.parse(JSON.stringify(this.project)) const newProject = JSON.parse(JSON.stringify(this.project))
newProject.versions = newProject.versions.concat([data]) newProject.versions = newProject.versions.concat([data.id])
const newVersions = this.$computeVersions(this.versions.concat([data]))
await this.$emit('update:project', newProject) await this.$emit('update:project', newProject)
await this.$emit('update:versions', this.versions.concat([data])) await this.$emit('update:versions', newVersions)
await this.$router.push( await this.$router.push(
`/${this.project.project_type}/${ `/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.project_id this.project.slug ? this.project.slug : this.project.project_id
}/version/${encodeURIComponent(this.version.version_number)}` }/version/${encodeURI(
newVersions[newVersions.length - 1].displayUrlEnding
)}`
) )
} catch (err) { } catch (err) {
this.$notify({ this.$notify({

View File

@@ -48,7 +48,7 @@
<nuxt-link <nuxt-link
:to="`/${project.project_type}/${ :to="`/${project.project_type}/${
project.slug ? project.slug : project.id project.slug ? project.slug : project.id
}/version/${encodeURIComponent(version.version_number)}`" }/version/${encodeURI(version.displayUrlEnding)}`"
> >
{{ version.name }} {{ version.name }}
</nuxt-link> </nuxt-link>
@@ -78,11 +78,7 @@
<p> <p>
{{ {{
version.loaders version.loaders
.map((x) => .map((x) => $formatCategory(x))
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ') + .join(', ') +
' ' + ' ' +
$formatVersion(version.game_versions) $formatVersion(version.game_versions)
@@ -104,15 +100,7 @@
</td> </td>
<td> <td>
<p> <p>
{{ {{ version.loaders.map((x) => $formatCategory(x)).join(', ') }}
version.loaders
.map((x) =>
x.toLowerCase() === 'modloader'
? 'ModLoader'
: x.charAt(0).toUpperCase() + x.slice(1)
)
.join(', ')
}}
</p> </p>
<p>{{ $formatVersion(version.game_versions) }}</p> <p>{{ $formatVersion(version.game_versions) }}</p>
</td> </td>

View File

@@ -54,16 +54,18 @@
<label> <label>
<span> <span>
<h3>Type<span class="required">*</span></h3> <h3>Type<span class="required">*</span></h3>
<span class="no-padding">The type of project of your project.</span> <span class="no-padding">The type of project your project is.</span>
</span> </span>
<Multiselect <Multiselect
v-model="projectType" v-model="projectType"
placeholder="Select one" placeholder="Select one"
label="display"
:options="projectTypes" :options="projectTypes"
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
:show-labels="false" :show-labels="false"
:allow-empty="false" :allow-empty="false"
@input="setCategories(true)"
/> />
</label> </label>
<label> <label>
@@ -106,32 +108,59 @@
<multiselect <multiselect
id="categories" id="categories"
v-model="categories" v-model="categories"
:options=" :options="selectableCategories"
$tag.categories
.filter((x) => x.project_type === projectType.toLowerCase())
.map((it) => it.name)
"
:custom-label=" :custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1) (value) => value.charAt(0).toUpperCase() + value.slice(1)
" "
:loading="$tag.categories.length === 0" :loading="$tag.categories.length === 0"
:multiple="true" :multiple="true"
:searchable="false" :searchable="true"
:show-no-results="false" :show-no-results="false"
:close-on-select="false" :close-on-select="false"
:clear-on-select="false" :clear-on-select="true"
:show-labels="false" :show-labels="false"
:max="3" :max="3"
:limit="6" :limit="6"
:hide-selected="true" :hide-selected="true"
placeholder="Choose categories" placeholder="Choose categories"
@input="setCategories(false)"
/>
</label>
<label>
<span>
<h3>Additional Categories</h3>
<span class="no-padding">
Select more categories that will help others <br />
find your project. These are searchable, but not <br />
displayed in search.
</span>
</span>
<multiselect
id="additional_categories"
v-model="additional_categories"
:show-no-results="false"
:options="selectableAdditionalCategories"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.categories.length === 0"
:multiple="true"
:searchable="true"
:close-on-select="false"
:clear-on-select="true"
:show-labels="false"
:max="255"
:limit="6"
:hide-selected="true"
placeholder="Choose additional categories"
@input="setCategories(false)"
/> />
</label> </label>
<label> <label>
<span> <span>
<h3>Vanity URL (slug)<span class="required">*</span></h3> <h3>Vanity URL (slug)<span class="required">*</span></h3>
<span class="slug-description" <span class="slug-description"
>https://modrinth.com/{{ projectType.toLowerCase() }}/{{ >https://modrinth.com/{{ projectType.id }}/{{
slug ? slug : 'your-slug' slug ? slug : 'your-slug'
}} }}
</span> </span>
@@ -174,7 +203,13 @@
Reset Reset
</button> </button>
</section> </section>
<section class="card game-sides"> <section
v-if="
projectType.realId !== 'resourcepack' &&
projectType.realId !== 'plugin'
"
class="card game-sides"
>
<div class="columns"> <div class="columns">
<div class="column"> <div class="column">
<h3>Supported environments</h3> <h3>Supported environments</h3>
@@ -214,7 +249,7 @@
for="body" for="body"
title="You can type an extended description of your project here." title="You can type an extended description of your project here."
> >
Body<span class="required">*</span> Description<span class="required">*</span>
</label> </label>
</h3> </h3>
<span> <span>
@@ -318,7 +353,7 @@
Your version must have a unique version number. Your version must have a unique version number.
</li> </li>
<li v-if="versions[currentVersionIndex].loaders.length < 1"> <li v-if="versions[currentVersionIndex].loaders.length < 1">
Your version must have the supported mod loaders selected. Your version must have the supported loaders selected.
</li> </li>
<li v-if="versions[currentVersionIndex].game_versions.length < 1"> <li v-if="versions[currentVersionIndex].game_versions.length < 1">
Your version must have the supported Minecraft versions Your version must have the supported Minecraft versions
@@ -387,10 +422,10 @@
:allow-empty="false" :allow-empty="false"
/> />
</label> </label>
<label> <label v-if="projectType.realId !== 'resourcepack'">
<span> <span>
<h3>Mod loaders<span class="required">*</span></h3> <h3>Loaders<span class="required">*</span></h3>
<span> Select all mod loaders this version supports. </span> <span> Select all loaders this version supports. </span>
</span> </span>
<multiselect <multiselect
v-model="versions[currentVersionIndex].loaders" v-model="versions[currentVersionIndex].loaders"
@@ -401,19 +436,18 @@
}" }"
:options=" :options="
$tag.loaders $tag.loaders
.filter((x) => .filter((x) => {
x.supported_project_types.includes( if (projectType.realId === 'plugin') {
projectType.toLowerCase() return $tag.loaderData.allPluginLoaders.includes(x.name)
) } else if (projectType.realId === 'mod') {
) return $tag.loaderData.modLoaders.includes(x.name)
}
return x.supported_project_types.includes(projectType.id)
})
.map((it) => it.name) .map((it) => it.name)
" "
:custom-label=" :custom-label="(value) => $formatCategory(value)"
(value) =>
value === 'modloader'
? 'Risugami\'s ModLoader'
: value.charAt(0).toUpperCase() + value.slice(1)
"
:loading="$tag.loaders.length === 0" :loading="$tag.loaders.length === 0"
:multiple="true" :multiple="true"
:searchable="false" :searchable="false"
@@ -423,7 +457,7 @@
:show-labels="false" :show-labels="false"
:limit="6" :limit="6"
:hide-selected="true" :hide-selected="true"
placeholder="Choose mod loaders..." placeholder="Choose loaders..."
/> />
</label> </label>
<label> <label>
@@ -547,12 +581,9 @@
<h3>Files<span class="required">*</span></h3> <h3>Files<span class="required">*</span></h3>
<span> <span>
You may upload multiple files, but this should only be used for You may upload multiple files, but this should only be used for
cases like sources or Javadocs. cases like sources or Javadocs for mods/plugins.
</span> </span>
<p <p v-if="projectType.id === 'modpack'" aria-label="Warning">
v-if="projectType.toLowerCase() === 'modpack'"
aria-label="Warning"
>
Modpack support is currently in alpha, and you may encounter Modpack support is currently in alpha, and you may encounter
issues. Our documentation includes instructions on issues. Our documentation includes instructions on
<a <a
@@ -578,9 +609,9 @@
class="file-input" class="file-input"
multiple multiple
:accept=" :accept="
projectType.toLowerCase() === 'modpack' projectType.id === 'modpack'
? '.mrpack,application/x-modrinth-modpack+zip' ? '.mrpack,application/x-modrinth-modpack+zip'
: projectType.toLowerCase() === 'mod' : projectType.id === 'mod'
? '.jar,application/java-archive' ? '.jar,application/java-archive'
: '*' : '*'
" "
@@ -661,7 +692,7 @@
> >
<th>Name</th> <th>Name</th>
<th>Version</th> <th>Version</th>
<th>Mod loaders</th> <th>Project loaders</th>
<th>Minecraft versions</th> <th>Minecraft versions</th>
<th>Release channel</th> <th>Release channel</th>
<th>Actions</th> <th>Actions</th>
@@ -1137,6 +1168,7 @@ export default {
body: '', body: '',
versions: [], versions: [],
categories: [], categories: [],
additional_categories: [],
issues_url: null, issues_url: null,
source_url: null, source_url: null,
wiki_url: null, wiki_url: null,
@@ -1145,8 +1177,41 @@ export default {
license: null, license: null,
license_url: null, license_url: null,
projectTypes: ['Mod', 'Modpack'], selectableCategories: [],
projectType: 'Mod', selectableAdditionalCategories: [],
projectTypes: [
{
display: 'Mod',
id: 'mod',
realId: 'mod',
},
{
display: 'Plugin',
id: 'mod',
realId: 'plugin',
},
{
display: 'Mod and plugin',
id: 'mod',
realId: 'mod+plugin',
},
{
display: 'Modpack',
id: 'modpack',
realId: 'modpack',
},
{
display: 'Resource pack',
id: 'resourcepack',
realId: 'resourcepack',
},
],
projectType: {
display: 'Mod',
id: 'mod',
realId: 'mod',
},
sideTypes: ['Required', 'Optional', 'Unsupported'], sideTypes: ['Required', 'Optional', 'Unsupported'],
clientSideType: 'Required', clientSideType: 'Required',
@@ -1171,6 +1236,9 @@ export default {
savingAsDraft: false, savingAsDraft: false,
} }
}, },
fetch() {
this.setCategories()
},
watch: { watch: {
license(newValue, oldValue) { license(newValue, oldValue) {
if (newValue == null) { if (newValue == null) {
@@ -1199,6 +1267,28 @@ export default {
}) })
}, },
methods: { methods: {
setCategories(reset) {
this.selectableCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.projectType.id &&
!this.additional_categories.includes(x.name)
)
.map((it) => it.name)
this.selectableAdditionalCategories = this.$tag.categories
.filter(
(x) =>
x.project_type === this.projectType.id &&
!this.categories.includes(x.name)
)
.map((it) => it.name)
if (reset) {
this.categories = []
this.additional_categories = []
}
},
checkFields() { checkFields() {
const reviewConditions = this.body !== '' && this.versions.length > 0 const reviewConditions = this.body !== '' && this.versions.length > 0
if ( if (
@@ -1221,6 +1311,7 @@ export default {
return false return false
}, },
async createDraft() { async createDraft() {
this.setValues()
this.savingAsDraft = true this.savingAsDraft = true
if (this.checkFields()) { if (this.checkFields()) {
this.draft = true this.draft = true
@@ -1228,11 +1319,21 @@ export default {
} }
}, },
async createProjectForReview() { async createProjectForReview() {
this.setValues()
this.savingAsDraft = false this.savingAsDraft = false
if (this.checkFields()) { if (this.checkFields()) {
await this.createProject() await this.createProject()
} }
}, },
setValues() {
if (this.projectType.realId === 'resourcepack') {
this.clientSideType = 'required'
this.serverSideType = 'optional'
} else if (this.projectType.realId === 'plugin') {
this.clientSideType = 'unsupported'
this.serverSideType = 'required'
}
},
async createProject() { async createProject() {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
@@ -1256,7 +1357,7 @@ export default {
'data', 'data',
JSON.stringify({ JSON.stringify({
title: this.name, title: this.name,
project_type: this.projectType.toLowerCase(), project_type: this.projectType.id,
slug: this.slug, slug: this.slug,
description: this.description, description: this.description,
body: this.body, body: this.body,
@@ -1269,6 +1370,7 @@ export default {
}, },
], ],
categories: this.categories, categories: this.categories,
additional_categories: this.additional_categories,
issues_url: this.issues_url ? this.issues_url : null, issues_url: this.issues_url ? this.issues_url : null,
source_url: this.source_url ? this.source_url : null, source_url: this.source_url ? this.source_url : null,
wiki_url: this.wiki_url ? this.wiki_url : null, wiki_url: this.wiki_url ? this.wiki_url : null,
@@ -1405,6 +1507,9 @@ export default {
saveVersion() { saveVersion() {
const version = this.versions[this.currentVersionIndex] const version = this.versions[this.currentVersionIndex]
if (this.projectType.realId === 'resourcepack') {
version.loaders = ['minecraft']
}
if ( if (
version.version_number !== '' && version.version_number !== '' &&
version.releaseChannels !== null && version.releaseChannels !== null &&

View File

@@ -89,6 +89,7 @@
:client-side="project.client_side" :client-side="project.client_side"
:server-side="project.server_side" :server-side="project.server_side"
:type="project.project_type" :type="project.project_type"
:moderation="true"
> >
<button <button
class="iconified-button" class="iconified-button"

View File

@@ -76,8 +76,8 @@ import UpToDate from '~/assets/images/illustrations/up_to_date.svg?inline'
import ThisOrThat from '~/components/ui/ThisOrThat' import ThisOrThat from '~/components/ui/ThisOrThat'
const NOTIFICATION_TYPES = { const NOTIFICATION_TYPES = {
'Team Invites': 'team_invite', 'Team invites': 'team_invite',
'Project Updates': 'project_update', 'Project updates': 'project_update',
} }
export default { export default {

View File

@@ -34,28 +34,41 @@
Clear filters Clear filters
</button> </button>
<section aria-label="Category filters"> <section aria-label="Category filters">
<h3 <div v-for="(categories, header) in categoriesMap" :key="header">
v-if=" <h3
$tag.categories.filter((x) => x.project_type === projectType) v-if="
.length > 0 categories.filter((x) => x.project_type === projectType)
" .length > 0
class="sidebar-menu-heading" "
> class="sidebar-menu-heading"
Categories >
</h3> {{ $formatCategoryHeader(header) }}
<SearchFilter </h3>
v-for="category in $tag.categories.filter(
(x) => x.project_type === projectType <SearchFilter
)" v-for="category in categories
:key="category.name" .filter((x) => x.project_type === projectType)
:active-filters="facets" .sort((a, b) => {
:display-name="category.name" if (header === 'resolutions') {
:facet-name="`categories:${category.name}`" return (
:icon="category.icon" a.name.replace(/\D/g, '') - b.name.replace(/\D/g, '')
@toggle="toggleFacet" )
/> }
return 0
})"
:key="category.name"
:active-filters="facets"
:display-name="$formatCategory(category.name)"
:facet-name="`categories:${category.name}`"
:icon="header === 'resolutions' ? null : category.icon"
@toggle="toggleFacet"
/>
</div>
</section> </section>
<section aria-label="Loader filters"> <section
v-if="projectType !== 'resourcepack'"
aria-label="Loader filters"
>
<h3 <h3
v-if=" v-if="
$tag.loaders.filter((x) => $tag.loaders.filter((x) =>
@@ -69,6 +82,8 @@
<SearchFilter <SearchFilter
v-for="loader in $tag.loaders.filter((x) => { v-for="loader in $tag.loaders.filter((x) => {
if ( if (
projectType === 'mod' &&
!isPlugins &&
!showAllLoaders && !showAllLoaders &&
x.name !== 'forge' && x.name !== 'forge' &&
x.name !== 'fabric' && x.name !== 'fabric' &&
@@ -76,18 +91,25 @@
) { ) {
return false return false
} }
return x.supported_project_types.includes(projectType)
if (projectType === 'mod' && showAllLoaders) {
return $tag.loaderData.modLoaders.includes(x.name)
}
return isPlugins
? $tag.loaderData.pluginLoaders.includes(x.name)
: x.supported_project_types.includes(projectType)
})" })"
:key="loader.name" :key="loader.name"
ref="loaderFilters"
:active-filters="orFacets" :active-filters="orFacets"
:display-name=" :display-name="$formatCategory(loader.name)"
loader.name === 'modloader' ? 'ModLoader' : loader.name
"
:facet-name="`categories:${loader.name}`" :facet-name="`categories:${loader.name}`"
:icon="loader.icon" :icon="loader.icon"
@toggle="toggleOrFacet" @toggle="toggleOrFacet"
/> />
<Checkbox <Checkbox
v-if="projectType === 'mod' && !isPlugins"
v-model="showAllLoaders" v-model="showAllLoaders"
:label="showAllLoaders ? 'Less' : 'More'" :label="showAllLoaders ? 'Less' : 'More'"
description="Show all loaders" description="Show all loaders"
@@ -96,7 +118,34 @@
:collapsing-toggle-style="true" :collapsing-toggle-style="true"
/> />
</section> </section>
<section aria-label="Environment filters"> <section v-if="isPlugins" aria-label="Platform loader filters">
<h3
v-if="
$tag.loaders.filter((x) =>
x.supported_project_types.includes(projectType)
).length > 0
"
class="sidebar-menu-heading"
>
Proxies
</h3>
<SearchFilter
v-for="loader in $tag.loaders.filter((x) =>
$tag.loaderData.pluginPlatformLoaders.includes(x.name)
)"
:key="loader.name"
ref="platformFilters"
:active-filters="orFacets"
:display-name="$formatCategory(loader.name)"
:facet-name="`categories:${loader.name}`"
:icon="loader.icon"
@toggle="toggleOrFacet"
/>
</section>
<section
v-if="projectType !== 'resourcepack' && !isPlugins"
aria-label="Environment filters"
>
<h3 class="sidebar-menu-heading">Environments</h3> <h3 class="sidebar-menu-heading">Environments</h3>
<SearchFilter <SearchFilter
:active-filters="selectedEnvironments" :active-filters="selectedEnvironments"
@@ -267,7 +316,7 @@
:icon-url="result.icon_url" :icon-url="result.icon_url"
:client-side="result.client_side" :client-side="result.client_side"
:server-side="result.server_side" :server-side="result.server_side"
:categories="result.categories" :categories="result.display_categories"
/> />
<div v-if="results && results.length === 0" class="no-results"> <div v-if="results && results.length === 0" class="no-results">
<p>No results found for your query!</p> <p>No results found for your query!</p>
@@ -336,6 +385,7 @@ export default {
currentPage: 1, currentPage: 1,
projectType: 'mod', projectType: 'mod',
isPlugins: false,
sortTypes: [ sortTypes: [
{ display: 'Relevance', name: 'relevance' }, { display: 'Relevance', name: 'relevance' },
@@ -405,8 +455,39 @@ export default {
this.$route.name.length - 1 this.$route.name.length - 1
) )
if (this.projectType === 'plugin') {
this.projectType = 'mod'
this.isPlugins = true
}
await this.onSearchChange(this.currentPage) await this.onSearchChange(this.currentPage)
}, },
computed: {
categoriesMap() {
const categories = {}
for (const category of this.$tag.categories) {
if (categories[category.header]) {
categories[category.header].push(category)
} else {
categories[category.header] = [category]
}
}
const newVals = Object.keys(categories)
.sort()
.reduce((obj, key) => {
obj[key] = categories[key]
return obj
}, {})
for (const header of Object.keys(categories)) {
newVals[header].sort((a, b) => a.name.localeCompare(b.name))
}
return newVals
},
},
watch: { watch: {
'$route.path': { '$route.path': {
async handler() { async handler() {
@@ -415,12 +496,21 @@ export default {
this.$route.name.length - 1 this.$route.name.length - 1
) )
if (this.projectType === 'plugin') {
this.projectType = 'mod'
this.isPlugins = true
} else {
this.isPlugins = false
}
this.results = null this.results = null
this.pages = [] this.pages = []
this.currentPage = 1 this.currentPage = 1
this.query = '' this.query = ''
this.maxResults = 20 this.maxResults = 20
this.sortType = { display: 'Relevance', name: 'relevance' } this.sortType = { display: 'Relevance', name: 'relevance' }
this.showAllLoaders = false
this.sidebarMenuOpen = false
await this.clearFilters() await this.clearFilters()
}, },
@@ -463,6 +553,18 @@ export default {
if (index !== -1) { if (index !== -1) {
this.orFacets.splice(index, 1) this.orFacets.splice(index, 1)
} else { } else {
if (elementName === 'categories:purpur') {
this.orFacets.push('categories:paper')
this.orFacets.push('categories:spigot')
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:paper') {
this.orFacets.push('categories:spigot')
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:spigot') {
this.orFacets.push('categories:bukkit')
} else if (elementName === 'categories:waterfall') {
this.orFacets.push('categories:bungeecord')
}
this.orFacets.push(elementName) this.orFacets.push(elementName)
} }
@@ -498,6 +600,7 @@ export default {
if ( if (
this.facets.length > 0 || this.facets.length > 0 ||
this.orFacets.length > 0 ||
this.selectedVersions.length > 0 || this.selectedVersions.length > 0 ||
this.selectedEnvironments.length > 0 || this.selectedEnvironments.length > 0 ||
this.projectType this.projectType
@@ -507,8 +610,19 @@ export default {
formattedFacets.push([facet]) formattedFacets.push([facet])
} }
// loaders specifier
if (this.orFacets.length > 0) { if (this.orFacets.length > 0) {
formattedFacets.push(this.orFacets) formattedFacets.push(this.orFacets)
} else if (this.isPlugins) {
formattedFacets.push(
this.$tag.loaderData.allPluginLoaders.map(
(x) => `categories:${x}`
)
)
} else if (this.projectType === 'mod') {
formattedFacets.push(
this.$tag.loaderData.modLoaders.map((x) => `categories:${x}`)
)
} }
if (this.selectedVersions.length > 0) { if (this.selectedVersions.length > 0) {

View File

@@ -5,9 +5,6 @@
<script> <script>
export default { export default {
name: 'Modpacks', name: 'Modpacks',
asyncData(ctx) {
ctx.params.projectType = 'modpack'
},
head: { head: {
title: 'Modpacks - Modrinth', title: 'Modpacks - Modrinth',
meta: [ meta: [

View File

@@ -5,9 +5,6 @@
<script> <script>
export default { export default {
name: 'Mods', name: 'Mods',
asyncData(ctx) {
ctx.params.projectType = 'mod'
},
head: { head: {
title: 'Mods - Modrinth', title: 'Mods - Modrinth',
meta: [ meta: [

31
pages/search/plugins.vue Normal file
View File

@@ -0,0 +1,31 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'Plugins',
head: {
title: 'Plugins - Modrinth',
meta: [
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Plugins - Modrinth',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Plugins - Modrinth',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/plugins`,
},
],
},
}
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,31 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'ResourcePacks',
head: {
title: 'Resource packs - Modrinth',
meta: [
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'Resource packs - Modrinth',
},
{
hid: 'og:title',
name: 'og:title',
content: 'Resource packs - Modrinth',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/resourcepacks`,
},
],
},
}
</script>
<style lang="scss" scoped></style>

View File

@@ -204,6 +204,9 @@ export default {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
gap: 0.5rem; gap: 0.5rem;
flex-wrap: wrap;
justify-content: flex-end;
} }
} }
</style> </style>

View File

@@ -90,7 +90,8 @@
<ProjectCard <ProjectCard
v-for="project in selectedProjectType !== 'all' v-for="project in selectedProjectType !== 'all'
? projects.filter( ? projects.filter(
(x) => x.project_type === selectedProjectType.slice(0, -1) (x) =>
x.project_type === convertProjectType(selectedProjectType)
) )
: projects" : projects"
:id="project.slug || project.id" :id="project.slug || project.id"
@@ -246,13 +247,24 @@ export default {
const obj = { all: true } const obj = { all: true }
for (const project of this.projects) { for (const project of this.projects) {
obj[project.project_type + 's'] = true if (project.project_type === 'resourcepack') {
obj['Resource Packs'] = true
} else {
obj[project.project_type + 's'] = true
}
} }
return Object.keys(obj) return Object.keys(obj)
}, },
}, },
methods: { methods: {
convertProjectType(name) {
if (name === 'Resource Packs') {
return 'resourcepack'
} else {
return name.slice(0, -1)
}
},
sumDownloads() { sumDownloads() {
let sum = 0 let sum = 0

View File

@@ -1,7 +1,7 @@
export default ({ store }, inject) => { export default (ctx, inject) => {
inject('user', store.state.user) inject('user', ctx.store.state.user)
inject('tag', store.state.tag) inject('tag', ctx.store.state.tag)
inject('auth', store.state.auth) inject('auth', ctx.store.state.auth)
inject('defaultHeaders', () => { inject('defaultHeaders', () => {
const obj = { headers: {} } const obj = { headers: {} }
@@ -9,100 +9,143 @@ export default ({ store }, inject) => {
obj.headers['x-ratelimit-key'] = process.env.RATE_LIMIT_IGNORE_KEY || '' obj.headers['x-ratelimit-key'] = process.env.RATE_LIMIT_IGNORE_KEY || ''
} }
if (store.state.auth.user) { if (ctx.store.state.auth.user) {
obj.headers.Authorization = store.state.auth.token obj.headers.Authorization = ctx.store.state.auth.token
} }
return obj return obj
}) })
inject('formatNumber', formatNumber) inject('formatNumber', formatNumber)
inject('formatVersion', (versionArray) => { inject('formatVersion', (versionsArray) =>
const allVersions = store.state.tag.gameVersions.slice().reverse() formatVersions(versionsArray, ctx.store)
const allReleases = allVersions.filter((x) => x.version_type === 'release') )
inject('formatBytes', formatBytes)
inject('formatProjectType', formatProjectType)
inject('formatCategory', formatCategory)
inject('formatCategoryHeader', formatCategoryHeader)
inject('computeVersions', (versions) => {
const versionsMap = {}
const intervals = [] for (const version of versions.reverse()) {
let currentInterval = 0 if (versionsMap[version.version_number]) {
versionsMap[version.version_number].push(version)
for (let i = 0; i < versionArray.length; i++) {
const index = allVersions.findIndex((x) => x.version === versionArray[i])
const releaseIndex = allReleases.findIndex(
(x) => x.version === versionArray[i]
)
if (i === 0) {
intervals.push([[versionArray[i], index, releaseIndex]])
} else { } else {
const intervalBase = intervals[currentInterval] versionsMap[version.version_number] = [version]
if (
(index - intervalBase[intervalBase.length - 1][1] === 1 ||
releaseIndex - intervalBase[intervalBase.length - 1][2] === 1) &&
(allVersions[intervalBase[0][1]].version_type === 'release' ||
allVersions[index].version_type !== 'release')
) {
intervalBase[1] = [versionArray[i], index, releaseIndex]
} else {
currentInterval += 1
intervals[currentInterval] = [[versionArray[i], index, releaseIndex]]
}
} }
} }
const newIntervals = [] const returnVersions = []
for (let i = 0; i < intervals.length; i++) {
const interval = intervals[i]
if ( for (const id in versionsMap) {
interval.length === 2 && const versions = versionsMap[id]
interval[0][2] !== -1 &&
interval[1][2] === -1
) {
let lastSnapshot = null
for (let j = interval[1][1]; j > interval[0][1]; j--) {
if (allVersions[j].version_type === 'release') {
newIntervals.push([
interval[0],
[
allVersions[j].version,
j,
allReleases.findIndex(
(x) => x.version === allVersions[j].version
),
],
])
if (lastSnapshot !== null && lastSnapshot !== j + 1) { if (versions.length === 1) {
newIntervals.push([ versions[0].displayUrlEnding = versions[0].version_number
[allVersions[lastSnapshot].version, lastSnapshot, -1],
interval[1], returnVersions.push(versions[0])
]) } else {
const reservedNames = {}
const seenLoaders = {}
const duplicateLoaderIndexes = []
for (let i = 0; i < versions.length; i++) {
const version = versions[i]
const value = version.loaders.join('+')
if (seenLoaders[value]) {
duplicateLoaderIndexes.push(i)
} else {
if (i !== 0) {
version.displayUrlEnding = `${version.version_number}-${value}`
} else { } else {
newIntervals.push([interval[1]]) version.displayUrlEnding = version.version_number
} }
break reservedNames[version.displayUrlEnding] = true
} else {
lastSnapshot = j version.displayName = version.loaders
.map((x) => x.charAt(0).toUpperCase() + x.slice(1))
.join(', ')
returnVersions.push(version)
seenLoaders[value] = true
} }
} }
} else {
newIntervals.push(interval) const seenGameVersions = {}
const duplicateGameVersionIndexes = []
for (const i of duplicateLoaderIndexes) {
const version = versions[i]
const value = version.game_versions.join('+')
if (seenGameVersions[value]) {
duplicateGameVersionIndexes.push(i)
} else {
if (i !== 0) {
let setDisplayUrl = false
for (const gameVersion in version.game_versions) {
const displayUrlEnding = `${version.version_number}-${gameVersion}`
if (!reservedNames[version.version_number]) {
version.displayUrlEnding = displayUrlEnding
reservedNames[displayUrlEnding] = true
setDisplayUrl = true
break
}
}
if (!setDisplayUrl) {
version.displayUrlEnding = `${version.version_number}-${value}`
}
} else if (!reservedNames[version.version_number]) {
version.displayUrlEnding = version.version_number
reservedNames[version.version_number] = true
}
version.displayName = formatVersions(
version.game_versions,
ctx.store
)
returnVersions.push(version)
seenGameVersions[value] = true
}
}
for (const i in duplicateGameVersionIndexes) {
const version = versions[i]
version.displayUrlEnding = version.id
version.displayName = version.id
returnVersions.push(version)
}
} }
} }
const output = [] return returnVersions.reverse()
})
for (const interval of newIntervals) { inject('getProjectTypeForDisplay', (type, categories) => {
if (interval.length === 2) { if (type === 'mod') {
output.push(`${interval[0][0]}${interval[1][0]}`) const isPlugin = categories.some((category) => {
} else { return ctx.store.state.tag.loaderData.allPluginLoaders.includes(
output.push(interval[0][0]) category
} )
} })
const isMod = categories.some((category) => {
return output.join(', ') return ctx.store.state.tag.loaderData.modLoaders.includes(category)
})
return isPlugin && isMod ? 'mod and plugin' : isPlugin ? 'plugin' : 'mod'
} else {
return formatProjectType(type)
}
}) })
inject('formatBytes', formatBytes)
} }
export const formatNumber = (number) => { export const formatNumber = (number) => {
@@ -127,3 +170,126 @@ export const formatBytes = (bytes, decimals = 2) => {
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i] return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]
} }
export const formatProjectType = (name) => {
if (name === 'resourcepack') {
return 'resource pack'
}
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatCategory = (name) => {
if (name === 'modloader') {
return "Risugami's ModLoader"
} else if (name === 'bungeecord') {
return 'BungeeCord'
} else if (name === 'liteloader') {
return 'LiteLoader'
} else if (name === 'game-mechanics') {
return 'Game Mechanics'
} else if (name === 'worldgen') {
return 'World Generation'
} else if (name === 'core-shaders') {
return 'Core Shaders'
} else if (name === 'gui') {
return 'GUI'
} else if (name === '8x-') {
return '8x or lower'
} else if (name === '512x+') {
return '512x or higher'
} else if (name === 'kitchen-sink') {
return 'Kitchen Sink'
}
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatCategoryHeader = (name) => {
return name.charAt(0).toUpperCase() + name.slice(1)
}
export const formatVersions = (versionArray, store) => {
const allVersions = store.state.tag.gameVersions.slice().reverse()
const allReleases = allVersions.filter((x) => x.version_type === 'release')
const intervals = []
let currentInterval = 0
for (let i = 0; i < versionArray.length; i++) {
const index = allVersions.findIndex((x) => x.version === versionArray[i])
const releaseIndex = allReleases.findIndex(
(x) => x.version === versionArray[i]
)
if (i === 0) {
intervals.push([[versionArray[i], index, releaseIndex]])
} else {
const intervalBase = intervals[currentInterval]
if (
(index - intervalBase[intervalBase.length - 1][1] === 1 ||
releaseIndex - intervalBase[intervalBase.length - 1][2] === 1) &&
(allVersions[intervalBase[0][1]].version_type === 'release' ||
allVersions[index].version_type !== 'release')
) {
intervalBase[1] = [versionArray[i], index, releaseIndex]
} else {
currentInterval += 1
intervals[currentInterval] = [[versionArray[i], index, releaseIndex]]
}
}
}
const newIntervals = []
for (let i = 0; i < intervals.length; i++) {
const interval = intervals[i]
if (
interval.length === 2 &&
interval[0][2] !== -1 &&
interval[1][2] === -1
) {
let lastSnapshot = null
for (let j = interval[1][1]; j > interval[0][1]; j--) {
if (allVersions[j].version_type === 'release') {
newIntervals.push([
interval[0],
[
allVersions[j].version,
j,
allReleases.findIndex(
(x) => x.version === allVersions[j].version
),
],
])
if (lastSnapshot !== null && lastSnapshot !== j + 1) {
newIntervals.push([
[allVersions[lastSnapshot].version, lastSnapshot, -1],
interval[1],
])
} else {
newIntervals.push([interval[1]])
}
break
} else {
lastSnapshot = j
}
}
} else {
newIntervals.push(interval)
}
}
const output = []
for (const interval of newIntervals) {
if (interval.length === 2) {
output.push(`${interval[0][0]}${interval[1][0]}`)
} else {
output.push(interval[0][0])
}
}
return output.join(', ')
}

View File

@@ -1,5 +1,8 @@
import xss from 'xss' import xss from 'xss'
/**
* @type {import('xss').IFilterXSSOptions}
*/
const options = { const options = {
whiteList: { whiteList: {
...xss.whiteList, ...xss.whiteList,
@@ -12,6 +15,12 @@ const options = {
h6: ['id'], h6: ['id'],
input: ['checked', 'disabled', 'type'], input: ['checked', 'disabled', 'type'],
iframe: ['width', 'height', 'allowfullscreen', 'frameborder'], iframe: ['width', 'height', 'allowfullscreen', 'frameborder'],
img: [...xss.whiteList.img, 'style'],
},
css: {
whiteList: {
'image-rendering': /^pixelated$/,
},
}, },
onIgnoreTagAttr: (tag, name, value) => { onIgnoreTagAttr: (tag, name, value) => {
// Allow iframes from acceptable sources // Allow iframes from acceptable sources

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -4,6 +4,21 @@ export const state = () => ({
gameVersions: [], gameVersions: [],
licenses: [], licenses: [],
donationPlatforms: [], donationPlatforms: [],
loaderData: {
pluginLoaders: ['bukkit', 'spigot', 'paper', 'purpur', 'sponge'],
pluginPlatformLoaders: ['bungeecord', 'waterfall', 'velocity'],
allPluginLoaders: [
'bukkit',
'spigot',
'paper',
'purpur',
'sponge',
'bungeecord',
'waterfall',
'velocity',
],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
},
}) })
export const mutations = { export const mutations = {