New gallery creation/editing/deleting UI (#826)

* New gallery creation/editing/deleting UI

* Finish new gallery UI

* port dp changes here

* Add ordering fix optional fields

* Fix dropping

* Fix fmt

* Fix version creation broken, edit issues, project type setting

* Update gallery in search
This commit is contained in:
Geometrically
2022-12-31 23:47:47 -07:00
committed by GitHub
parent f11aab6c19
commit d2b1404907
18 changed files with 488 additions and 321 deletions

View File

@@ -513,8 +513,10 @@ tr.button-transparent {
box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent; box-shadow: var(--shadow-inset-sm), 0 0 0 0 transparent;
svg { svg {
width: 1.25rem; min-width: 1.25rem;
height: 1.25rem; max-width: 1.25rem;
min-height: 1.25rem;
max-height: 1.25rem;
margin: auto; margin: auto;
} }

View File

@@ -6,7 +6,7 @@
(event) => { (event) => {
$refs.drop_area.style.visibility = 'hidden' $refs.drop_area.style.visibility = 'hidden'
if (event.dataTransfer && event.dataTransfer.files) { if (event.dataTransfer && event.dataTransfer.files && fileAllowed) {
$emit('change', event.dataTransfer.files) $emit('change', event.dataTransfer.files)
} }
} }
@@ -26,30 +26,40 @@ export default {
default: '', default: '',
}, },
}, },
mounted() { data() {
// eslint-disable-next-line nuxt/no-env-in-hooks return {
if (process.client) { fileAllowed: false,
document.addEventListener('dragenter', () => {
this.$refs.drop_area.style.visibility = 'visible'
})
} }
}, },
mounted() {
document.addEventListener('dragenter', this.allowDrag)
},
methods: { methods: {
allowDrag(event) { allowDrag(event) {
const file = event.dataTransfer?.items[0] const file = event.dataTransfer?.items[0]
console.log(file)
if ( if (
file && file &&
this.accept this.accept
.split(',') .split(',')
.reduce( .reduce(
(acc, t) => acc || file.type.startsWith(t) || file.type === t, (acc, t) =>
acc || file.type.startsWith(t) || file.type === t || t === '*',
false false
) )
) { ) {
// Adds file dropping indicator this.fileAllowed = true
event.dataTransfer.dropEffect = 'copy' event.dataTransfer.dropEffect = 'copy'
event.preventDefault() event.preventDefault()
if (this.$refs.drop_area)
this.$refs.drop_area.style.visibility = 'visible'
} else {
this.fileAllowed = false
if (this.$refs.drop_area)
this.$refs.drop_area.style.visibility = 'hidden'
} }
}, },
}, },

View File

