aboutsummaryrefslogtreecommitdiffstats
path: root/src/pages
diff options
context:
space:
mode:
Diffstat (limited to 'src/pages')
-rw-r--r--src/pages/404.vue16
-rw-r--r--src/pages/FetchToken.vue68
-rw-r--r--src/pages/UploadCreate.vue264
-rw-r--r--src/pages/UploadDetail.vue223
-rw-r--r--src/pages/UploadList.vue119
5 files changed, 690 insertions, 0 deletions
diff --git a/src/pages/404.vue b/src/pages/404.vue
new file mode 100644
index 0000000..6484cb2
--- /dev/null
+++ b/src/pages/404.vue
@@ -0,0 +1,16 @@
+<template>
+ <div class="fixed-center text-center">
+ <p>
+ <img
+ src="~assets/sad.svg"
+ style="width:30vw;max-width:150px;"
+ >
+ </p>
+ <p class="text-faded">Sorry, nothing here...<strong>(404)</strong></p>
+ <q-btn
+ color="secondary"
+ style="width:200px;"
+ @click="$router.push('/')"
+ >Go back</q-btn>
+ </div>
+</template>
diff --git a/src/pages/FetchToken.vue b/src/pages/FetchToken.vue
new file mode 100644
index 0000000..6745377
--- /dev/null
+++ b/src/pages/FetchToken.vue
@@ -0,0 +1,68 @@
+<template>
+ <div>
+ <div v-if="!tokenValid">
+ <h6 class="text-negative">Received an invalid token!</h6>
+ <p>{{ tokenData.message }}</p>
+ </div>
+ <div v-else>
+ <h6 class="text-success">you have successfully logged in. You can close this window now!</h6>
+ </div>
+ </div>
+</template>
+
+<script>
+ import jwtDecode from 'jwt-decode'
+
+ export default {
+ components: {
+ },
+ data () {
+ return {
+ tokenValid: false,
+ }
+ },
+ computed: {
+ routeToken () {
+ return this.$route.params.token
+ },
+ tokenData () {
+ try {
+ return jwtDecode(this.routeToken)
+ }
+ catch (e) {
+ return {
+ invalid: true,
+ message: e.message
+ }
+ }
+ },
+ },
+ watch: {
+ tokenData: {
+ handler: function (val, oldVal) {
+ console.log({val, oldVal})
+ if (val !== oldVal) {
+ if (val.invalid || !val.username) {
+ this.tokenValid = false
+ }
+ else {
+ let that = this
+ this.$http.get('/uploads', {headers: {Authorization: `Bearer ${this.routeToken}`}}).then(response => {
+ that.tokenValid = true
+ that.$store.commit('user/setAuthToken', { authToken: that.routeToken })
+ window.close()
+ }).catch(error => {
+ console.warn(error)
+ that.tokenValid = false
+ })
+ }
+ }
+ },
+ immediate: true,
+ },
+ }
+ }
+</script>
+
+<style lang="styl" type="text/stylus" scoped>
+</style>
diff --git a/src/pages/UploadCreate.vue b/src/pages/UploadCreate.vue
new file mode 100644
index 0000000..53a6a36
--- /dev/null
+++ b/src/pages/UploadCreate.vue
@@ -0,0 +1,264 @@
+<template>
+ <div class="flex row">
+ <div class="col" v-if="authToken">
+ <h5>Upload your own mod</h5>
+ <q-stepper ref="stepper" style="max-width: 800px" alternative-labels>
+ <q-step default name="data" title="Enter data" :error="errors.slug || errors.title || errors.description">
+ <p>Please enter a title and description for your mod</p>
+ <div class="text-negative" v-if="errors.slug">
+ {{ errors.slug }}
+ </div>
+ <q-field :error="errors.title !== undefined"
+ :error-label="errors.title"
+ icon="fa-pencil">
+ <q-input v-model="title" float-label="Title" />
+ </q-field>
+ <q-field :error="errors.description !== undefined"
+ :error-label="errors.description"
+ icon="fa-bars">
+ <q-input v-model="description" type="textarea" float-label="Description" />
+ </q-field>
+ <q-stepper-navigation>
+ <q-btn color="primary" :disabled="!description || !title" @click="$refs.stepper.next()">Next</q-btn>
+ </q-stepper-navigation>
+ </q-step>
+ <q-step name="pic" title="Add image">
+ <p>You can upload an image, e.g. a screenshot, that will be used as a preview for your mod</p>
+ <q-alert color="warning"
+ enter="bounceInLeft"
+ appear>
+ Please note that you must not upload anything protected by copyrights you do not own or don't have explicit permission to upload by the owner
+ </q-alert>
+ <dropzone id="picDropzone"
+ ref="picDropzone"
+ @vdropzone-success="picUploadSuccess"
+ :options="picDropzoneOptions">
+ </dropzone>
+ <q-stepper-navigation>
+ <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn>
+ <q-btn color="primary" @click="$refs.stepper.next()">Next</q-btn>
+ <transition enter="fadeIn" leave="fadeOut">
+ <q-btn v-if="pic"
+ icon-right="fa-trash-o"
+ color="negative"
+ outline
+ @click="$refs.picDropzone.removeAllFiles();pic = null;$refs.picDropzone.enable()">
+ Use another image
+ </q-btn>
+ </transition>
+ </q-stepper-navigation>
+ </q-step>
+ <q-step name="files" title="Add files">
+ <p>Now please upload your mod files (e.g. the .ocs file). You have to upload at least one file</p>
+ <q-alert color="warning"
+ enter="bounceInLeft"
+ appear>
+ Please note that you must not upload anything protected by copyrights you do not own or don't have explicit permission to upload by the owner
+ </q-alert>
+ <dropzone id="filesDropzone"
+ ref="filesDropzone"
+ @vdropzone-success="uploadSuccess"
+ :options="filesDropzoneOptions">
+ </dropzone>
+ <table class="q-table horizontal-separator" v-if="files.length > 0">
+ <thead>
+ <tr>
+ <th>Filename</th>
+ <th>ID</th>
+ <th>Size</th>
+ <th></th>
+ <th></th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="(file, idx) of files" :key="file._id">
+ <td>{{ file.filename }}</td>
+ <td>{{ file._id }}</td>
+ <td>{{ file.length | prettyBytes }}</td>
+ <td>uploaded {{ file.uploadDate|moment("from") }}</td>
+ <td><q-btn round color="negative" small icon="fa-trash" @click="deleteFile(idx)"></q-btn></td>
+ </tr>
+ </tbody>
+ </table>
+ <q-stepper-navigation>
+ <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn>
+ <q-btn color="primary" :disabled="!files.length" @click="$refs.stepper.next()">Next</q-btn>
+ </q-stepper-navigation>
+ </q-step>
+ <q-step name="final" title="Finalize upload">
+ <q-field :error="errors.copyright"
+ :error-label="errors.copyright">
+ <q-checkbox v-model="copyright"
+ label="I hereby confirm that I am the copyright owner of all content I upload, or that the copyright owner has granted me permission to do so" />
+ </q-field>
+ <q-stepper-navigation>
+ <q-btn color="primary" flat @click="$refs.stepper.previous()">Back</q-btn>
+ <q-btn color="positive" icon="fa-save" @click="postUpload" :disabled="!uploadEnabled">Save mod</q-btn>
+ </q-stepper-navigation>
+ </q-step>
+ <q-step name="done" title="Done">
+ <h6 class="text-positive" v-if="savedScenario">
+ Successfully saved your mod with the id {{ savedScenario._id }}
+ </h6>
+ <q-stepper-navigation>
+ <q-btn color="warning" icon="fa-refresh" @click="reset">Upload another one</q-btn>
+ <q-btn color="primary"
+ icon="fa-eye"
+ @click="$router.push({name: 'upload-detail', params: {uploadId: savedScenario._id}})">
+ Show the saved mod
+ </q-btn>
+ </q-stepper-navigation>
+ </q-step>
+ <q-inner-loading :visible="saving" />
+ </q-stepper>
+ </div>
+ <div v-else>
+ Please log in first
+ </div>
+ </div>
+</template>
+
+<script>
+ import Dropzone from 'vue2-dropzone'
+ import 'vue2-dropzone/dist/vue2Dropzone.css'
+
+ import {
+ LocalStorage,
+ } from 'quasar'
+
+ export default {
+ components: {
+ Dropzone,
+ },
+ data () {
+ return {
+ files: [],
+ title: '',
+ description: '',
+ slug: '',
+ errors: {},
+ pic: null,
+ savedScenario: null,
+ selectedFile: null,
+ copyright: false,
+ saving: false,
+ }
+ },
+ mounted () {
+ this.files = LocalStorage.get.item('uploadedFiles') || []
+ },
+ computed: {
+ authToken () {
+ return this.$store.state.user.authToken
+ },
+ filesDropzoneOptions () {
+ return {
+ url: `${this.$http.defaults.baseURL}/media`,
+ paramName: 'media',
+ headers: { Authorization: `Bearer ${this.$store.state.user.authToken}` },
+ acceptedFiles: '.ocs,.ocf,.ocd,.ocg,.ocr,.c4d,.c4g,.c4f,.c4r,.c4s,c4v',
+ dictDefaultMessage: "<p><i class='fa fa-3x fa-cloud-upload'></i></p><p>Drop your mod files here or click to upload</p>",
+ maxFilesize: 30, // MB
+ }
+ },
+ picDropzoneOptions () {
+ return {
+ url: `${this.$http.defaults.baseURL}/media`,
+ paramName: 'media',
+ headers: { Authorization: `Bearer ${this.$store.state.user.authToken}` },
+ acceptedFiles: '.png,.jpg',
+ dictDefaultMessage: "<p><i class='fa fa-3x fa-cloud-upload'></i></p><p>Drop your image here or click to upload</p>",
+ maxFilesize: 3, // MB
+ maxFiles: 1,
+ thumbnailWidth: null,
+ thumbnailHeight: 300,
+ createImageThumbnails: true,
+ thumbnailMethod: 'contain',
+ }
+ },
+ uploadEnabled () {
+ return this.files.length > 0 && this.copyright
+ },
+ picSrc () {
+ if (!this.pic) {
+ return ''
+ }
+ return `${this.$http.defaults.baseURL}/media/${this.pic._id}`
+ },
+ },
+ watch: {
+ files (val, oldVal) {
+ if (!oldVal || val.length !== oldVal.length) {
+ LocalStorage.set('uploadedFiles', this.files)
+ }
+ },
+ },
+ methods: {
+ deleteFile (idx) {
+ let file = this.files[idx]
+ this.files.splice(idx, 1)
+ this.$http.delete(`${this.$http.defaults.baseURL}/media/${file._id}`)
+ },
+ picUploadSuccess (file, response) {
+ this.pic = response
+ this.$refs.picDropzone.disable()
+ },
+ uploadSuccess (file, response) {
+ console.log({success: file, xhr: response})
+ this.files.push(response)
+ this.$refs.filesDropzone.removeFile(file)
+ },
+ postUpload () {
+ let that = this
+ this.saving = true
+ this.errors = {}
+ if (!this.copyright) {
+ this.errors.copyright = 'You have to accept the copyright notice'
+ this.saving = false
+ that.$refs.stepper.goToStep('final')
+ return
+ }
+ if (this.files.length === 0) {
+ this.errors.files = 'Please upload at least one file'
+ this.saving = false
+ that.$refs.stepper.goToStep('files')
+ return
+ }
+ let params = {
+ upload: {
+ title: this.title,
+ description: this.description,
+ files: this.files.map(el => el._id),
+ pic: this.pic,
+ },
+ }
+ this.$http.post('/uploads', params)
+ .then((response) => {
+ that.savedScenario = response.data
+ that.saving = false
+ that.$refs.stepper.goToStep('done')
+ })
+ .catch((error) => {
+ that.errors.slug = (error.response.data.error.errors.slug || {}).message
+ that.errors.title = (error.response.data.error.errors.title || {}).message
+ that.errors.description = (error.response.data.error.errors.description || {}).message
+ that.saving = false
+ that.$refs.stepper.goToStep('data')
+ })
+ },
+ reset () {
+ this.title = ''
+ this.description = ''
+ this.savedScenario = ''
+ this.errors = {}
+ this.pic = null
+ this.files = []
+ },
+ },
+ }
+</script>
+
+<style lang="styl" type="text/stylus" scoped>
+ .indent
+ margin: 1rem 3rem 2rem 3rem
+</style>
diff --git a/src/pages/UploadDetail.vue b/src/pages/UploadDetail.vue
new file mode 100644
index 0000000..d742a4a
--- /dev/null
+++ b/src/pages/UploadDetail.vue
@@ -0,0 +1,223 @@
+<template>
+ <div>
+ <q-btn class="pull-right" @click="$router.push({name: 'upload-list'})" outline icon="fa-list">Back to list</q-btn>
+ <div v-if="upload" class="row">
+ <q-card class="col-6">
+ <q-card-media v-if="upload.pic" overlay-position="top">
+ <q-card-title slot="overlay">
+ Mod: {{ upload.title }}
+ <span slot="subtitle">by {{ upload.author.username }}</span>
+ <span slot="right" class="text-white" style="margin-left: 3rem">updated {{ upload.updatedAt | moment("from") }}</span>
+ </q-card-title>
+ <img :src="`${$http.defaults.baseURL}/media/${upload.pic}`">
+ </q-card-media>
+ <q-card-title class="bg-positive text-white" v-else>
+ {{ upload.title }}
+ <span slot="subtitle" class="text-light">by {{ upload.author.username }}</span>
+ <span slot="right" class="text-light" style="margin-left: 3rem">updated {{ upload.updatedAt | moment("from") }}</span>
+ </q-card-title>
+ <q-card-separator />
+ <q-card-main>
+ <p class="text-faded description">
+ {{ upload.description }}
+ </p>
+ </q-card-main>
+ <q-card-actions>
+ <q-btn @click="openInOpenclonk" color="positive" outline>
+ Install mod with OpenClonk
+ </q-btn>
+ <q-btn v-if="$store.state.user.decodedToken.username === upload.author.username"
+ outline
+ color="negative"
+ icon="fa-trash-o"
+ @click="deleteUpload(upload)">
+ Delete mod
+ </q-btn>
+ </q-card-actions>
+ <q-card-media v-if="upload.pic" overlay-position="bottom">
+ <q-card-title slot="overlay">
+ Voting
+ </q-card-title>
+ <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150">
+ </q-parallax>
+ </q-card-media>
+ <q-card-title v-else>
+ Voting
+ </q-card-title>
+ <q-card-main>
+ <div class="group">
+ <upload-voter :upload="upload" @voted="refresh"></upload-voter>
+ </div>
+ </q-card-main>
+ <q-card-media v-if="upload.pic" overlay-position="bottom">
+ <q-card-title slot="overlay">
+ Dependencies
+ </q-card-title>
+ <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150">
+ </q-parallax>
+ </q-card-media>
+ <q-card-title v-else>
+ Dependencies
+ </q-card-title>
+ <q-card-main>
+ <q-btn disabled flat v-if="upload.dependency.length === 0">
+ No dependencies
+ </q-btn>
+ <div class="group" v-else>
+ <div v-for="d of upload.dependency"
+ :key="d._id">
+ <q-btn @click="$router.push({name: 'upload-detail', params: {uploadId: d._id}})" no-caps outline>
+ {{ d.title }}
+ </q-btn>
+ </div>
+ </div>
+ </q-card-main>
+ <q-card-media v-if="upload.pic" overlay-position="bottom">
+ <q-card-title slot="overlay">
+ File downloads
+ </q-card-title>
+ <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150">
+ </q-parallax>
+ </q-card-media>
+ <q-card-title v-else>
+ File downloads
+ </q-card-title>
+ <q-card-main>
+ <q-btn disabled flat v-if="upload.file.length === 0">
+ No files
+ </q-btn>
+ <div class="group"
+ v-else
+ v-for="fid of upload.file"
+ :key="fid._id">
+ <q-btn loader
+ no-caps
+ color="primary"
+ :percentage="(downloadProgresses[fid._id] || {}).percentage"
+ @click="(event, done) => {downloadMedia(fid._id, fid.filename, done)}">
+ {{ fid.filename }} ({{ fid.length|prettyBytes }})
+ <span slot="loading">Downloading...</span>
+ </q-btn>
+ <span v-if="downloadProgresses[fid._id]">
+ <q-transition enter="fadeIn" leave="fadeOut" mode="out-in">
+ <span key="sizeDownloaded" v-if="downloadProgresses[fid._id].percentage < 100">
+ {{ downloadProgresses[fid._id].loaded|prettyBytes }} / {{ downloadProgresses[fid._id].total|prettyBytes }}
+ </span>
+ <span key="downloadDone" v-else><i class="fa fa-check fa-2x text-positive"></i></span>
+ </q-transition>
+ </span>
+ </div>
+ </q-card-main>
+ <q-card-media v-if="upload.pic" overlay-position="bottom">
+ <q-card-title slot="overlay">
+ Other data
+ </q-card-title>
+ <q-parallax :src="`${$http.defaults.baseURL}/media/${upload.pic}`" :height="150">
+ </q-parallax>
+ </q-card-media>
+ <q-card-title v-else>
+ Other data
+ </q-card-title>
+ <q-card-main>
+ <p>ID: {{ upload._id }}</p>
+ </q-card-main>
+ </q-card>
+ </div>
+ <div v-else>
+ <q-spinner size="50"></q-spinner> Loading mod data...
+ </div>
+ </div>
+</template>
+
+<script>
+ import {
+ Dialog,
+ openURL,
+ } from 'quasar'
+ import UploadVoter from 'components/UploadVoter'
+ import FileSaver from 'file-saver'
+
+ export default {
+ components: {
+ UploadVoter,
+ },
+ computed: {
+ routeId () {
+ return this.$route.params.uploadId
+ },
+ },
+ watch: {
+ routeId: {
+ handler (val, oldVal) {
+ if (val && val !== oldVal) {
+ this.refresh()
+ }
+ },
+ immediate: true,
+ },
+ },
+ data () {
+ return {
+ upload: null,
+ downloadProgresses: {},
+ }
+ },
+ methods: {
+ refresh () {
+ let that = this
+ this.$http.get(`/uploads/${this.routeId}`).then(response => {
+ that.upload = response.data
+ })
+ },
+ deleteUpload () {
+ let that = this
+ Dialog.create({
+ title: 'Delete mod?',
+ message: `Do you really want to delete the mod ${this.upload.title}?<br>This cannot be undone!`,
+ buttons: [
+ 'Cancel',
+ {
+ label: '<i class="fa fa-trash-o"></i> Yes, delete!',
+ color: 'negative',
+ outline: true,
+ handler () {
+ that.$http.delete(`/uploads/${that.routeId}`).then(response => that.$router.push({name: 'upload-list'}))
+ }
+ }
+ ]
+ })
+ },
+ downloadMedia (mediaId, filename, done) {
+ console.log({mediaId, done})
+ let that = this
+ this.$set(this.downloadProgresses, mediaId, {})
+ this.$set(this.downloadProgresses[mediaId], 'done', done)
+ this.$set(this.downloadProgresses[mediaId], 'percentage', 0)
+ this.$set(this.downloadProgresses[mediaId], 'loaded', 0)
+ this.$set(this.downloadProgresses[mediaId], 'total', 0)
+ this.$http.get(
+ `${this.$http.defaults.baseURL}/media/${mediaId}`,
+ {
+ responseType: 'blob',
+ onDownloadProgress: progressEvent => { that.downloadProgress(mediaId, progressEvent) },
+ }
+ ).then((response) => {
+ FileSaver.saveAs(response.data, filename)
+ that.downloadProgresses[mediaId].done()
+ that.downloadProgresses[mediaId].percentage = 100
+ })
+ },
+ downloadProgress (mediaId, progressEvent) {
+ this.downloadProgresses[mediaId].percentage = progressEvent.loaded * 100 / progressEvent.total
+ this.downloadProgresses[mediaId].loaded = progressEvent.loaded
+ this.downloadProgresses[mediaId].total = progressEvent.total
+ },
+ openInOpenclonk () {
+ openURL(`openclonk://installmod/${this.upload._id}`)
+ }
+ }
+ }
+</script>
+
+<style lang="styl" type="text/stylus" scoped>
+</style>
diff --git a/src/pages/UploadList.vue b/src/pages/UploadList.vue
new file mode 100644
index 0000000..a8a731c
--- /dev/null
+++ b/src/pages/UploadList.vue
@@ -0,0 +1,119 @@
+<template>
+ <div>
+ <transition :enter="showList ? 'fadeInLeft' : 'fadeInRight'" :leave="showList ? 'fadeOutRight' : 'fadeOutLeft'" mode="out-in" duration="500">
+ <div v-if="showList">
+ <h4>Available Mods</h4>
+ <q-table
+ no-data-label="No mods available"
+ :data="uploads"
+ :config="tableConfig"
+ :columns="tableColumns"
+ @refresh="refresh">
+ <template slot='col-action' slot-scope='cell'>
+ <q-btn color="primary"
+ small
+ outline
+ icon="fa-info-circle"
+ @click="$router.push({name: 'upload-detail', params: {uploadId: cell.row._id}})">
+ Details</q-btn>
+ <q-btn color="negative"
+ outline
+ small
+ icon="fa-trash-o"
+ v-if="cell.row.author.username === $store.state.user.decodedToken.username"
+ @click="deleteUpload(cell.row)">
+ Delete</q-btn>
+ </template>
+ <template slot='col-voting' slot-scope='cell'>
+ {{ cell.data.sum }} <i class="fa" :class="{'fa-caret-up text-positive': cell.data.sum > 0, 'fa-caret-down text-negative': cell.data.sum < 0, 'fa-sort text-dark': cell.data.sum === 0}"></i>
+ </template>
+ </q-table>
+ </div>
+ <router-view></router-view>
+ </transition>
+ </div>
+</template>
+
+<script>
+ import {
+ Dialog,
+ } from 'quasar'
+ import Truncate from 'vue-truncate-collapsed'
+ import moment from 'moment'
+
+ export default {
+ components: {
+ Truncate,
+ },
+ computed: {
+ showList () {
+ return this.$route.name === 'upload-list'
+ }
+ },
+ watch: {
+ showList: {
+ handler (val, oldVal) {
+ if (val && val !== oldVal) {
+ this.refresh()
+ }
+ },
+ immediate: true,
+ }
+ },
+ data () {
+ return {
+ response: {},
+ uploads: [],
+ tableConfig: {
+ refresh: true,
+ leftStickyColumns: 1,
+ rightStickyColumns: 3,
+ rowHeight: '60px',
+ },
+ tableColumns: [
+ {label: 'Title', field: 'title', width: '100px'},
+ {label: 'Description', field: 'description', width: '500px'},
+ {label: 'Author', field: 'author', width: '100px', format: el => el.username},
+ {label: 'Voting', field: 'voting', width: '100px'},
+ {label: 'Last update', field: 'updatedAt', width: '100px', format: el => moment(el).from()},
+ {label: 'Actions', field: 'action', width: '200px'},
+ ],
+ }
+ },
+ methods: {
+ refresh (done) {
+ let that = this
+ this.$http.get('/uploads').then((response) => {
+ that.response = response
+ that.uploads = response.data.uploads
+ if (done) {
+ done()
+ }
+ })
+ },
+ deleteUpload (upload) {
+ let that = this
+ Dialog.create({
+ title: 'Delete mod?',
+ message: `Do you really want to delete the mod ${upload.title}?<br>This cannot be undone!`,
+ buttons: [
+ 'Cancel',
+ {
+ label: '<i class="fa fa-trash-o"></i> Yes, delete!',
+ color: 'negative',
+ outline: true,
+ handler () {
+ that.$http.delete(`/uploads/${upload._id}`).then(response => that.refresh())
+ }
+ }
+ ]
+ })
+ },
+ },
+ }
+</script>
+
+<style lang="styl" type="text/stylus" scoped>
+ .description
+ max-width: 400px
+</style>