diff options
| author | Arne Schauf <git@asw.io> | 2018-03-03 01:06:44 +0100 |
|---|---|---|
| committer | Arne Schauf <git@asw.io> | 2018-03-03 01:08:55 +0100 |
| commit | 8769fc4dc19447f67a42eb941fb43ba7223772eb (patch) | |
| tree | 0a73daee2ede4cc1cdf515559429c9ba9fcc211c /src/pages | |
| download | parry-ui-8769fc4dc19447f67a42eb941fb43ba7223772eb.tar.gz parry-ui-8769fc4dc19447f67a42eb941fb43ba7223772eb.zip | |
upgrade quasar to latest version
Diffstat (limited to 'src/pages')
| -rw-r--r-- | src/pages/404.vue | 16 | ||||
| -rw-r--r-- | src/pages/FetchToken.vue | 68 | ||||
| -rw-r--r-- | src/pages/UploadCreate.vue | 264 | ||||
| -rw-r--r-- | src/pages/UploadDetail.vue | 223 | ||||
| -rw-r--r-- | src/pages/UploadList.vue | 119 |
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> |
