Datapack support (#815)

* Shader support PR

* Make search page work

* Fix env showing

* Make moderation look reasonable

* Fix search for shaders

* Datapack support

* Make file types work + datapack inferring

* Add editing to file types

* Finish datapack file generation

* Fix bugs, make forge support work

* Fix inconsistent data pack label

* Final fixes
This commit is contained in:
Geometrically
2022-12-29 09:59:41 -07:00
committed by GitHub
parent 879576b613
commit 1f133dbcd0
22 changed files with 978 additions and 262 deletions

View File

@@ -85,7 +85,14 @@
border-bottom: 1px solid var(--color-header-underline); border-bottom: 1px solid var(--color-header-underline);
} }
h1, h2, h3, h4, h5, h6, li, p { h1,
h2,
h3,
h4,
h5,
h6,
li,
p {
word-wrap: break-word; word-wrap: break-word;
overflow-wrap: anywhere; overflow-wrap: anywhere;
} }
@@ -227,11 +234,17 @@
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
iframe,
video {
aspect-ratio: 16 / 9;
width: 850px;
height: auto;
}
@media screen and (max-width: 850px) { @media screen and (max-width: 850px) {
iframe { iframe,
aspect-ratio: 16 / 9; video {
width: 100%; width: 100%;
height: auto;
} }
} }
} }
@@ -332,8 +345,8 @@
} }
.button-animation { .button-animation {
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
outline 0.2s ease-in-out; transform 0.05s ease-in-out, outline 0.2s ease-in-out;
&:active:not(&:disabled) { &:active:not(&:disabled) {
transform: scale(0.95); transform: scale(0.95);
@@ -403,8 +416,8 @@ tr.button-transparent {
} }
.button-within { .button-within {
transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out, transform 0.05s ease-in-out, transition: opacity 0.5s ease-in-out, filter 0.2s ease-in-out,
outline 0.2s ease-in-out; transform 0.05s ease-in-out, outline 0.2s ease-in-out;
&:focus-visible:not(&.disabled), &:focus-visible:not(&.disabled),
&:hover:not(&.disabled) { &:hover:not(&.disabled) {
@@ -541,6 +554,11 @@ tr.button-transparent {
--text-color: var(--color-brand-inverted); --text-color: var(--color-brand-inverted);
} }
.alt-brand-button {
--background-color: var(--color-brand-highlight);
--text-color: var(--color-text);
}
.button-group { .button-group {
display: flex; display: flex;
grid-gap: var(--spacing-card-sm); grid-gap: var(--spacing-card-sm);
@@ -563,6 +581,15 @@ tr.button-transparent {
} }
.multiselect { .multiselect {
&.raised-multiselect {
.multiselect__tags,
.multiselect__content-wrapper,
.multiselect__spinner {
background-color: var(--color-raised-bg);
box-shadow: none;
}
}
color: var(--color-text) !important; color: var(--color-text) !important;
outline: 2px solid transparent; outline: 2px solid transparent;
@@ -584,7 +611,7 @@ tr.button-transparent {
padding-left: 7px; padding-left: 7px;
padding-top: 10px; padding-top: 10px;
transition: background-color .1s ease-in-out; transition: background-color 0.1s ease-in-out;
&:active { &:active {
background: var(--color-button-bg-hover); background: var(--color-button-bg-hover);
@@ -790,7 +817,7 @@ tr.button-transparent {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
grid-gap: .5rem; grid-gap: 0.5rem;
z-index: 2; z-index: 2;
} }
@@ -929,7 +956,10 @@ h1 {
font-weight: bold; font-weight: bold;
} }
.nuxt-link-exact-active, h1, h2, h3 { .nuxt-link-exact-active,
h1,
h2,
h3 {
.beta-badge { .beta-badge {
background-color: var(--color-button-text-active); background-color: var(--color-button-text-active);
box-sizing: border-box; box-sizing: border-box;
@@ -939,7 +969,8 @@ h1 {
} }
@media (prefers-reduced-motion) { @media (prefers-reduced-motion) {
.button-animation, button { .button-animation,
button {
transform: none !important; transform: none !important;
} }
} }
@@ -965,7 +996,7 @@ h1 {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: flex-end; align-items: flex-end;
grid-gap: .5rem; grid-gap: 0.5rem;
z-index: 2; z-index: 2;
} }
@@ -986,7 +1017,8 @@ h1 {
} }
.universal-labels { .universal-labels {
label, .label { label,
.label {
.label__title { .label__title {
display: block; display: block;
margin-block: var(--spacing-card-md) var(--spacing-card-sm); margin-block: var(--spacing-card-md) var(--spacing-card-sm);
@@ -1074,13 +1106,16 @@ h1 {
} }
.input-group { .input-group {
.multiselect, input { .multiselect,
input {
width: auto; width: auto;
flex-basis: 0; flex-basis: 0;
} }
} }
button, .button, .iconified-button { button,
.button,
.iconified-button {
width: fit-content; width: fit-content;
} }
@@ -1095,12 +1130,14 @@ h1 {
} }
} }
.adjacent-input, &.adjacent-input { .adjacent-input,
&.adjacent-input {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
.iconified-button, .input-group { .iconified-button,
.input-group {
flex-shrink: 0; flex-shrink: 0;
} }
@@ -1152,7 +1189,9 @@ h1 {
} }
.full-width-inputs { .full-width-inputs {
.multiselect, input, .iconified-input { .multiselect,
input,
.iconified-input {
width: 100%; width: 100%;
flex-basis: 100%; flex-basis: 100%;
} }
@@ -1286,7 +1325,8 @@ button {
flex-shrink: 0; flex-shrink: 0;
} }
input, textarea { input,
textarea {
border-radius: 0; border-radius: 0;
background-color: transparent; background-color: transparent;
box-shadow: unset; box-shadow: unset;
@@ -1294,8 +1334,10 @@ button {
flex-grow: 1; flex-grow: 1;
} }
&:focus, &:focus-visible, &:focus-within { &:focus,
box-shadow: inset 0 0 0 transparent, 0 0 0 .25rem var(--color-brand-shadow); &:focus-visible,
&:focus-within {
box-shadow: inset 0 0 0 transparent, 0 0 0 0.25rem var(--color-brand-shadow);
color: var(--color-button-text-active); color: var(--color-button-text-active);
} }
} }
@@ -1365,7 +1407,8 @@ button {
} }
@media screen and (max-width: 550px) { @media screen and (max-width: 550px) {
grid-template-columns: repeat(1, minmax(0, 1fr)); display: flex;
flex-direction: column;
} }
} }

View File

@@ -61,8 +61,8 @@ html {
--color-special-orange: #e08325; --color-special-orange: #e08325;
--color-special-green: var(--color-brand-green); --color-special-green: var(--color-brand-green);
--color-special-blue: #1f68c0; --color-special-blue: #1f68c0;
--color-special-purple: #8e32F3; --color-special-purple: #8e32f3;
--color-special-gray: #595B61; --color-special-gray: #595b61;
--color-warning-bg: hsl(355, 70%, 88%); --color-warning-bg: hsl(355, 70%, 88%);
--color-warning-text: hsl(342, 70%, 35%); --color-warning-text: hsl(342, 70%, 35%);
@@ -86,8 +86,9 @@ html {
--shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15), --shadow-raised: 0.3px 0.5px 0.6px hsl(var(--shadow-color) / 0.15),
1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12), 1px 2px 2.2px -1.7px hsl(var(--shadow-color) / 0.12),
4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09); 4.4px 8.8px 9.7px -3.4px hsl(var(--shadow-color) / 0.09);
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px; hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px,
hsla(0, 0%, 0%, 0.1) 0px 2px 4px -1px;
--shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px; --shadow-card: rgba(50, 50, 100, 0.1) 0px 2px 4px 0px;
} }
@@ -109,9 +110,9 @@ html {
--color-special-red: #ff496e; --color-special-red: #ff496e;
--color-special-orange: #ffa347; --color-special-orange: #ffa347;
--color-special-green: var(--color-brand-green); --color-special-green: var(--color-brand-green);
--color-special-blue: #4F9CFF; --color-special-blue: #4f9cff;
--color-special-purple: #C78AFF; --color-special-purple: #c78aff;
--color-special-gray: #9FA4B3; --color-special-gray: #9fa4b3;
--color-brand-green: #1bd96a; --color-brand-green: #1bd96a;
--color-brand: var(--color-brand-green); --color-brand: var(--color-brand-green);
@@ -168,8 +169,9 @@ html {
--shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2); --shadow-raised-lg: 0px 2px 4px hsla(221, 39%, 11%, 0.2);
--shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1); --shadow-raised: 0px -2px 4px hsla(221, 39%, 11%, 0.1);
--shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, --shadow-floating: hsla(0, 0%, 0%, 0) 0px 0px 0px 0px,
hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px, rgba(0, 0, 0, 0.06) 0px 2px 4px -1px; hsla(0, 0%, 0%, 0) 0px 0px 0px 0px, hsla(0, 0%, 0%, 0.1) 0px 4px 6px -1px,
rgba(0, 0, 0, 0.06) 0px 2px 4px -1px;
--shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px; --shadow-card: rgba(0, 0, 0, 0.25) 0px 2px 4px 0px;
} }
@@ -194,8 +196,10 @@ body {
// Defaults // Defaults
background-color: var(--color-bg); background-color: var(--color-bg);
color: var(--color-text); color: var(--color-text);
--font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen, Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; --font-standard: Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Oxygen,
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; Ubuntu, Roboto, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
--mono-font: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
Liberation Mono, monospace;
font-family: var(--font-standard); font-family: var(--font-standard);
font-size: 16px; font-size: 16px;
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
@@ -216,7 +220,7 @@ body {
--size-navbar-height: 3.5rem; --size-navbar-height: 3.5rem;
--size-mobile-navbar-height: 3.5rem; --size-mobile-navbar-height: 3.5rem;
--size-mobile-navbar-height-expanded: 11.75rem; --size-mobile-navbar-height-expanded: 13.75rem;
--spacing-card-lg: 1.5rem; --spacing-card-lg: 1.5rem;
--spacing-card-bg: 1rem; --spacing-card-bg: 1rem;
@@ -245,11 +249,15 @@ body {
--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: 318px) { @media screen and (min-width: 320px) {
--size-mobile-navbar-height-expanded: 11.5rem;
}
@media screen and (min-width: 432px) {
--size-mobile-navbar-height-expanded: 9.25rem; --size-mobile-navbar-height-expanded: 9.25rem;
} }
@media screen and (min-width: 625px) { @media screen and (min-width: 765px) {
--size-mobile-navbar-height-expanded: 7rem; --size-mobile-navbar-height-expanded: 7rem;
} }
} }
@@ -304,8 +312,9 @@ textarea {
transition: box-shadow 0.1s ease-in-out; transition: box-shadow 0.1s ease-in-out;
min-height: 40px; min-height: 40px;
&:focus, &:focus-visible { &:focus,
box-shadow: inset 0 0 0 transparent, 0 0 0 .25rem var(--color-brand-shadow); &:focus-visible {
box-shadow: inset 0 0 0 transparent, 0 0 0 0.25rem var(--color-brand-shadow);
color: var(--color-button-text-active); color: var(--color-button-text-active);
} }
@@ -326,7 +335,8 @@ textarea {
} }
} }
button, input[type=button] { button,
input[type='button'] {
cursor: pointer; cursor: pointer;
border: none; border: none;
outline: 2px solid transparent; outline: 2px solid transparent;
@@ -348,7 +358,9 @@ kbd {
@import '~assets/styles/components.scss'; @import '~assets/styles/components.scss';
@import '~assets/styles/normalize.scss'; @import '~assets/styles/normalize.scss';
button:focus-visible, a:focus-visible, [tabindex="0"]:focus-visible { button:focus-visible,
outline: .25rem solid #ea80ff; a:focus-visible,
border-radius: .25rem; [tabindex='0']:focus-visible {
outline: 0.25rem solid #ea80ff;
border-radius: 0.25rem;
} }

View File

@@ -27,9 +27,12 @@ export default {
}, },
}, },
mounted() { mounted() {
document.addEventListener('dragenter', () => { // eslint-disable-next-line nuxt/no-env-in-hooks
this.$refs.drop_area.style.visibility = 'visible' if (process.client) {
}) document.addEventListener('dragenter', () => {
this.$refs.drop_area.style.visibility = 'visible'
})
}
}, },
methods: { methods: {
allowDrag(event) { allowDrag(event) {

View File

@@ -123,6 +123,8 @@ export default {
return 'required' return 'required'
case 'shader': case 'shader':
return 'required' return 'required'
case 'datapack':
return 'optional'
default: default:
return 'unknown' return 'unknown'
} }
@@ -135,6 +137,8 @@ export default {
return 'unsupported' return 'unsupported'
case 'shader': case 'shader':
return 'unsupported' return 'unsupported'
case 'datapack':
return 'required'
default: default:
return 'unknown' return 'unknown'
} }

View File

@@ -50,7 +50,15 @@
<p class="description"> <p class="description">
{{ description }} {{ description }}
</p> </p>
<Categories :categories="categories" :type="type" class="tags"> <Categories
:categories="
categories.filter(
(x) => !hideLoaders || !$tag.loaders.find((y) => y.name === x)
)
"
:type="type"
class="tags"
>
<span v-if="moderation" class="environment"> <span v-if="moderation" class="environment">
<InfoIcon aria-hidden="true" /> <InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }} A {{ projectTypeDisplay }}
@@ -58,7 +66,8 @@
<span <span
v-else-if=" v-else-if="
!['resourcepack', 'shader'].includes(type) && !['resourcepack', 'shader'].includes(type) &&
!(projectTypeDisplay === 'plugin' && search) !(projectTypeDisplay === 'plugin' && search) &&
!categories.some((x) => $tag.loaderData.dataPackLoaders.includes(x))
" "
class="environment" class="environment"
> >
@@ -261,6 +270,11 @@ export default {
required: false, required: false,
default: true, default: true,
}, },
hideLoaders: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
projectTypeDisplay() { projectTypeDisplay() {

View File

@@ -31,8 +31,7 @@ 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'
) )
}, },
}, },