@@ -206,7 +206,14 @@ Questions? [Join the Modrinth Discord for support!](https://discord.gg/EUHuJHt)`
}) })
this.$refs.modal.hide() this.$refs.modal.hide()
await this.$router.replace(`/${projectType.actual}/${this.slug}`) await this.$router.push({
name: 'type-id',
params: {
type: projectType.id,
id: this.slug,
overrideProjectType: projectType.id,
},
})
} catch (err) { } catch (err) {
this.$notify({ this.$notify({
group: 'main', group: 'main',

View File

@@ -15,12 +15,9 @@
class="gallery" class="gallery"
tabindex="-1" tabindex="-1"
:to="`/${$getProjectTypeForUrl(type, categories)}/${id}`" :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"
:style="color ? `background-color: ${toColor};` : ''"
> >
<img <img v-if="featuredImage" :src="featuredImage" alt="gallery image" />
v-if="galleryImages.length > 0"
:src="galleryImages[0]"
alt="Gallery image TODO: improve this lol"
/>
</nuxt-link> </nuxt-link>
<div class="title"> <div class="title">
<nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`"> <nuxt-link :to="`/${$getProjectTypeForUrl(type, categories)}/${id}`">
@@ -99,6 +96,14 @@
<ServerIcon aria-hidden="true" /> <ServerIcon aria-hidden="true" />
Server Server
</template> </template>
<template
v-else-if="
serverSide === 'unsupported' && clientSide === 'unsupported'
"
>
<GlobeIcon aria-hidden="true" />
Unsupported
</template>
<template v-else-if="moderation"> <template v-else-if="moderation">
<InfoIcon aria-hidden="true" /> <InfoIcon aria-hidden="true" />
A {{ projectTypeDisplay }} A {{ projectTypeDisplay }}
@@ -258,12 +263,10 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
galleryImages: { featuredImage: {
type: Array, type: String,
required: false, required: false,
default() { default: null,
return []
},
}, },
showUpdatedDate: { showUpdatedDate: {
type: Boolean, type: Boolean,
@@ -275,11 +278,25 @@ export default {
required: false, required: false,
default: false, default: false,
}, },
color: {
type: Number,
required: false,
default: null,
},
}, },
computed: { computed: {
projectTypeDisplay() { projectTypeDisplay() {
return this.$getProjectTypeForDisplay(this.type, this.categories) return this.$getProjectTypeForDisplay(this.type, this.categories)
}, },
toColor() {
let color = this.color
color >>>= 0
const b = color & 0xff
const g = (color & 0xff00) >>> 8
const r = (color & 0xff0000) >>> 16
return 'rgba(' + [r, g, b, 1].join(',') + ')'
},
}, },
} }
</script> </script>
@@ -338,7 +355,10 @@ export default {
height: 10rem; height: 10rem;
background-color: var(--color-button-bg-active); background-color: var(--color-button-bg-active);
filter: brightness(0.7);
img { img {
box-shadow: none;
width: 100%; width: 100%;
height: 10rem; height: 10rem;
object-fit: cover; object-fit: cover;
@@ -349,6 +369,13 @@ export default {
margin-left: var(--spacing-card-bg); margin-left: var(--spacing-card-bg);
margin-top: -3rem; margin-top: -3rem;
z-index: 1; z-index: 1;
img,
svg {
border-radius: var(--size-rounded-lg);
border: 0.25rem solid var(--color-raised-bg);
border-bottom: none;
}
} }
.title { .title {

View File

@@ -508,9 +508,9 @@ export default {
} }
}, },
}, },
beforeCreate() { async beforeCreate() {
if (this.$route.query.code) { if (this.$route.query.code) {
this.$router.push(this.$route.path) await this.$router.push(this.$route.path)
} }
}, },
created() { created() {
@@ -619,10 +619,6 @@ export default {
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
@media screen and (max-width: 750px) {
justify-content: center;
}
section.logo { section.logo {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -1082,7 +1078,7 @@ export default {
&.active { &.active {
display: flex; display: flex;
@media screen and (min-width: 750px) { @media screen and (min-width: 1024px) {
display: none; display: none;
} }
} }

View File

@@ -755,10 +755,12 @@ export default {
JSON.stringify(project.project_type) JSON.stringify(project.project_type)
) )
project.project_type = data.$getProjectTypeForUrl( project.project_type = data.params.overrideProjectType
project.project_type, ? data.params.overrideProjectType
Object.keys(projectLoaders) : data.$getProjectTypeForUrl(
) project.project_type,
Object.keys(projectLoaders)
)
if ( if (
project.project_type !== data.params.type || project.project_type !== data.params.type ||
@@ -922,6 +924,33 @@ export default {
}, },
}, },
methods: { methods: {
async resetProject() {
const project = (
await this.$axios.get(
`project/${this.project.id}`,
this.$defaultHeaders()
)
).data
const projectLoaders = {}
for (const version of this.versions) {
for (const loader of version.loaders) {
projectLoaders[loader] = true
}
}
project.actualProjectType = JSON.parse(
JSON.stringify(project.project_type)
)
project.project_type = this.$getProjectTypeForUrl(
project.project_type,
Object.keys(projectLoaders)
)
this.project = project
},
findPrimary(version) { findPrimary(version) {
let file = version.files.find((x) => x.primary) let file = version.files.find((x) => x.primary)

View File

@@ -150,9 +150,9 @@ export default {
} }
}, },
methods: { methods: {
switchPage(page, toTop) { async switchPage(page, toTop) {
this.currentPage = page this.currentPage = page
this.$router.replace(this.getPageLink(page)) await this.$router.replace(this.getPageLink(page))
if (toTop) { if (toTop) {
setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50) setTimeout(() => window.scrollTo({ top: 0, behavior: 'smooth' }), 50)

View File

@@ -829,19 +829,7 @@ export default {
) )
} }
// While the emit below will take care of most changes, await this.$parent.resetProject()
// some items require manually updating
this.newProject.license.id = this.licenseId
this.newProject.client_side = this.clientSideType.toLowerCase()
this.newProject.server_side = this.serverSideType.toLowerCase()
this.newProject.client_side = this.clientSideType.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.isEditing = false this.isEditing = false

View File

@@ -1,5 +1,136 @@
<template> <template>
<div> <div>
<Modal
v-if="$auth.user && currentMember"
ref="modal_edit_item"
:header="editIndex === -1 ? 'Upload gallery image' : 'Edit gallery item'"
>
<div class="modal-gallery universal-labels">
<div class="gallery-file-input">
<div class="file-header">
<ImageIcon />
<strong>{{ editFile ? editFile.name : 'Current image' }}</strong>
<FileInput
v-if="editIndex === -1"
class="iconified-button raised-button"
prompt="Replace"
:accept="acceptFileTypes"
:max-size="524288000"
should-always-reset
@change="
(x) => {
editFile = x[0]
showPreviewImage()
}
"
>
<TransferIcon />
</FileInput>
</div>
<img
:src="
previewImage
? previewImage
: project.gallery[editIndex] && project.gallery[editIndex].url
? project.gallery[editIndex].url
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
alt="gallery-preview"
/>
</div>
<label for="gallery-image-title">
<span class="label__title">Title</span>
</label>
<input
id="gallery-image-title"
v-model="editTitle"
type="text"
maxlength="64"
placeholder="Enter title..."
/>
<label for="gallery-image-desc">
<span class="label__title">Description</span>
</label>
<div class="textarea-wrapper">
<textarea
id="gallery-image-desc"
v-model="editDescription"
maxlength="255"
placeholder="Enter description..."
/>
</div>
<label for="gallery-image-ordering">
<span class="label__title">Order Index</span>
</label>
<input
id="gallery-image-ordering"
v-model="editOrder"
type="number"
placeholder="Enter order index..."
/>
<label for="gallery-image-featured">
<span class="label__title">Featured</span>
<span class="label__description">
A featured gallery image shows up in search and your project card.
Only one gallery image can be featured.
</span>
</label>
<button
v-if="!editFeatured"
id="gallery-image-featured"
class="iconified-button"
@click="editFeatured = true"
>
<StarIcon aria-hidden="true" />
Feature image
</button>
<button
v-else
id="gallery-image-featured"
class="iconified-button"
@click="editFeatured = false"
>
<StarIcon fill="currentColor" aria-hidden="true" />
Unfeature image
</button>
<div class="button-group">
<button
class="iconified-button"
@click="$refs.modal_edit_item.hide()"
>
<CrossIcon />
Cancel
</button>
<button
v-if="editIndex === -1"
class="iconified-button brand-button"
:disabled="!$nuxt.$loading"
@click="createGalleryItem"
>
<PlusIcon />
Add gallery image
</button>
<button
v-else
class="iconified-button brand-button"
:disabled="!$nuxt.$loading"
@click="editGalleryItem"
>
<SaveIcon />
Save changes
</button>
</div>
</div>
</Modal>
<ModalConfirm
v-if="$auth.user && currentMember"
ref="modal_confirm"
title="Are you sure you want to delete this gallery image?"
description="This will remove this gallery image forever (like really forever)."
:has-to-type="false"
proceed-label="Delete"
@proceed="deleteGalleryImage"
/>
<div <div
v-if="expandedGalleryItem != null" v-if="expandedGalleryItem != null"
class="expanded-image-modal" class="expanded-image-modal"
@@ -55,14 +186,14 @@
<ContractIcon v-else aria-hidden="true" /> <ContractIcon v-else aria-hidden="true" />
</button> </button>
<button <button
v-if="gallery.length > 1" v-if="project.gallery.length > 1"
class="previous circle-button" class="previous circle-button"
@click="previousImage()" @click="previousImage()"
> >
<LeftArrowIcon aria-hidden="true" /> <LeftArrowIcon aria-hidden="true" />
</button> </button>
<button <button
v-if="gallery.length > 1" v-if="project.gallery.length > 1"
class="next circle-button" class="next circle-button"
@click="nextImage()" @click="nextImage()"
> >
@@ -73,61 +204,24 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="currentMember" class="card buttons header-buttons"> <div v-if="currentMember" class="card header-buttons">
<button <FileInput
class="iconified-button" :max-size="524288000"
:class="{ :accept="acceptFileTypes"
'brand-button': prompt="Upload an image"
newGalleryItems.length === 0 && class="brand-button iconified-button"
editGalleryIndexes.length === 0 && @change="handleFiles"
deleteGalleryUrls.length === 0,
}"
@click="
newGalleryItems.push({
title: '',
description: '',
featured: false,
url: '',
})
"
> >
<PlusIcon /> <UploadIcon />
{{ </FileInput>
newGalleryItems.length === 0 && <span class="indicator">
editGalleryIndexes.length === 0 && <InfoIcon /> Click to choose an image or drag one onto this page
deleteGalleryUrls.length === 0 </span>
? 'Add an image' <DropArea :accept="acceptFileTypes" @change="handleFiles" />
: 'Add another image'
}}
</button>
<button
v-if="
newGalleryItems.length > 0 ||
editGalleryIndexes.length > 0 ||
deleteGalleryUrls.length > 0
"
class="action iconified-button"
@click="resetGallery"
>
<CrossIcon />
Cancel
</button>
<button
v-if="
newGalleryItems.length > 0 ||
editGalleryIndexes.length > 0 ||
deleteGalleryUrls.length > 0
"
class="action brand-button iconified-button"
@click="saveGallery"
>
<CheckIcon />
Save changes
</button>
</div> </div>
<div class="items"> <div class="items">
<div <div
v-for="(item, index) in gallery" v-for="(item, index) in project.gallery"
:key="index" :key="index"
class="card gallery-item" class="card gallery-item"
> >
@@ -142,22 +236,7 @@
/> />
</a> </a>
<div class="gallery-body"> <div class="gallery-body">
<div v-if="editGalleryIndexes.includes(index)" class="gallery-info"> <div class="gallery-info">
<input
v-model="item.title"
type="text"
placeholder="Enter the title..."
/>
<div class="textarea-wrapper">
<textarea
id="body"
v-model="item.description"
placeholder="Enter the description..."
/>
</div>
<Checkbox v-model="item.featured" label="Featured" />
</div>
<div v-else class="gallery-info">
<h2 v-if="item.title">{{ item.title }}</h2> <h2 v-if="item.title">{{ item.title }}</h2>
<p v-if="item.description">{{ item.description }}</p> <p v-if="item.description">{{ item.description }}</p>
</div> </div>
@@ -169,22 +248,16 @@
</div> </div>
<div v-if="currentMember" class="gallery-buttons input-group"> <div v-if="currentMember" class="gallery-buttons input-group">
<button <button
v-if="editGalleryIndexes.includes(index)"
class="iconified-button" class="iconified-button"
@click=" @click="
editGalleryIndexes.splice(editGalleryIndexes.indexOf(index), 1) resetEdit()
gallery[index] = JSON.parse( editIndex = index
JSON.stringify(project.gallery[index]) editTitle = item.title
) editDescription = item.description
editFeatured = item.featured
editOrder = item.ordering
$refs.modal_edit_item.show()
" "
>
<CrossIcon />
Cancel
</button>
<button
v-else
class="iconified-button"
@click="editGalleryIndexes.push(index)"
> >
<EditIcon /> <EditIcon />
Edit Edit
@@ -192,8 +265,8 @@
<button <button
class="iconified-button" class="iconified-button"
@click=" @click="
deleteGalleryUrls.push(item.url) deleteIndex = index
gallery.splice(index, 1) $refs.modal_confirm.show()
" "
> >
<TrashIcon /> <TrashIcon />
@@ -202,59 +275,6 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-for="(item, index) in newGalleryItems"
:key="index + 'new'"
class="card gallery-item"
>
<img
:src="
newGalleryItems[index].preview
? newGalleryItems[index].preview
: 'https://cdn.modrinth.com/placeholder-banner.svg'
"
:alt="item.title ? item.title : 'gallery-image'"
/>
<div class="gallery-body">
<div class="gallery-info">
<input
v-model="item.title"
type="text"
placeholder="Enter the title..."
/>
<div class="resizable-textarea-wrapper">
<textarea
id="body"
v-model="item.description"
placeholder="Enter the description..."
/>
</div>
<Checkbox v-model="item.featured" label="Featured" />
</div>
</div>
<div class="gallery-bottom">
<FileInput
:max-size="5242880"
accept="image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp"
prompt="Choose image or drag it here"
class="iconified-button"
@change="(files) => showPreviewImage(files, index)"
>
<UploadIcon />
</FileInput>
<div class="gallery-buttons">
<div class="delete-button-container">
<button
class="iconified-button"
@click="newGalleryItems.splice(index, 1)"
>
<TrashIcon />
Remove
</button>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -267,24 +287,29 @@ import CrossIcon from '~/assets/images/utils/x.svg?inline'
import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline' import RightArrowIcon from '~/assets/images/utils/right-arrow.svg?inline'
import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline' import LeftArrowIcon from '~/assets/images/utils/left-arrow.svg?inline'
import EditIcon from '~/assets/images/utils/edit.svg?inline' import EditIcon from '~/assets/images/utils/edit.svg?inline'
import CheckIcon from '~/assets/images/utils/check.svg?inline' import SaveIcon from '~/assets/images/utils/save.svg?inline'
import ExternalIcon from '~/assets/images/utils/external.svg?inline' import ExternalIcon from '~/assets/images/utils/external.svg?inline'
import ExpandIcon from '~/assets/images/utils/expand.svg?inline' import ExpandIcon from '~/assets/images/utils/expand.svg?inline'
import ContractIcon from '~/assets/images/utils/contract.svg?inline' import ContractIcon from '~/assets/images/utils/contract.svg?inline'
import StarIcon from '~/assets/images/utils/star.svg?inline'
import UploadIcon from '~/assets/images/utils/upload.svg?inline' import UploadIcon from '~/assets/images/utils/upload.svg?inline'
import InfoIcon from '~/assets/images/utils/info.svg?inline'
import ImageIcon from '~/assets/images/utils/image.svg?inline'
import TransferIcon from '~/assets/images/utils/transfer.svg?inline'
import FileInput from '~/components/ui/FileInput' import FileInput from '~/components/ui/FileInput'
import Checkbox from '~/components/ui/Checkbox' import DropArea from '~/components/ui/DropArea'
import ModalConfirm from '~/components/ui/ModalConfirm'
import Modal from '~/components/ui/Modal'
export default { export default {
components: { components: {
CalendarIcon, CalendarIcon,
PlusIcon, PlusIcon,
Checkbox,
EditIcon, EditIcon,
TrashIcon, TrashIcon,
CheckIcon, SaveIcon,
FileInput, StarIcon,
CrossIcon, CrossIcon,
RightArrowIcon, RightArrowIcon,
LeftArrowIcon, LeftArrowIcon,
@@ -292,13 +317,15 @@ export default {
ExpandIcon, ExpandIcon,
ContractIcon, ContractIcon,
UploadIcon, UploadIcon,
InfoIcon,
ImageIcon,
TransferIcon,
ModalConfirm,
Modal,
FileInput,
DropArea,
}, },
auth: false, auth: false,
beforeRouteLeave(to, from, next) {
this.resetGallery()
next()
},
props: { props: {
project: { project: {
type: Object, type: Object,
@@ -315,18 +342,21 @@ export default {
}, },
data() { data() {
return { return {
gallery: [],
newGalleryItems: [],
editGalleryIndexes: [],
deleteGalleryUrls: [],
expandedGalleryItem: null, expandedGalleryItem: null,
expandedGalleryIndex: 0, expandedGalleryIndex: 0,
zoomedIn: false, zoomedIn: false,
deleteIndex: -1,
editIndex: -1,
editTitle: '',
editDescription: '',
editFeatured: false,
editOrder: null,
editFile: null,
previewImage: null,
} }
}, },
fetch() {
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
},
head() { head() {
const title = `${this.project.title} - Gallery` const title = `${this.project.title} - Gallery`
const description = `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.` const description = `View ${this.project.gallery.length} images of ${this.project.title} on Modrinth.`
@@ -357,6 +387,11 @@ export default {
], ],
} }
}, },
computed: {
acceptFileTypes() {
return 'image/png,image/jpeg,image/gif,image/webp,.png,.jpeg,.gif,.webp'
},
},
mounted() { mounted() {
this._keyListener = function (e) { this._keyListener = function (e) {
if (this.expandedGalleryItem) { if (this.expandedGalleryItem) {
@@ -371,125 +406,156 @@ export default {
} }
} }
// eslint-disable-next-line nuxt/no-env-in-hooks document.addEventListener('keydown', this._keyListener.bind(this))
if (process.client) {
document.addEventListener('keydown', this._keyListener.bind(this))
}
}, },
methods: { methods: {
showPreviewImage(files, index) {
const reader = new FileReader()
this.newGalleryItems[index].icon = files[0]
if (this.newGalleryItems[index].icon instanceof Blob) {
reader.readAsDataURL(this.newGalleryItems[index].icon)
reader.onload = (event) => {
this.newGalleryItems[index].preview = event.target.result
// TODO: Find an alternative for this!
this.$forceUpdate()
}
}
},
async saveGallery() {
this.$nuxt.$loading.start()
try {
for (const item of this.newGalleryItems) {
let url = `project/${this.project.id}/gallery?ext=${
item.icon
? item.icon.type.split('/')[item.icon.type.split('/').length - 1]
: null
}&featured=${item.featured}`
if (item.title) url += `&title=${encodeURIComponent(item.title)}`
if (item.description)
url += `&description=${encodeURIComponent(item.description)}`
await this.$axios.post(url, item.icon, this.$defaultHeaders())
}
for (const index of this.editGalleryIndexes) {
const item = this.gallery[index]
let url = `project/${
this.project.id
}/gallery?url=${encodeURIComponent(item.url)}&featured=${
item.featured
}`
if (item.title) url += `&title=${encodeURIComponent(item.title)}`
if (item.description)
url += `&description=${encodeURIComponent(item.description)}`
await this.$axios.patch(url, {}, this.$defaultHeaders())
}
for (const url of this.deleteGalleryUrls) {
await this.$axios.delete(
`project/${this.project.id}/gallery?url=${encodeURIComponent(url)}`,
this.$defaultHeaders()
)
}
const project = (
await this.$axios.get(
`project/${this.project.id}`,
this.$defaultHeaders()
)
).data
this.$emit('update:project', project)
this.gallery = JSON.parse(JSON.stringify(project.gallery))
this.deleteGalleryUrls = []
this.editGalleryIndexes = []
this.newGalleryItems = []
} catch (err) {
const description = err.response.data.description
this.$notify({
group: 'main',
title: 'An error occurred',
text: description,
type: 'error',
})
window.scrollTo({ top: 0, behavior: 'smooth' })
}
this.$nuxt.$loading.finish()
},
resetGallery() {
this.newGalleryItems = []
this.editGalleryIndexes = []
this.deleteGalleryUrls = []
this.gallery = JSON.parse(JSON.stringify(this.project.gallery))
},
nextImage() { nextImage() {
this.expandedGalleryIndex++ this.expandedGalleryIndex++
if (this.expandedGalleryIndex >= this.gallery.length) { if (this.expandedGalleryIndex >= this.project.gallery.length) {
this.expandedGalleryIndex = 0 this.expandedGalleryIndex = 0
} }
this.expandedGalleryItem = this.gallery[this.expandedGalleryIndex] this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
}, },
previousImage() { previousImage() {
this.expandedGalleryIndex-- this.expandedGalleryIndex--
if (this.expandedGalleryIndex < 0) { if (this.expandedGalleryIndex < 0) {
this.expandedGalleryIndex = this.gallery.length - 1 this.expandedGalleryIndex = this.project.gallery.length - 1
} }
this.expandedGalleryItem = this.gallery[this.expandedGalleryIndex] this.expandedGalleryItem = this.project.gallery[this.expandedGalleryIndex]
}, },
expandImage(item, index) { expandImage(item, index) {
this.expandedGalleryItem = item this.expandedGalleryItem = item
this.expandedGalleryIndex = index this.expandedGalleryIndex = index
this.zoomedIn = false this.zoomedIn = false
}, },
resetEdit() {
this.editIndex = -1
this.editTitle = ''
this.editDescription = ''
this.editFeatured = false
this.editOrder = null
this.editFile = null
this.previewImage = null
},
handleFiles(files) {
this.resetEdit()
this.editFile = files[0]
this.showPreviewImage()
this.$refs.modal_edit_item.show()
},
showPreviewImage() {
const reader = new FileReader()
if (this.editFile instanceof Blob) {
reader.readAsDataURL(this.editFile)
reader.onload = (event) => {
this.previewImage = event.target.result
}
}
},
async createGalleryItem() {
this.$nuxt.$loading.start()
try {
let url = `project/${this.project.id}/gallery?ext=${
this.editFile
? this.editFile.type.split('/')[
this.editFile.type.split('/').length - 1
]
: null
}&featured=${this.editFeatured}`
if (this.editTitle) url += `&title=${this.editTitle}`
if (this.editDescription) url += `&description=${this.editDescription}`
if (this.editOrder) url += `&ordering=${this.editOrder}`
await this.$axios.post(url, this.editFile, this.$defaultHeaders())
await this.updateProject()
this.$refs.modal_edit_item.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
},
async editGalleryItem() {
this.$nuxt.$loading.start()
try {
let url = `project/${this.project.id}/gallery?url=${encodeURIComponent(
this.project.gallery[this.editIndex].url
)}&featured=${this.editFeatured}`
if (this.editTitle) url += `&title=${this.editTitle}`
if (this.editDescription) url += `&description=${this.editDescription}`
if (this.editOrder) url += `&ordering=${this.editOrder}`
await this.$axios.patch(url, {}, this.$defaultHeaders())
await this.updateProject()
this.$refs.modal_edit_item.hide()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
},
async deleteGalleryImage() {
this.$nuxt.$loading.start()
try {
await this.$axios.delete(
`project/${this.project.id}/gallery?url=${encodeURIComponent(
this.project.gallery[this.deleteIndex].url
)}`,
this.$defaultHeaders()
)
await this.updateProject()
} catch (err) {
this.$notify({
group: 'main',
title: 'An error occurred',
text: err.response ? err.response.data.description : err,
type: 'error',
})
}
this.$nuxt.$loading.finish()
},
async updateProject() {
await this.$parent.resetProject()
this.resetEdit()
},
}, },
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.header-buttons {
display: flex;
align-items: center;
gap: 1rem;
.indicator {
display: flex;
gap: 0.5ch;
align-items: center;
color: var(--color-text-inactive);
}
}
.expanded-image-modal { .expanded-image-modal {
position: fixed; position: fixed;
z-index: 20; z-index: 20;
@@ -656,15 +722,6 @@ export default {
width: calc(100% - 2 * var(--spacing-card-md)); width: calc(100% - 2 * var(--spacing-card-md));
padding: var(--spacing-card-sm) var(--spacing-card-md); padding: var(--spacing-card-sm) var(--spacing-card-md);
textarea {
border-radius: var(--size-rounded-sm);
}
input {
width: 100%;
margin: 0 0 0.25rem;
}
.gallery-info { .gallery-info {
h2 { h2 {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
@@ -716,8 +773,46 @@ export default {
} }
} }
.header-buttons { .modal-gallery {
padding: var(--spacing-card-bg);
display: flex; display: flex;
justify-content: right; flex-direction: column;
.gallery-file-input {
.file-header {
border-radius: var(--size-rounded-card) var(--size-rounded-card) 0 0;
display: flex;
align-items: center;
gap: 0.5rem;
background-color: var(--color-button-bg);
padding: var(--spacing-card-md);
svg {
min-width: 1rem;
}
strong {
word-wrap: anywhere;
}
.iconified-button {
margin-left: auto;
}
}
img {
border-radius: 0 0 var(--size-rounded-card) var(--size-rounded-card);
width: 100%;
height: auto;
max-height: 15rem;
object-fit: contain;
background-color: #000000;
}
}
.button-group {
margin-left: auto;
margin-top: 1.5rem;
}
} }
</style> </style>

View File

@@ -1287,7 +1287,7 @@ export default {
for (const hash of this.deleteFiles) { for (const hash of this.deleteFiles) {
await this.$axios.delete( await this.$axios.delete(
`version_file/${hash}`, `version_file/${hash}?version_id=${this.version.id}`,
this.$defaultHeaders() this.$defaultHeaders()
) )
} }

View File

@@ -197,8 +197,8 @@ export default {
updateVersions(updatedVersions) { updateVersions(updatedVersions) {
this.filteredVersions = updatedVersions this.filteredVersions = updatedVersions
}, },
handleFiles(files) { async handleFiles(files) {
this.$router.push({ await this.$router.push({
name: 'type-id-version-create', name: 'type-id-version-create',
params: { params: {
type: this.project.project_type, type: this.project.project_type,

View File

@@ -86,6 +86,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"
:color="project.color"
:moderation="true" :moderation="true"
> >
<button <button

View File

@@ -365,7 +365,11 @@
:id="result.slug ? result.slug : result.project_id" :id="result.slug ? result.slug : result.project_id"
:key="result.project_id" :key="result.project_id"
:display="$cosmetics.searchDisplayMode[projectType.id]" :display="$cosmetics.searchDisplayMode[projectType.id]"
:gallery-images="result.gallery" :gallery-images="
result.featured_gallery
? result.featured_gallery
: result.gallery[0]
"
:type="result.project_type" :type="result.project_type"
:author="result.author" :author="result.author"
:name="result.title" :name="result.title"
@@ -383,6 +387,7 @@
:hide-loaders=" :hide-loaders="
['resourcepack', 'datapack'].includes(projectType.id) ['resourcepack', 'datapack'].includes(projectType.id)
" "
:color="result.color"
/> />
<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>
@@ -966,6 +971,11 @@ export default {
white-space: nowrap; white-space: nowrap;
} }
} }
.square-button {
margin-top: auto;
margin-bottom: 0.25rem;
}
} }
} }

View File

@@ -14,6 +14,7 @@
:name="project.title" :name="project.title"
:client-side="project.client_side" :client-side="project.client_side"
:server-side="project.server_side" :server-side="project.server_side"
:color="project.color"
> >
<button <button
class="iconified-button" class="iconified-button"

View File

@@ -213,7 +213,7 @@
project.gallery project.gallery
.slice() .slice()
.sort((a, b) => b.featured - a.featured) .sort((a, b) => b.featured - a.featured)
.map((x) => x.url) .map((x) => x.url)[0]
" "
:description="project.description" :description="project.description"
:created-at="project.published" :created-at="project.published"
@@ -234,6 +234,7 @@
" "
:has-mod-message="project.moderator_message" :has-mod-message="project.moderator_message"
:type="project.project_type" :type="project.project_type"
:color="project.color"
/> />
</div> </div>
<div v-else class="error"> <div v-else class="error">

View File

@@ -31,9 +31,9 @@ export const fileIsValid = (file, validationOptions) => {
export const acceptFileFromProjectType = (projectType) => { export const acceptFileFromProjectType = (projectType) => {
switch (projectType) { switch (projectType) {
case 'mod': case 'mod':
return '.jar,.zip,.litemod,application/java-archive,application/zip' return '.jar,.zip,.litemod,application/java-archive,application/x-java-archive,application/zip'
case 'plugin': case 'plugin':
return '.jar,.zip,application/java-archive,application/zip' return '.jar,.zip,application/java-archive,application/x-java-archive,application/zip'
case 'resourcepack': case 'resourcepack':
return '.zip,application/zip' return '.zip,application/zip'
case 'shader': case 'shader':
@@ -41,7 +41,7 @@ export const acceptFileFromProjectType = (projectType) => {
case 'datapack': case 'datapack':
return '.zip,application/zip' return '.zip,application/zip'
case 'modpack': case 'modpack':
return '.mrpack,application/x-modrinth-modpack+zip' return '.mrpack,application/x-modrinth-modpack+zip,application/zip'
default: default:
return '*' return '*'
} }
@@ -437,7 +437,7 @@ export const createDataPackVersion = async function (
modLoader: newForge ? 'lowcodefml' : 'javafml', modLoader: newForge ? 'lowcodefml' : 'javafml',
loaderVersion: newForge ? '[40,)' : '[25,)', loaderVersion: newForge ? '[40,)' : '[25,)',
license: project.license.id, license: project.license.id,
showAsResourcePack: true, showAsResourcePack: false,
mods: [ mods: [
{ {
modId: newSlug, modId: newSlug,

View File

@@ -287,7 +287,7 @@ export const formatCategory = (name) => {
} else if (name === 'pbr') { } else if (name === 'pbr') {
return 'PBR' return 'PBR'
} else if (name === 'datapack') { } else if (name === 'datapack') {
return 'Data pack' return 'Data Pack'
} }
return capitalizeString(name) return capitalizeString(name)

View File

@@ -20,7 +20,7 @@ export const state = () => ({
{ {
actual: 'mod', actual: 'mod',
id: 'datapack', id: 'datapack',
display: 'datapack', display: 'data pack',
}, },
{ {
actual: 'resourcepack', actual: 'resourcepack',