View File

@@ -32,12 +32,16 @@
label: 'Plugins', label: 'Plugins',
href: '/plugins', href: '/plugins',
}, },
{
label: 'Data Packs',
href: '/datapacks',
},
{ {
label: 'Shaders', label: 'Shaders',
href: '/shaders', href: '/shaders',
}, },
{ {
label: 'Resourcepacks', label: 'Resource Packs',
href: '/resourcepacks', href: '/resourcepacks',
}, },
{ {
@@ -201,6 +205,14 @@
> >
<span>Plugins</span> <span>Plugins</span>
</NuxtLink> </NuxtLink>
<NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1"
to="/datapacks"
class="tab iconified-button"
@click.native="isBrowseMenuOpen = false"
>
<span>Data packs</span>
</NuxtLink>
<NuxtLink <NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1" :tabindex="isBrowseMenuOpen ? 0 : -1"
to="/shaders" to="/shaders"
@@ -215,7 +227,7 @@
class="tab iconified-button" class="tab iconified-button"
@click.native="isBrowseMenuOpen = false" @click.native="isBrowseMenuOpen = false"
> >
<span>Resourcepacks</span> <span>Resource packs</span>
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink
:tabindex="isBrowseMenuOpen ? 0 : -1" :tabindex="isBrowseMenuOpen ? 0 : -1"
@@ -487,12 +499,13 @@ export default {
watch: { watch: {
$route() { $route() {
this.isMobileMenuOpen = false this.isMobileMenuOpen = false
document.body.style.overflowY = 'scroll'
this.$store.dispatch('user/fetchAll') this.$store.dispatch('user/fetchAll')
document.body.setAttribute('tabindex', '-1') if (process.client) {
document.body.removeAttribute('tabindex') document.body.style.overflowY = 'scroll'
document.body.setAttribute('tabindex', '-1')
document.body.removeAttribute('tabindex')
}
}, },
}, },
beforeCreate() { beforeCreate() {
@@ -848,7 +861,7 @@ export default {
} }
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 1024px) {
display: none; display: none;
} }
} }
@@ -963,7 +976,7 @@ export default {
} }
} }
@media screen and (max-width: 750px) { @media screen and (max-width: 1024px) {
display: flex; display: flex;
} }

View File

@@ -32,7 +32,7 @@ export default {
hid: 'description', hid: 'description',
name: 'description', name: 'description',
content: content:
'Download Minecraft mods, plugins, resource packs, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.', 'Download Minecraft mods, plugins, datapacks, shaders, resourcepacks, and modpacks on Modrinth. Discover and publish projects on Modrinth with a modern, easy to use interface and API.',
}, },
{ {
hid: 'publisher', hid: 'publisher',
@@ -170,6 +170,11 @@ export default {
component: resolve(__dirname, 'pages/search/shaders.vue'), component: resolve(__dirname, 'pages/search/shaders.vue'),
name: 'shaders', name: 'shaders',
}, },
{
path: '/datapacks',
component: resolve(__dirname, 'pages/search/datapacks.vue'),
name: 'datapacks',
},
], ],
}) })

View File

@@ -45,7 +45,9 @@
<div <div
v-if=" v-if="
project.project_type !== 'resourcepack' && project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' project.project_type !== 'plugin' &&
project.project_type !== 'shader' &&
project.project_type !== 'datapack'
" "
> >
<div <div
@@ -502,7 +504,8 @@
v-if=" v-if="
project.project_type !== 'resourcepack' && project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' && project.project_type !== 'plugin' &&
project.project_type !== 'shader' project.project_type !== 'shader' &&
project.project_type !== 'datapack'
" "
class="info" class="info"
> >
@@ -515,7 +518,8 @@
v-if=" v-if="
project.project_type !== 'resourcepack' && project.project_type !== 'resourcepack' &&
project.project_type !== 'plugin' && project.project_type !== 'plugin' &&
project.project_type !== 'shader' project.project_type !== 'shader' &&
project.project_type !== 'datapack'
" "
class="info" class="info"
> >
@@ -843,10 +847,9 @@ export default {
this.featuredVersions = this.$computeVersions(this.featuredVersions) this.featuredVersions = this.$computeVersions(this.featuredVersions)
}, },
head() { head() {
const title = `${this.project.title} - Minecraft ${ const title = `${this.project.title} - Minecraft ${this.$formatProjectType(
this.projectTypeDisplay.charAt(0).toUpperCase() + this.projectTypeDisplay
this.projectTypeDisplay.slice(1) )}`
}`
return { return {
title, title,
@@ -869,9 +872,11 @@ export default {
{ {
hid: 'description', hid: 'description',
name: 'description', name: 'description',
content: `${this.project.description} - Download the Minecraft ${ content: `${
this.project.description
} - Download the Minecraft ${this.$formatProjectType(
this.projectTypeDisplay this.projectTypeDisplay
} ${this.project.title} by ${ )} ${this.project.title} by ${
this.members.find((x) => x.role === 'Owner').user.username this.members.find((x) => x.role === 'Owner').user.username
} on Modrinth`, } on Modrinth`,
}, },

View File

@@ -209,7 +209,8 @@
<section <section
v-if=" v-if="
project.project_type !== 'resourcepack' && project.project_type !== 'resourcepack' &&
project.project_type !== 'shader' project.project_type !== 'shader' &&
project.project_type !== 'datapack'
" "
class="card game-sides" class="card game-sides"
> >
@@ -833,6 +834,9 @@ export default {
this.newProject.client_side = this.clientSideType.toLowerCase() this.newProject.client_side = this.clientSideType.toLowerCase()
this.newProject.server_side = this.serverSideType.toLowerCase() this.newProject.server_side = this.serverSideType.toLowerCase()
this.newProject.client_side = this.clientSideType.toLowerCase()
this.newProject.server_side = this.serverSideType.toLowerCase()
this.$emit('update:project', this.newProject) this.$emit('update:project', this.newProject)
this.isEditing = false this.isEditing = false

View File

@@ -371,7 +371,10 @@ export default {
} }
} }
document.addEventListener('keydown', this._keyListener.bind(this)) // eslint-disable-next-line nuxt/no-env-in-hooks
if (process.client) {
document.addEventListener('keydown', this._keyListener.bind(this))
}
}, },
methods: { methods: {
showPreviewImage(files, index) { showPreviewImage(files, index) {

View File

@@ -1,6 +1,7 @@
<template> <template>
<div v-if="version" class="version-page"> <div v-if="version" class="version-page">
<ModalConfirm <ModalConfirm
v-if="$auth.user && currentMember"
ref="modal_confirm" ref="modal_confirm"
title="Are you sure you want to delete this version?" title="Are you sure you want to delete this version?"
description="This will remove this version forever (like really forever)." description="This will remove this version forever (like really forever)."
@@ -14,6 +15,58 @@
:item-id="version.id" :item-id="version.id"
item-type="version" item-type="version"
/> />
<Modal
v-if="$auth.user && currentMember"
ref="modal_package_mod"
header="Package data pack"
>
<div class="modal-package-mod universal-labels">
<div class="markdown-body">
<p>
Package your data pack as a mod. This will create a new version with
support for the selected mod loaders. You will be redirected to the
new version and can edit it to your liking.
</p>
</div>
<label for="package-mod-loaders">
<span class="label__title">Mod loaders</span>
<span class="label__description">
The mod loaders you would like to package your data pack for.
</span>
</label>
<multiselect
id="package-mod-loaders"
v-model="packageLoaders"
:options="['fabric', 'forge', 'quilt']"
:custom-label="
(value) => value.charAt(0).toUpperCase() + value.slice(1)
"
:multiple="true"
:searchable="false"
:show-no-results="false"
:show-labels="false"
placeholder="Choose loaders.."
open-direction="top"
/>
<div class="button-group">
<button
class="iconified-button"
@click="$refs.modal_package_mod.hide()"
>
<CrossIcon />
Cancel
</button>
<button
class="iconified-button brand-button"
:disabled="!$nuxt.$loading"
@click="createDataPackVersion"
>
<RightArrowIcon />
Begin packaging data pack
</button>
</div>
</div>
</Modal>
<div class="version-page__title universal-card"> <div class="version-page__title universal-card">
<div class="version-header"> <div class="version-header">
<template v-if="isEditing"> <template v-if="isEditing">
@@ -143,7 +196,7 @@
Back to list Back to list
</nuxt-link> </nuxt-link>
<button <button
v-if="$auth.user" v-if="$auth.user && !currentMember"
class="iconified-button" class="iconified-button"
@click="$refs.modal_version_report.show()" @click="$refs.modal_version_report.show()"
> >
@@ -164,6 +217,19 @@
<EditIcon aria-hidden="true" /> <EditIcon aria-hidden="true" />
Edit Edit
</nuxt-link> </nuxt-link>
<button
v-if="
currentMember &&
version.loaders.some((x) =>
$tag.loaderData.dataPackLoaders.includes(x)
)
"
class="iconified-button"
@click="$refs.modal_package_mod.show()"
>
<BoxIcon aria-hidden="true" />
Package as mod
</button>
<button <button
v-if="currentMember" v-if="currentMember"
class="iconified-button danger-button" class="iconified-button danger-button"
@@ -222,7 +288,10 @@
></div> ></div>
</div> </div>
<div <div
v-if="version.dependencies.length > 0 || isEditing" v-if="
version.dependencies.length > 0 ||
(isEditing && project.project_type !== 'modpack')
"
class="version-page__dependencies universal-card" class="version-page__dependencies universal-card"
> >
<h3>Dependencies</h3> <h3>Dependencies</h3>
@@ -269,7 +338,7 @@
</span> </span>
</div> </div>
<button <button
v-if="isEditing" v-if="isEditing && project.project_type !== 'modpack'"
class="iconified-button" class="iconified-button"
@click="version.dependencies.splice(index, 1)" @click="version.dependencies.splice(index, 1)"
> >
@@ -308,7 +377,7 @@
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
:show-labels="false" :show-labels="false"
:allow-empty="false" :allow-empty="true"
/> />
<input <input
v-model="newDependencyId" v-model="newDependencyId"
@@ -334,7 +403,7 @@
:searchable="false" :searchable="false"
:close-on-select="true" :close-on-select="true"
:show-labels="false" :show-labels="false"
:allow-empty="false" :allow-empty="true"
/> />
</div> </div>
<div class="input-group"> <div class="input-group">
@@ -385,7 +454,48 @@
<span class="filename"> <span class="filename">
<strong>{{ file.filename }}</strong> <strong>{{ file.filename }}</strong>
<span class="file-size">({{ $formatBytes(file.size) }})</span> <span class="file-size">({{ $formatBytes(file.size) }})</span>
<span
v-if="primaryFile.hashes.sha1 === file.hashes.sha1"
class="file-type"
>
Primary
</span>
<span
v-else-if="
file.file_type === 'required-resource-pack' && !isEditing
"
class="file-type"
>
Required resource pack
</span>
<span
v-else-if="
file.file_type === 'optional-resource-pack' && !isEditing
"
class="file-type"
>
Optional resource pack
</span>
</span> </span>
<multiselect
v-if="
version.loaders.some((x) =>
$tag.loaderData.dataPackLoaders.includes(x)
) &&
isEditing &&
primaryFile.hashes.sha1 !== file.hashes.sha1
"
v-model="oldFileTypes[index]"
class="raised-multiselect"
placeholder="Select file type"
:options="fileTypes"
track-by="value"
label="display"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
<FileInput <FileInput
v-if="isEditing && primaryFile.hashes.sha1 === file.hashes.sha1" v-if="isEditing && primaryFile.hashes.sha1 === file.hashes.sha1"
class="iconified-button raised-button" class="iconified-button raised-button"
@@ -397,6 +507,8 @@
(x) => { (x) => {
deleteFiles.push(file.hashes.sha1) deleteFiles.push(file.hashes.sha1)
version.files.splice(index, 1) version.files.splice(index, 1)
oldFileTypes.splice(index, 1)
replaceFile = x[0] replaceFile = x[0]
} }
" "
@@ -409,6 +521,7 @@
@click=" @click="
deleteFiles.push(file.hashes.sha1) deleteFiles.push(file.hashes.sha1)
version.files.splice(index, 1) version.files.splice(index, 1)
oldFileTypes.splice(index, 1)
" "
> >
<TrashIcon /> <TrashIcon />
@@ -432,9 +545,29 @@
<strong>{{ file.name }}</strong> <strong>{{ file.name }}</strong>
<span class="file-size">({{ $formatBytes(file.size) }})</span> <span class="file-size">({{ $formatBytes(file.size) }})</span>
</span> </span>
<multiselect
v-if="
version.loaders.some((x) =>
$tag.loaderData.dataPackLoaders.includes(x)
)
"
v-model="newFileTypes[index]"
class="raised-multiselect"
placeholder="Select file type"
:options="fileTypes"
track-by="value"
label="display"
:searchable="false"
:close-on-select="true"
:show-labels="false"
:allow-empty="false"
/>
<button <button
class="iconified-button raised-button" class="iconified-button raised-button"
@click="newFiles.splice(index, 1)" @click="
newFiles.splice(index, 1)
newFileTypes.splice(index, 1)
"
> >
<TrashIcon /> <TrashIcon />
Remove Remove
@@ -442,14 +575,29 @@
</div> </div>
<div class="additional-files"> <div class="additional-files">
<h4>Upload additional files</h4> <h4>Upload additional files</h4>
<span>Used for files such as sources or Javadocs.</span> <span
v-if="
version.loaders.some((x) =>
$tag.loaderData.dataPackLoaders.includes(x)
)
"
>
Used for additional files such as required/optional resource packs
</span>
<span v-else>Used for files such as sources or Javadocs.</span>
<FileInput <FileInput
prompt="Drag and drop to upload or click to select" prompt="Drag and drop to upload or click to select"
multiple multiple
long-style long-style
:accept="acceptFileFromProjectType(project.project_type)" :accept="acceptFileFromProjectType(project.project_type)"
:max-size="524288000" :max-size="524288000"
@change="(x) => x.forEach((y) => newFiles.push(y))" @change="
(x) =>
x.forEach((y) => {
newFiles.push(y)
newFileTypes.push(null)
})
"
> >
<UploadIcon /> <UploadIcon />
</FileInput> </FileInput>
@@ -629,6 +777,7 @@ import Multiselect from 'vue-multiselect'
import { import {
acceptFileFromProjectType, acceptFileFromProjectType,
inferVersionInfo, inferVersionInfo,
createDataPackVersion,
} from '~/plugins/fileUtils' } from '~/plugins/fileUtils'
import VersionBadge from '~/components/ui/Badge' import VersionBadge from '~/components/ui/Badge'
@@ -654,9 +803,13 @@ import PlusIcon from '~/assets/images/utils/plus.svg?inline'
import TransferIcon from '~/assets/images/utils/transfer.svg?inline' import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
import UploadIcon from '~/assets/images/utils/upload.svg?inline' import UploadIcon from '~/assets/images/utils/upload.svg?inline'
import BackIcon from '~/assets/images/utils/left-arrow.svg?inline' import BackIcon from '~/assets/images/utils/left-arrow.svg?inline'
import BoxIcon from '~/assets/images/utils/box.svg?inline'
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
import Modal from '~/components/ui/Modal.vue'
export default { export default {
components: { components: {
Modal,
FileInput, FileInput,
Checkbox, Checkbox,
Chips, Chips,
@@ -680,6 +833,8 @@ export default {
ModalConfirm, ModalConfirm,
ModalReport, ModalReport,
Multiselect, Multiselect,
BoxIcon,
RightArrowIcon,
}, },
props: { props: {
project: { project: {
@@ -722,6 +877,7 @@ export default {
data() { data() {
return { return {
primaryFile: {}, primaryFile: {},
alternateFile: {},
version: {}, version: {},
isEditing: false, isEditing: false,
@@ -738,6 +894,21 @@ export default {
deleteFiles: [], deleteFiles: [],
replaceFile: null, replaceFile: null,
newFileTypes: [],
oldFileTypes: [],
fileTypes: [
{
display: 'Required resource pack',
value: 'required-resource-pack',
},
{
display: 'Optional resource pack',
value: 'optional-resource-pack',
},
],
packageLoaders: ['forge', 'fabric', 'quilt'],
showKnownErrors: false, showKnownErrors: false,
} }
}, },
@@ -814,6 +985,7 @@ export default {
acceptFileFromProjectType, acceptFileFromProjectType,
reset() { reset() {
this.primaryFile = {} this.primaryFile = {}
this.alternateFile = {}
this.version = {} this.version = {}
this.changelogViewMode = 'source' this.changelogViewMode = 'source'
@@ -827,10 +999,14 @@ export default {
this.newFiles = [] this.newFiles = []
this.deleteFiles = [] this.deleteFiles = []
this.replaceFile = null this.replaceFile = null
this.oldFileTypes = []
this.newFileTypes = []
this.isEditing = false this.isEditing = false
this.isCreating = false this.isCreating = false
this.packageLoaders = ['forge', 'fabric', 'quilt']
this.showKnownErrors = false this.showKnownErrors = false
}, },
async setVersion() { async setVersion() {
@@ -916,6 +1092,9 @@ export default {
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]
this.alternateFile = this.version.files.find(
(file) => file.file_type && file.file_type.includes('resource-pack')
)
this.version.author_member = this.members.find( this.version.author_member = this.members.find(
(x) => x.user.id === this.version.author_id (x) => x.user.id === this.version.author_id
@@ -948,6 +1127,10 @@ export default {
}` }`
: '' : ''
} }
this.oldFileTypes = this.version.files.map((x) =>
this.fileTypes.find((y) => y.value === x.file_type)
)
}, },
async addDependency( async addDependency(
dependencyAddMode, dependencyAddMode,
@@ -1037,12 +1220,24 @@ export default {
if (this.newFiles.length > 0 || this.replaceFile) { if (this.newFiles.length > 0 || this.replaceFile) {
const formData = new FormData() const formData = new FormData()
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
formData.append('data', JSON.stringify({})) formData.append(
'data',
JSON.stringify({
file_types: this.newFileTypes.reduce(
(acc, x, i) => ({
...acc,
[fileParts[i]]: x ? x.value : null,
}),
{}
),
})
)
for (let i = 0; i < this.newFiles.length; i++) { for (let i = 0; i < this.newFiles.length; i++) {
formData.append( formData.append(
this.newFiles[i].name.concat('-' + i), fileParts[i],
new Blob([this.newFiles[i]]), new Blob([this.newFiles[i]]),
this.newFiles[i].name this.newFiles[i].name
) )
@@ -1079,6 +1274,13 @@ export default {
loaders: this.version.loaders, loaders: this.version.loaders,
primary_file: ['sha1', this.primaryFile.hashes.sha1], primary_file: ['sha1', this.primaryFile.hashes.sha1],
featured: this.version.featured, featured: this.version.featured,
file_types: this.oldFileTypes.map((x, i) => {
return {
algorithm: 'sha1',
hash: this.version.files[i].hashes.sha1,
file_type: x ? x.value : null,
}
}),
}, },
this.$defaultHeaders() this.$defaultHeaders()
) )
@@ -1090,7 +1292,7 @@ export default {
) )
} }
const [versions, featuredVersions] = ( const [versions, featuredVersions, dependencies] = (
await Promise.all([ await Promise.all([
this.$axios.get( this.$axios.get(
`project/${this.version.project_id}/version`, `project/${this.version.project_id}/version`,
@@ -1100,12 +1302,17 @@ export default {
`project/${this.version.project_id}/version?featured=true`, `project/${this.version.project_id}/version?featured=true`,
this.$defaultHeaders() this.$defaultHeaders()
), ),
this.$axios.get(
`project/${this.version.project_id}/dependencies`,
this.$defaultHeaders()
),
]) ])
).map((it) => it.data) ).map((it) => it.data)
const newEditedVersions = this.$computeVersions(versions) const newEditedVersions = this.$computeVersions(versions)
this.$emit('update:versions', newEditedVersions) this.$emit('update:versions', newEditedVersions)
this.$emit('update:featuredVersions', featuredVersions) this.$emit('update:featuredVersions', featuredVersions)
this.$emit('update:dependencies', dependencies)
await this.$router.replace( await this.$router.replace(
`/${this.project.project_type}/${ `/${this.project.project_type}/${
@@ -1128,7 +1335,6 @@ export default {
}, },
async createVersion() { async createVersion() {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
if (this.fieldErrors) { if (this.fieldErrors) {
this.showKnownErrors = true this.showKnownErrors = true
@@ -1136,6 +1342,21 @@ export default {
return return
} }
try {
await this.createVersionRaw(this.version)
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}
this.$nuxt.$loading.finish()
},
async createVersionRaw(version) {
const formData = new FormData() const formData = new FormData()
const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`) const fileParts = this.newFiles.map((f, idx) => `${f.name}-${idx}`)
@@ -1144,20 +1365,27 @@ export default {
} }
if (this.project.project_type === 'resourcepack') { if (this.project.project_type === 'resourcepack') {
this.version.loaders = ['minecraft'] version.loaders = ['minecraft']
} }
const newVersion = { const newVersion = {
project_id: this.version.project_id, project_id: version.project_id,
file_parts: fileParts, file_parts: fileParts,
version_number: this.version.version_number, version_number: version.version_number,
version_title: this.version.name || this.version.version_number, version_title: version.name || version.version_number,
version_body: this.version.changelog, version_body: version.changelog,
dependencies: this.version.dependencies, dependencies: version.dependencies,
game_versions: this.version.game_versions, game_versions: version.game_versions,
loaders: this.version.loaders, loaders: version.loaders,
release_channel: this.version.version_type, release_channel: version.version_type,
featured: this.version.featured, featured: version.featured,
file_types: this.newFileTypes.reduce(
(acc, x, i) => ({
...acc,
[fileParts[this.replaceFile ? i + 1 : i]]: x ? x.value : null,
}),
{}
),
} }
formData.append('data', JSON.stringify(newVersion)) formData.append('data', JSON.stringify(newVersion))
@@ -1172,58 +1400,51 @@ export default {
for (let i = 0; i < this.newFiles.length; i++) { for (let i = 0; i < this.newFiles.length; i++) {
formData.append( formData.append(
fileParts[i], fileParts[this.replaceFile ? i + 1 : i],
new Blob([this.newFiles[i]]), new Blob([this.newFiles[i]]),
this.newFiles[i].name this.newFiles[i].name
) )
} }
try { const data = (
const data = ( await this.$axios({
await this.$axios({ url: 'version',
url: 'version', method: 'POST',
method: 'POST', data: formData,
data: formData, headers: {
headers: { 'Content-Type': 'multipart/form-data',
'Content-Type': 'multipart/form-data', Authorization: this.$auth.token,
Authorization: this.$auth.token, },
},
})
).data
const [versions, featuredVersions] = (
await Promise.all([
this.$axios.get(
`project/${this.version.project_id}/version`,
this.$defaultHeaders()
),
this.$axios.get(
`project/${this.version.project_id}/version?featured=true`,
this.$defaultHeaders()
),
])
).map((it) => it.data)
const newCreatedVersions = this.$computeVersions(versions)
this.$emit('update:versions', newCreatedVersions)
this.$emit('update:featuredVersions', featuredVersions)
await this.$router.push(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.project_id
}/version/${data.id}`
)
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response.data.description,
type: 'error',
}) })
window.scrollTo({ top: 0, behavior: 'smooth' }) ).data
}
this.$nuxt.$loading.finish() const [versions, featuredVersions, dependencies] = (
await Promise.all([
this.$axios.get(
`project/${this.version.project_id}/version`,
this.$defaultHeaders()
),
this.$axios.get(
`project/${this.version.project_id}/version?featured=true`,
this.$defaultHeaders()
),
this.$axios.get(
`project/${this.version.project_id}/dependencies`,
this.$defaultHeaders()
),
])
).map((it) => it.data)
const newCreatedVersions = this.$computeVersions(versions)
this.$emit('update:versions', newCreatedVersions)
this.$emit('update:featuredVersions', featuredVersions)
this.$emit('update:dependencies', dependencies)
await this.$router.push(
`/${this.project.project_type}/${
this.project.slug ? this.project.slug : this.project.project_id
}/version/${data.id}`
)
}, },
async deleteVersion() { async deleteVersion() {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
@@ -1238,6 +1459,56 @@ export default {
) )
this.$nuxt.$loading.finish() this.$nuxt.$loading.finish()
}, },
async createDataPackVersion() {
this.$nuxt.$loading.start()
try {
const blob = await createDataPackVersion(
this.project,
this.version,
this.primaryFile,
this.members,
this.$tag.gameVersions,
this.packageLoaders
)
this.newFiles = []
this.newFileTypes = []
this.replaceFile = new File(
[blob],
`${this.project.slug}-${this.version.version_number}.jar`
)
await this.createVersionRaw({
project_id: this.project.id,
author_id: this.currentMember.user.id,
name: this.version.name,
version_number: `${this.version.version_number}+mod`,
changelog: this.version.changelog,
version_type: this.version.version_type,
dependencies: this.version.dependencies,
game_versions: this.version.game_versions,
loaders: this.packageLoaders,
featured: this.version.featured,
})
this.$refs.modal_package_mod.hide()
this.$notify({
group: 'main',
title: 'Packaging Success',
text: 'Your data pack was successfully packaged as a mod! Make sure to playtest to check for errors.',
type: 'success',
})
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
},
}, },
} }
</script> </script>
@@ -1383,8 +1654,7 @@ export default {
} }
.filename { .filename {
word-wrap: break-word; word-wrap: anywhere;
overflow-wrap: anywhere;
} }
.file-size { .file-size {
@@ -1392,6 +1662,19 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
.file-type {
font-style: italic;
font-weight: 300;
}
.raised-multiselect {
display: none;
margin: 0 0.5rem;
height: 40px;
max-height: 40px;
min-width: 235px;
}
.iconified-button { .iconified-button {
margin-left: auto; margin-left: auto;
} }
@@ -1399,6 +1682,13 @@ export default {
&:not(:nth-child(2)) { &:not(:nth-child(2)) {
margin-top: 0.5rem; margin-top: 0.5rem;
} }
// TODO: Make file type editing work on mobile
@media (min-width: 600px) {
.raised-multiselect {
display: block;
}
}
} }
.additional-files { .additional-files {
@@ -1454,4 +1744,23 @@ export default {
.separator { .separator {
margin: var(--spacing-card-sm) 0; margin: var(--spacing-card-sm) 0;
} }
.modal-package-mod {
padding: var(--spacing-card-bg);
display: flex;
flex-direction: column;
.markdown-body {
margin-bottom: 1rem;
}
.multiselect {
max-width: 20rem;
}
.button-group {
margin-left: auto;
margin-top: 1.5rem;
}
}
</style> </style>

View File

@@ -1,98 +0,0 @@
<template>
<div class="main">
<div class="card">
<h1>About</h1>
<p>
Founded in 2020, Modrinth was created to provide modders with an open
and intuitive platform to publish their projects on.
</p>
<p>
Our primary goal is to be as open as possible, with all our code being
<a :target="$external()" href="https://github.com/modrinth">
open source</a
>, while giving back to the modding community as much as possible.
</p>
<p>
While we still are in beta, we hope we can be a major modding platform
for all modders.
</p>
<h2>How does Modrinth work?</h2>
<p>
Modrinth is not just a website: it is made of several different
components that all come together to make a fast and flexible modding
platform.
</p>
<p>
On the technical level, Modrinth is made up of two main components: the
Rust-based backend named
<a :target="$external()" href="https://github.com/modrinth/labrinth"
>Labrinth</a
>, and the Vue-based frontend named
<a :target="$external()" href="https://github.com/modrinth/knossos">
Knossos</a
>.
</p>
<p>
Additionally, some other custom-created resources exist, including but
not limited to:
<a :target="$external()" href="https://github.com/modrinth/minotaur"
>Minotaur</a
>, a Gradle plugin for easily publishing mods to Modrinth, and
<a :target="$external()" href="https://github.com/modrinth/minos">
Minos</a
>, an authentication provider. All of Modrinth's code can be found on
<a :target="$external()" href="https://github.com/modrinth"
>our GitHub page</a
>.
</p>
<h2>Backend Documentation</h2>
<p>
Documentation for the Modrinth API (Labrinth) can be found on the GitHub
repository's wiki
<a :target="$external()" href="https://docs.modrinth.com">here</a>.
</p>
</div>
</div>
</template>
<script>
export default {
auth: false,
head: {
title: 'About - Modrinth',
meta: [
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content: 'About',
},
{
hid: 'og:title',
name: 'og:title',
content: 'About',
},
{
hid: 'og:url',
name: 'og:url',
content: `https://modrinth.com/about`,
},
],
},
}
</script>
<style lang="scss" scoped>
.main {
margin: var(--spacing-card-sm) auto;
max-width: 800px;
}
a {
text-decoration: underline;
color: var(--color-link);
}
</style>

View File

@@ -126,7 +126,7 @@
<div class="title"> <div class="title">
<h3> <h3>
{{ item.item_type }} {{ item.item_type }}
<a :href="item.url">{{ item.item_id }}</a> <nuxt-link :to="item.url">{{ item.item_id }}</nuxt-link>
</h3> </h3>
reported by reported by
<a :href="`/user/${item.reporter}`">{{ item.reporter }}</a> <a :href="`/user/${item.reporter}`">{{ item.reporter }}</a>

View File

@@ -78,7 +78,9 @@
</div> </div>
</section> </section>
<section <section
v-if="projectType.id !== 'resourcepack'" v-if="
projectType.id !== 'resourcepack' && projectType.id !== 'datapack'
"
aria-label="Loader filters" aria-label="Loader filters"
> >
<h3 <h3
@@ -101,15 +103,15 @@
x.name !== 'quilt' x.name !== 'quilt'
) { ) {
return false return false
} } else if (projectType.id === 'mod' && showAllLoaders) {
if (projectType.id === 'mod' && showAllLoaders) {
return $tag.loaderData.modLoaders.includes(x.name) return $tag.loaderData.modLoaders.includes(x.name)
} else if (projectType.id === 'plugin') {
return $tag.loaderData.pluginLoaders.includes(x.name)
} else if (projectType.id === 'datapack') {
return $tag.loaderData.dataPackLoaders.includes(x.name)
} else {
return x.supported_project_types.includes(projectType.actual)
} }
return projectType.id === 'plugin'
? $tag.loaderData.pluginLoaders.includes(x.name)
: x.supported_project_types.includes(projectType.actual)
})" })"
:key="loader.name" :key="loader.name"
ref="loaderFilters" ref="loaderFilters"
@@ -158,7 +160,9 @@
</section> </section>
<section <section
v-if=" v-if="
!['resourcepack', 'plugin', 'shader'].includes(projectType.id) !['resourcepack', 'plugin', 'shader', 'datapack'].includes(
projectType.id
)
" "
aria-label="Environment filters" aria-label="Environment filters"
> >
@@ -376,6 +380,9 @@
:categories="result.display_categories" :categories="result.display_categories"
:search="true" :search="true"
:show-updated-date="sortType.name !== 'newest'" :show-updated-date="sortType.name !== 'newest'"
:hide-loaders="
['resourcepack', 'datapack'].includes(projectType.id)
"
/> />
<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>
@@ -714,6 +721,12 @@ export default {
(x) => `categories:'${encodeURIComponent(x)}'` (x) => `categories:'${encodeURIComponent(x)}'`
) )
) )
} else if (this.projectType.id === 'datapack') {
formattedFacets.push(
this.$tag.loaderData.dataPackLoaders.map(
(x) => `categories:'${encodeURIComponent(x)}'`
)
)
} }
if (this.selectedVersions.length > 0) { if (this.selectedVersions.length > 0) {

View File

@@ -0,0 +1,11 @@
<template>
<div></div>
</template>
<script>
export default {
name: 'Datapacks',
}
</script>
<style lang="scss" scoped></style>

View File

@@ -161,6 +161,7 @@ export default {
resourcepack: 'gallery', resourcepack: 'gallery',
modpack: 'list', modpack: 'list',
shader: 'gallery', shader: 'gallery',
datapack: 'list',
user: 'list', user: 'list',
}, },
} }

View File

@@ -160,7 +160,7 @@
}, },
...projectTypes.map((x) => { ...projectTypes.map((x) => {
return { return {
label: x === 'resourcepack' ? 'Resource Packs' : x + 's', label: $formatProjectType(x) + 's',
href: x, href: x,
} }
}), }),
@@ -326,13 +326,47 @@ export default {
} }
let gitHubUser = {} let gitHubUser = {}
let versions = []
try { try {
gitHubUser = ( const [gitHubUserData, versionsData] = (
await data.$axios.get(`https://api.github.com/user/` + user.github_id) await Promise.all([
).data data.$axios.get(`https://api.github.com/user/` + user.github_id),
data.$axios.get(
`versions?ids=${JSON.stringify(
[].concat.apply(
[],
projects.map((x) => x.versions)
)
)}`
),
])
).map((it) => it.data)
gitHubUser = gitHubUserData
versions = versionsData
} catch {} } catch {}
for (const version of versions) {
const projectIndex = projects.findIndex(
(x) => x.id === version.project_id
)
if (projects[projectIndex].loaders) {
for (const loader of version.loaders) {
if (!projects[projectIndex].loaders.includes(loader)) {
projects[projectIndex].loaders.push(loader)
}
}
} else {
projects[projectIndex].loaders = version.loaders
}
}
for (const project of projects) {
project.categories = project.categories.concat(project.loaders)
project.project_type = data.$getProjectTypeForUrl(
project.project_type,
project.categories
)
}
return { return {
user, user,
projects, projects,

View File

@@ -15,7 +15,6 @@ import { formatBytes } from '~/plugins/shorthands'
export const fileIsValid = (file, validationOptions) => { export const fileIsValid = (file, validationOptions) => {
const { maxSize, alertOnInvalid } = validationOptions const { maxSize, alertOnInvalid } = validationOptions
if (maxSize !== null && maxSize !== undefined && file.size > maxSize) { if (maxSize !== null && maxSize !== undefined && file.size > maxSize) {
console.log(`File size: ${file.size}, max size: ${maxSize}`)
if (alertOnInvalid) { if (alertOnInvalid) {
alert( alert(
`File ${file.name} is too big! Must be less than ${formatBytes( `File ${file.name} is too big! Must be less than ${formatBytes(
@@ -39,6 +38,8 @@ export const acceptFileFromProjectType = (projectType) => {
return '.zip,application/zip' return '.zip,application/zip'
case 'shader': case 'shader':
return '.zip,application/zip' return '.zip,application/zip'
case 'datapack':
return '.zip,application/zip'
case 'modpack': case 'modpack':
return '.mrpack,application/x-modrinth-modpack+zip' return '.mrpack,application/x-modrinth-modpack+zip'
default: default:
@@ -108,7 +109,7 @@ export const inferVersionInfo = async function (
'META-INF/mods.toml': (file, zip) => { 'META-INF/mods.toml': (file, zip) => {
const metadata = TOML.parse(file) const metadata = TOML.parse(file)
console.log(JSON.stringify(metadata)) // ${file.jarVersion} -> Implementation-Version from manifest
// TODO: Parse minecraft version ranges, handle if version is set to value from manifest // TODO: Parse minecraft version ranges, handle if version is set to value from manifest
if (metadata.mods && metadata.mods.length > 0) { if (metadata.mods && metadata.mods.length > 0) {
@@ -165,8 +166,10 @@ export const inferVersionInfo = async function (
version_type: versionType(metadata.quilt_loader.version), version_type: versionType(metadata.quilt_loader.version),
game_versions: metadata.quilt_loader.depends game_versions: metadata.quilt_loader.depends
? gameVersionRange( ? gameVersionRange(
metadata.depends.find((x) => x.id === 'minecraft') metadata.quilt_loader.depends.find((x) => x.id === 'minecraft')
? metadata.depends.find((x) => x.id === 'minecraft').versions ? metadata.quilt_loader.depends.find(
(x) => x.id === 'minecraft'
).versions
: [], : [],
gameVersions gameVersions
) )
@@ -222,6 +225,112 @@ export const inferVersionInfo = async function (
.map((x) => x.version), .map((x) => x.version),
} }
}, },
// Resource Packs + Data Packs
'pack.mcmeta': (file) => {
const metadata = JSON.parse(file)
function getRange(versionA, versionB) {
const startingIndex = gameVersions.findIndex(
(x) => x.version === versionA
)
const endingIndex = gameVersions.findIndex(
(x) => x.version === versionB
)
const final = []
const filterOnlyRelease =
gameVersions[startingIndex].version_type === 'release'
for (let i = startingIndex; i >= endingIndex; i--) {
if (
gameVersions[i].version_type === 'release' ||
!filterOnlyRelease
) {
final.push(gameVersions[i].version)
}
}
return final
}
const loaders = []
let newGameVersions = []
if (project.actualProjectType === 'mod') {
loaders.push('datapack')
switch (metadata.pack.pack_format) {
case 4:
newGameVersions = getRange('1.13', '1.14.4')
break
case 5:
newGameVersions = getRange('1.15', '1.16.1')
break
case 6:
newGameVersions = getRange('1.16.2', '1.16.5')
break
case 7:
newGameVersions = getRange('1.17', '1.17.1')
break
case 8:
newGameVersions = getRange('1.18', '1.18.1')
break
case 9:
newGameVersions.push('1.18.2')
break
case 10:
newGameVersions = getRange('1.19', '1.19.3')
break
default:
}
}
if (project.actualProjectType === 'resourcepack') {
loaders.push('minecraft')
switch (metadata.pack.pack_format) {
case 1:
newGameVersions = getRange('1.6.1', '1.8.9')
break
case 2:
newGameVersions = getRange('1.9', '1.10.2')
break
case 3:
newGameVersions = getRange('1.11', '1.12.2')
break
case 4:
newGameVersions = getRange('1.13', '1.14.4')
break
case 5:
newGameVersions = getRange('1.15', '1.16.1')
break
case 6:
newGameVersions = getRange('1.16.2', '1.16.5')
break
case 7:
newGameVersions = getRange('1.17', '1.17.1')
break
case 8:
newGameVersions = getRange('1.18', '1.18.2')
break
case 9:
newGameVersions = getRange('1.19', '1.19.2')
break
case 11:
newGameVersions = getRange('22w42a', '22w44a')
break
case 12:
newGameVersions.push('1.19.3')
break
default:
}
}
return {
loaders,
game_versions: newGameVersions,
}
},
} }
const zipReader = new JSZip() const zipReader = new JSZip()
@@ -237,3 +346,201 @@ export const inferVersionInfo = async function (
} }
} }
} }
export const createDataPackVersion = async function (
project,
version,
primaryFile,
members,
allGameVersions,
loaders
) {
// force version to start with number, as required by FML
const newVersionNumber = version.version_number.match(/^\d/)
? version.version_number
: `1-${version.version_number}`
const newSlug = `${project.slug
.replace('-', '_')
.replace(/\W/g, '')
.substring(0, 63)}_mr`
const iconPath = `${project.slug}_pack.png`
const fabricModJson = {
schemaVersion: 1,
id: newSlug,
version: newVersionNumber,
name: project.title,
description: project.description,
authors: members.map((x) => x.name),
contact: {
homepage: `${process.env.domain}/${project.project_type}/${
project.slug ?? project.id
}`,
},
license: project.license.id,
icon: iconPath,
environment: '*',
depends: {
'fabric-resource-loader-v0': '*',
},
}
const quiltModJson = {
schema_version: 1,
quilt_loader: {
group: 'com.modrinth',
id: newSlug,
version: newVersionNumber,
metadata: {
name: project.title,
description: project.description,
contributors: members.reduce(
(acc, x) => ({
...acc,
[x.name]: x.role,
}),
{}
),
contact: {
homepage: `${process.env.domain}/${project.project_type}/${
project.slug ?? project.id
}`,
},
icon: iconPath,
},
intermediate_mappings: 'net.fabricmc:intermediary',
depends: [
{
id: 'quilt_resource_loader',
versions: '*',
unless: 'fabric-resource-loader-v0',
},
],
},
}
const cutoffIndex = allGameVersions.findIndex((x) => x.version === '1.18.2')
let maximumIndex = Number.MIN_VALUE
for (const val of version.game_versions) {
const index = allGameVersions.findIndex((x) => x.version === val)
if (index > maximumIndex) {
maximumIndex = index
}
}
const newForge = maximumIndex < cutoffIndex
const forgeModsToml = {
modLoader: newForge ? 'lowcodefml' : 'javafml',
loaderVersion: newForge ? '[40,)' : '[25,)',
license: project.license.id,
showAsResourcePack: true,
mods: [
{
modId: newSlug,
version: newVersionNumber,
displayName: project.title,
description: project.description,
logoFile: iconPath,
updateJSONURL: `${process.env.authURLBase.replace(
'/v2/',
''
)}/updates/${project.id}/forge_updates.json`,
credits: 'Generated by Modrinth',
authors: members.map((x) => x.name).join(', '),
displayURL: `${process.env.domain}/${project.project_type}/${
project.slug ?? project.id
}`,
},
],
}
if (project.source_url) {
quiltModJson.quilt_loader.metadata.contact.sources = project.source_url
fabricModJson.contact.sources = project.source_url
}
if (project.issues_url) {
quiltModJson.quilt_loader.metadata.contact.issues = project.issues_url
fabricModJson.contact.issues = project.issues_url
forgeModsToml.issueTrackerURL = project.issues_url
}
const primaryFileData = await (await fetch(primaryFile.url)).blob()
const primaryZipReader = new JSZip()
await primaryZipReader.loadAsync(primaryFileData)
if (loaders.includes('fabric'))
primaryZipReader.file('fabric.mod.json', JSON.stringify(fabricModJson))
if (loaders.includes('quilt'))
primaryZipReader.file('quilt.mod.json', JSON.stringify(quiltModJson))
if (loaders.includes('forge'))
primaryZipReader.file('META-INF/mods.toml', TOML.stringify(forgeModsToml))
if (!newForge && loaders.includes('forge')) {
const classFile = new Uint8Array(
await (
await fetch(
'https://cdn.modrinth.com/wrapper/ModrinthWrapperRestiched.class'
)
).arrayBuffer()
)
let binary = ''
for (let i = 0; i < classFile.byteLength; i++) {
binary += String.fromCharCode(classFile[i])
}
binary = binary
.replace(
String.fromCharCode(32) + 'needs1to1be1changed1modrinth1mod',
String.fromCharCode(newSlug.length) + newSlug
)
.replace('/wrappera/', `/${project.id.substring(0, 8)}/`)
const newArr = []
for (let i = 0; i < binary.length; i++) {
newArr.push(binary.charCodeAt(i))
}
primaryZipReader.file(
`com/modrinth/${project.id.substring(0, 8)}/ModrinthWrapper.class`,
new Uint8Array(newArr)
)
}
const resourcePack = version.files.find(
(x) => x.file_type === 'required-resource-pack'
)
const resourcePackData = resourcePack
? await (await fetch(resourcePack.url)).blob()
: null
if (resourcePackData) {
const resourcePackReader = new JSZip()
await resourcePackReader.loadAsync(resourcePackData)
for (const [path, file] of Object.entries(resourcePackReader.files)) {
if (!primaryZipReader.file(path) && !path.includes('.mcassetsroot')) {
primaryZipReader.file(path, await file.async('uint8array'))
}
}
}
if (primaryZipReader.file('pack.png')) {
primaryZipReader.file(
iconPath,
await primaryZipReader.file('pack.png').async('uint8array')
)
}
return await primaryZipReader.generateAsync({
type: 'blob',
mimeType: 'application/java-archive',
})
}

View File

@@ -153,24 +153,46 @@ export default (ctx, inject) => {
const isMod = categories.some((category) => { const isMod = categories.some((category) => {
return ctx.store.state.tag.loaderData.modLoaders.includes(category) return ctx.store.state.tag.loaderData.modLoaders.includes(category)
}) })
return isPlugin && isMod ? 'mod and plugin' : isPlugin ? 'plugin' : 'mod' const isDataPack = categories.some((category) => {
} else { return ctx.store.state.tag.loaderData.dataPackLoaders.includes(category)
return formatProjectType(type) })
if (isMod && isPlugin && isDataPack) {
return 'mod, plugin, and data pack'
} else if (isMod && isPlugin) {
return 'mod and plugin'
} else if (isMod && isDataPack) {
return 'mod and datapack'
}
} }
return type
}) })
inject('getProjectTypeForUrl', (type, categories) => { inject('getProjectTypeForUrl', (type, categories) => {
if (type === 'mod') { if (type === 'mod') {
const isMod = categories.some((category) => {
return ctx.store.state.tag.loaderData.modLoaders.includes(category)
})
const isPlugin = categories.some((category) => { const isPlugin = categories.some((category) => {
return ctx.store.state.tag.loaderData.allPluginLoaders.includes( return ctx.store.state.tag.loaderData.allPluginLoaders.includes(
category category
) )
}) })
const isMod = categories.some((category) => { const isDataPack = categories.some((category) => {
return ctx.store.state.tag.loaderData.modLoaders.includes(category) return ctx.store.state.tag.loaderData.dataPackLoaders.includes(category)
}) })
return isPlugin && isMod ? 'mod' : isPlugin ? 'plugin' : 'mod' if (isDataPack) {
return 'datapack'
} else if (isPlugin) {
return 'plugin'
} else if (isMod) {
return 'mod'
} else {
return 'mod'
}
} else { } else {
return type return type
} }
@@ -232,7 +254,10 @@ export const formatWallet = (name) => {
export const formatProjectType = (name) => { export const formatProjectType = (name) => {
if (name === 'resourcepack') { if (name === 'resourcepack') {
return 'Resource Pack' return 'Resource Pack'
} else if (name === 'datapack') {
return 'Data Pack'
} }
return capitalizeString(name) return capitalizeString(name)
} }
@@ -261,6 +286,8 @@ export const formatCategory = (name) => {
return 'Path Tracing' return 'Path Tracing'
} else if (name === 'pbr') { } else if (name === 'pbr') {
return 'PBR' return 'PBR'
} else if (name === 'datapack') {
return 'Data pack'
} }
return capitalizeString(name) return capitalizeString(name)

View File

@@ -19,6 +19,7 @@ export const defaults = {
resourcepack: 'gallery', resourcepack: 'gallery',
modpack: 'list', modpack: 'list',
shader: 'gallery', shader: 'gallery',
datapack: 'list',
user: 'list', user: 'list',
}, },
} }

View File

@@ -17,6 +17,11 @@ export const state = () => ({
id: 'plugin', id: 'plugin',
display: 'plugin', display: 'plugin',
}, },
{
actual: 'mod',
id: 'datapack',
display: 'datapack',
},
{ {
actual: 'resourcepack', actual: 'resourcepack',
id: 'resourcepack', id: 'resourcepack',
@@ -46,6 +51,7 @@ export const state = () => ({
'waterfall', 'waterfall',
'velocity', 'velocity',
], ],
dataPackLoaders: ['datapack'],
modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'], modLoaders: ['forge', 'fabric', 'quilt', 'liteloader', 'modloader', 'rift'],
}, },
projectViewModes: ['list', 'grid', 'gallery'], projectViewModes: ['list', 'grid', 'gallery'],