Move post uploader to vue.js and inline it

This commit is contained in:
Kira 2019-07-11 20:19:56 -07:00
parent ac2de04d9e
commit 1636b62c3e
18 changed files with 1313 additions and 1094 deletions

View File

@ -52,7 +52,13 @@ class UploadsController < ApplicationController
flash[:notice] = @service.warnings.join(".\n \n")
end
respond_with(@upload)
respond_with(@upload) do |format|
format.json do
return render json: {success: true, location: post_path(@upload.post_id), post_id: @upload.post_id} unless @upload.is_errored?
return render json: {success: false, reason: 'duplicate', location: post_path(@upload.duplicate_post_id), post_id: @upload.duplicate_post_id}, status: 412 if @upload.is_duplicate?
return render json: {success: false, reason: 'invalid', message: @upload.sanitized_status}, status: 412 if @upload.is_errored?
end
end
end
private

View File

@ -45,4 +45,5 @@ export { default as Upload } from '../src/javascripts/uploads.js';
export { default as Utility } from '../src/javascripts/utility.js';
export { default as Ugoira } from '../src/javascripts/ugoira.js';
export { default as Takedown } from '../src/javascripts/takedowns.js';
export { default as Thumbnails } from '../src/javascripts/thumbnails.js';
export { default as Thumbnails } from '../src/javascripts/thumbnails.js';
export { default as Uploader } from '../src/javascripts/uploader.js';

View File

@ -0,0 +1,12 @@
import Uploader from './uploader.vue';
import Vue from 'vue';
export default {
init() {
const app = new Vue({
render: (h) => h(Uploader)
});
app.$mount('#uploader');
}
}

View File

@ -0,0 +1,898 @@
<template>
<div class="flex-grid-outer">
<div class="col box-section" style="flex: 2 0 0;">
<div class="the_secret_switch" @click="normalMode = !normalMode"></div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_file">Image</label>
</div>
<div class="col2">
<div v-if="!disableFileUpload">
<label>File:
<input type="file" ref="post_file" @change="updatePreview" :disabled="disableFileUpload"/>
</label>
<button @click="clearFile" v-show="disableURLUpload">Clear</button>
</div>
<div v-if="!disableURLUpload">
<div class="box-section sect_red" v-if="badDirectURL">
The direct URL entered has the following problem: {{ directURLProblem }}<br>
You should review <a href="/wiki/show/howto:sites_and_sources">the sourcing guide</a>.
</div>
<label>{{!disableFileUpload ? '(or) ' : '' }}URL:
<input type="text" size="50" v-model="uploadURL" @keyup="updatePreview"
:disabled="disableURLUpload"/>
</label>
<div id="whitelist-warning" v-show="whitelist.visible"
:class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span v-if="whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are permitted.</span>
<span v-if="!whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are not permitted. (<a
href='/upload_whitelist'>View whitelisted domains</a>)</span>
</div>
</div>
</div>
</div>
<div class="box-section upload_preview_container in-editor below-upload">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;"/>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_sources">Sources</label>
<div>You should include: A link to the artists page where this was obtained, and a link to the
submission page
where this image was obtained. No available source should ONLY be used if the content has never
been posted
online anywhere else.
</div>
</div>
<div class="col2">
<div class="box-section sect_red" v-show="showErrors && sourceWarning">A source must be provided or
you must
select that there
is no available source.
</div>
<div v-if="!noSource">
<image-source :last="i === (sources.length-1)" :index="i" v-model="sources[i]"
v-for="s, i in sources"
@delete="removeSource(i)" @add="addSource" :key="i"></image-source>
</div>
<div>
<label class="section-label"><input type="checkbox" id="no_source" v-model="noSource"/> No
available source
/ I am the source.</label>
</div>
</div>
</div>
<template v-if="normalMode">
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="names">Artists</label>
</div>
<div class="col2">
<div>
<textarea class="tag-textarea" v-model="tagEntries.character" id="post_characters" rows="2"
placeholder="Ex: artist_name etc."></textarea>
</div>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected"
v-for="check in checkboxes.artist" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_sex_tags">Characters</label>
<div>Select (and write in) all that apply. Character sex is based only on what is visible in the
image.
Outside information or other images should not be used when deciding what tags are used.
</div>
</div>
<div class="col2">
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.sex"
@set="setCheck"
:key="check.name"></image-checkbox>
</div>
<hr>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected"
v-for="check in checkboxes.count" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<hr>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected"
v-for="check in checkboxes.pairing" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<textarea class="tag-textarea" rows="2" v-model="tagEntries.sex" id="post_sexes"
placeholder="Ex: character_name solo_focus etc."></textarea>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Body Types and Species</label>
</div>
<div class="col2">
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected"
v-for="check in checkboxes.body" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<textarea class="tag-textarea" rows="2" v-model="tagEntries.bodyType" id="post_bodyTypes"
placeholder="Ex: bear dragon hyena rat newt etc."></textarea>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Contentious Content</label>
<div>
These allow users to find or blacklist content with ease. Make sure that you are tagging
these upon
initial upload.
</div>
</div>
<div class="col2">
<textarea class="tag-textarea" v-model="tagEntries.theme" id="post_themes" rows="2"
placeholder="Ex: cub young gore scat watersports diaper my_little_pony vore not_furry rape etc."></textarea>
</div>
</div>
</template>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_rating_questionable">Rating</label>
<div>Explicit tags include sex, pussy, penis, masturbation, fellatio, etc.
(<%= link_to "help", help_pages_path(title: 'ratings'), target: "_blank" %>)
</div>
</div>
<div class="col2">
<div class="box-section sect_red" v-if="showErrors && invalidRating">
You must select an appropriate rating for this image.
</div>
<div>
<template v-if="!safe">
<button class="toggle-button" :class="{active: rating==='e'}" @click="rating = 'e'">
Explicit
</button>
<button class="toggle-button" :class="{active: rating==='q'}" @click="rating = 'q'">
Questionable
</button>
</template>
<button class="toggle-button" :class="{active: rating==='s'}" @click="rating = 's'">Safe
</button>
</div>
</div>
</div>
<div class="flex-grid come-together-now">
<div class="col">
<label class="section-label" for="post_tags">Other Tags</label>
<div>
Separate tags with spaces. (<%= link_to "help", help_pages_path(title: 'tags'), target: "_blank"
%>)
</div>
</div>
<div class="col2">
<div class="box-section upload_preview_container in-editor">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;"/>
</div>
<div class="box-section sect_red" v-show="showErrors && notEnoughTags">
You must provide at least <b>{{4 - tagCount}}</b> more tags. Tags in other sections count
towards this
total.
</div>
<div v-show="!preview.show">
<textarea class="tag-textarea" id="post_tags" v-model="tagEntries.other" rows="5"
ref="otherTags"></textarea>
</div>
<div v-show="preview.show">
<tag-preview :tags="preview.tags" :loading="preview.loading"
@close="previewFinalTags"></tag-preview>
</div>
<div class="related-tag-functions">
<a href="#" @click.prevent="findRelated()">Related Tags</a> |
<a href="#" @click.prevent="findRelated('artist')">Related Artists</a> |
<a href="#" @click.prevent="findRelated('char')">Related Characters</a> |
<a href="#" @click.prevent="findRelated('copyright')">Related Copyrights</a> |
<a href="#" @click.prevent="previewFinalTags">Preview Final Tags</a>
</div>
</div>
</div>
<div class="flex-grid border-bottom over-me">
<related-tags :tags="tagsArray" :related="relatedTags" :loading="loadingRelated"
@tag-active="pushTag"></related-tags>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Parent Post ID</label>
</div>
<div class="col2">
<input type="number" v-model.number="parentID" placeholder="Ex. 12345"/>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_description">Description</label>
</div>
<div class="col2">
<textarea class="tag-textarea" id="post_description" v-model="description" rows="5"></textarea>
</div>
</div>
<div class="flex-grid">
<div class="col"></div>
<div class="col2">
<div class="box-section sect_red" v-show="preventUpload && showErrors">
Unmet requirements above prevent the submission of the post.
</div>
<div class="box-section sect_green" v-show="submitting">
Submitting your post, please wait.
</div>
<div class="box-section sect_red" v-show="error">
{{ error }}
</div>
<div class="box-section sect_red" v-show="duplicateId">
Post is a duplicate of <a :href="duplicatePath">post #{{duplicateId}}.</a>
</div>
<button @click="submit" :disabled="(showErrors && preventUpload) || submitting" accesskey="s">{{
submitting ? 'Uploading...' :
'Upload' }}
</button>
</div>
</div>
</div>
<div id="preview-sidebar" class="col box-section" style="margin-left: 10px; padding: 10px;">
<div class="upload_preview_container in-sidebar">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;" @load="updatePreviewDims"
@error="previewError"/>
</div>
</div>
</div>
</template>
<style>
.toggle-button {
box-sizing: border-box;
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: center;
user-select: none;
padding: .175em .5rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
margin-right: 5px;
}
.toggle-button.active {
background-color: #FDF5D9;
}
.flex-grid-outer {
display: flex;
padding: 10px 0;
}
.upload_preview_container.in-editor {
display: none;
}
.upload_preview_container.in-sidebar {
position: sticky;
top: 20px;
}
.flex-grid {
display: flex;
padding: 10px 0;
}
.flex-wrap {
display: flex;
flex-wrap: wrap;
}
.col {
flex: 1 0 0;
margin-right: 5px;
}
.col2 {
flex: 2 1 0;
}
.border-bottom {
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}
.section-label {
white-space: normal;
}
.come-together-now {
padding-bottom: 0;
}
.over-me {
padding-top: 0;
}
.related-section {
display: flex;
flex-direction: row;
flex: 0 1 10%;
padding: 5px 10px;
}
.related-items {
display: flex;
flex-direction: column;
margin: 0 -5px;
padding: 0 5px;
}
.related-item {
padding: 0 5px;
max-width: 140px;
word-wrap: break-word;
overflow-wrap: break-word;
}
.related-title {
padding: 0 5px;
font-weight: bold;
}
.tag-active {
background: rgb(0, 111, 250);
color: white;
}
.tag-preview {
border: 1px solid rgba(0, 0, 0, 0.15);
background: rgba(1, 1, 1, 0.15);
border-radius: 2px;
padding: 3px;
margin-right: 5px;
box-sizing: border-box;
}
.tag-preview-alias {
background-color: rgba(150, 0, 0, 0.25);
}
.tag-preview-implication {
background-color: rgba(0, 150, 0, 0.25);
}
.tag-textarea {
display: inline-block; /* Why were we even unsetting this? It breaks EVERYTHING. */
font-size: 1rem;
width: 100%;
resize: vertical;
}
/* Need to override this so it shows up at all. */
#whitelist-warning {
display: block;
float: none;
}
@media only screen and (orientation: portrait), (max-width: 1100px) {
#preview-sidebar {
display: none;
}
.upload_preview_container.in-editor {
display: flex;
flex-direction: column-reverse;
}
.upload_preview_container.in-sidebar {
display: none;
}
.upload_preview_dims {
text-align: center;
}
.upload_preview_img {
display: block;
margin-left: auto;
margin-right: auto;
max-height: 500px;
}
.below-upload > .upload_preview_img {
max-height: 150px;
}
.related-section {
flex: 0 1 50%;
}
.flex-grid {
flex-direction: column;
}
input {
max-width: 90%;
}
}
.the_secret_switch {
height: 10px;
width: 100%;
}
</style>
<script>
import Vue from 'vue';
import source from './uploader_source.vue';
import checkbox from './uploader_checkbox.vue';
import relatedTags from './uploader_related.vue';
import tagPreview from './uploader_preview.vue';
const thumbURLs = [
"/images/notfound-preview.png",
"/images/download-preview.png",
"/images/webm-preview.png",
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
];
const thumbs = {
webm: "/images/webm-preview.png",
flash: "/images/download-preview.png",
notfound: "/images/notfound-preview.png",
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
};
const artist_checks = [
{name: 'Unknown Artist'},
{name: 'Anonymous Artist'}];
const sex_checks = [
{name: 'Male'},
{name: 'Female'},
{name: 'Andromorph'},
{name: 'Gynomorph'},
{name: 'Hermaphrodite', tag: 'herm'},
{name: 'Male-Herm', tag: 'maleherm'},
{name: 'Ambiguous', tag: 'ambiguous_gender'}];
const pairing_checks = [
{name: 'Male/Male'},
{name: 'Male/Female'},
{name: 'Female/Female'},
{name: 'Intersex/Male'},
{name: 'Intersex/Female'},
{name: 'Intersex/Intersex'}
];
const char_count_checks = [
{name: 'Solo'},
{name: 'Duo'},
{name: 'Group'},
{name: 'Zero Pictured'}];
const body_type_checks = [
{name: 'Anthro'},
{name: 'Feral'},
{name: 'Humanoid'},
{name: 'Human'},
{name: 'Taur'}];
function updatePreviewDims(e) {
var img = e.target;
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
this.previewHeight = img.naturalHeight;
this.previewWidth = img.naturalWidth;
this.overDims = (img.naturalHeight > 15000 || img.naturalWidth > 15000);
updateDimensionTag.call(this);
}
function previewError() {
this.previewWidth = this.previewHeight = 0;
this.overDims = false;
if (this.uploadURL === '' && !this.$refs['post_file']) {
this.previewURL = thumbs.none;
} else {
this.previewURL = thumbs.notfound;
}
}
function updatePreviewFile() {
var self = this;
var reader = new FileReader();
var file = this.$refs['post_file'].files[0];
this.previewHeight = 0;
this.previewWidth = 0;
reader.onload = function (e) {
var src = e.target.result;
if (file.type.match('video/webm'))
src = thumbs.webm;
else if (file.type.match('application/x-shockwave-flash'))
src = thumbs.flash;
self.previewURL = src;
};
reader.readAsDataURL(file);
this.disableURLUpload = true;
}
function updatePreviewURL() {
var self = this;
if (this.uploadURL.length === 0 || (this.$refs['post_file'] && this.$refs['post_file'].files.length > 0)) {
this.disableFileUpload = false;
this.oldDomain = '';
return;
}
this.disableFileUpload = true;
var domain = $j("<a>").prop("href", this.uploadURL).prop("hostname");
if (domain && domain != this.oldDomain) {
$j.getJSON("/upload_whitelist/is_whitelisted", {url: this.uploadURL}, function (data) {
if (data.domain)
self.whitelistWarning(data.is_whitelisted, data.domain);
});
}
this.oldDomain = domain;
var src = thumbs.none;
if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(jpg|jpeg|gif|png)/))
src = this.uploadURL;
else if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(swf)$/))
src = thumbs.flash;
else if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(webm)$/))
src = thumbs.webm;
this.previewURL = src;
}
function updateDimensionTag() {
var self = this;
if (!(self.previewHeight || self.previewWidth))
return;
var otherTags = ['low_res', 'hi_res', 'superabsurd_res', 'absurd_res'];
var ourTag = function (h, w) {
if (!(h && w)) {
return null;
}
if ((h <= 500) && (w <= 500))
return 'low_res';
if ((h >= 10000) || (w >= 10000))
return 'superabsurb_res';
if ((h >= 2400) || (w >= 3200))
return 'absurd_res';
if ((h >= 1200) || (w >= 1600))
return 'hi_res';
return null;
}(self.previewHeight, self.previewWidth);
var tagIdx = otherTags.indexOf(ourTag);
if (tagIdx > 0)
otherTags.splice(tagIdx, 1);
if (ourTag)
self.pushTag(ourTag, true);
for (var i = 0; i < otherTags.length; i++) {
self.pushTag(otherTags[i], false);
}
}
function updatePreview() {
if (this.$refs['post_file'] && this.$refs['post_file'].files[0])
updatePreviewFile.call(this);
else
updatePreviewURL.call(this);
}
function directURLCheck(url) {
var patterns = [{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
{reason: 'Sample URL', test: /pximg\.net\/img-master/gi},
// {reason: 'Sample URL', test: /\d+\.media\.tumblr\.com/gi}, // Tumblr broke raws.
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/\w+\.(jpg|png)(:large)?$/gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/\w+\?format=/gi},
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
for (var i = 0; i < patterns.length; ++i) {
var pattern = patterns[i];
if (pattern.test.test(url))
return pattern.reason;
}
return '';
}
function clearFileUpload() {
if (!(this.$refs['post_file'] && this.$refs['post_file'].files[0]))
return;
this.$refs['post_file'].value = null;
this.disableURLUpload = this.disableFileUpload = false;
this.previewURL = thumbs.none;
this.previewHeight = this.previewWidth = 0;
this.updatePreview();
}
export default {
components: {
'image-source': source,
'image-checkbox': checkbox,
'related-tags': relatedTags,
'tag-preview': tagPreview
},
data() {
var allChecks = {};
var addChecks = function (check) {
if (typeof check['tag'] !== "undefined") {
allChecks[check.tag] = true;
return
}
allChecks[check.name.toLowerCase().replace(' ', '_')] = true;
};
artist_checks.forEach(addChecks);
sex_checks.forEach(addChecks);
pairing_checks.forEach(addChecks);
char_count_checks.forEach(addChecks);
body_type_checks.forEach(addChecks);
return {
safe: window.safeSite,
showErrors: false,
whitelist: {
visible: false,
allowed: false,
domain: ''
},
submitting: false,
disableFileUpload: false,
disableURLUpload: false,
previewHeight: 0,
previewWidth: 0,
overDims: false,
uploadURL: '',
previewURL: thumbs.none,
oldDomain: '',
noSource: false,
sources: [''],
normalMode: true,
checkboxes: {
artist: artist_checks,
sex: sex_checks,
pairing: pairing_checks,
count: char_count_checks,
body: body_type_checks,
selected: {},
all: allChecks
},
tagEntries: {
character: '',
sex: '',
bodyType: '',
theme: '',
other: ''
},
preview: {
loading: false,
show: false,
tags: []
},
relatedTags: [],
loadingRelated: false,
parentID: '',
description: '',
rating: '',
error: '',
duplicateId: 0
};
},
methods: {
updatePreview: updatePreview,
updatePreviewDims: updatePreviewDims,
previewError: previewError,
clearFile: clearFileUpload,
whitelistWarning(allowed, domain) {
this.whitelist.allowed = allowed;
this.whitelist.domain = domain;
this.whitelist.visible = true;
},
removeSource(i) {
this.sources.splice(i, 1);
},
addSource() {
if (this.sources.length < 5)
this.sources.push('');
},
setCheck(tag, value) {
Vue.set(this.checkboxes.selected, tag, value);
},
submit() {
this.showErrors = true;
this.error = '';
if (this.preventUpload || this.submitting)
return;
var self = this;
this.submitting = true;
var data = new FormData();
var post_file = this.$refs['post_file'];
if (post_file && post_file.files && post_file.files.length) {
data.append('upload[file]', this.$refs['post_file'].files[0]);
} else {
data.append('upload[direct_url]', this.uploadURL);
}
data.append('upload[tag_string]', this.tags);
data.append('upload[rating]', this.rating);
data.append('upload[source]', this.sources.join('\n'));
// data.append('upload[description]', this.description);
data.append('upload[parent_id]', this.parentID);
jQuery.ajax('/uploads.json', {
contentType: false,
processData: false,
method: 'POST',
type: 'POST',
data: data,
success(data) {
self.submitting = false;
Danbooru.notice('Post uploaded successfully.');
location.assign(data.location);
},
error(data) {
self.submitting = false;
const data2 = data.responseJSON;
try {
if (data2 && data2.reason === 'duplicate') {
self.duplicateId = data2.post_id;
} else {
self.error = 'Error: ' + data2.messages.join('; ');
}
} catch (e) {
self.error = 'Error: Unknown error! ' + data2;
}
}
});
},
pushTag(tag, add) {
this.preview.show = false;
var isCheck = typeof this.checkboxes.all[tag] !== "undefined";
// In advanced mode we need to push these into the tags area because there are no checkboxes or other
// tag fields so we can't see them otherwise.
if (isCheck && this.normalMode) {
this.setCheck(tag, add);
return;
}
var tags = this.tagEntries.other ? this.tagEntries.other.trim().split(' ') : [];
var tagIdx = tags.indexOf(tag);
if (add) {
if (tagIdx === -1)
tags.push(tag);
} else {
if (tagIdx === -1)
return;
tags.splice(tagIdx, 1);
}
this.tagEntries.other = tags.join(' ') + ' ';
},
previewFinalTags() {
if (this.preview.loading)
return;
if (this.preview.show) {
this.preview.show = false;
return;
}
this.preview.loading = true;
this.preview.show = true;
this.preview.tags = [];
var self = this;
var data = {tags: this.tags};
jQuery.ajax("/tags/preview.json", {
method: 'POST',
type: 'POST',
data: data,
success: function (result) {
self.preview.loading = false;
self.preview.tags = result;
},
error: function (result) {
self.preview.loading = false;
self.preview.tags = [];
self.preview.show = false;
Danbooru.error('Error loading tag preview ' + result);
}
})
},
findRelated(type) {
const self = this;
const convertResponse = function (respData) {
var sortedRelated = [];
for (var key in respData) {
if (!respData.hasOwnProperty(key)) {
continue;
}
sortedRelated.push({title: 'Related: ' + key, tags: respData[key].sort(tagSorter)});
}
return sortedRelated;
};
const getSelectedTags = function () {
const field = self.$refs['otherTags'];
if (!field.hasOwnProperty('selectionStart'))
return null;
const length = field.selectionEnd - field.selectionStart;
if (length)
return field.value.substr(field.selectionStart, length);
return null;
};
this.loadingRelated = true;
this.relatedTags = [];
const selectedTags = getSelectedTags();
const params = selectedTags ? {query: selectedTags} : {query: this.tags};
if (type)
params['category'] = type;
$.getJSON("/related_tag/bulk.json", params, function (data) {
self.relatedTags = convertResponse(data);
}).always(function () {
self.loadingRelated = false;
});
}
},
computed: {
tags() {
var self = this;
var checked = Object.keys(this.checkboxes.selected).filter(function (x) {
return self.checkboxes.selected[x] === true;
});
return checked.concat([this.tagEntries.other, this.tagEntries.sex, this.tagEntries.bodyType,
this.tagEntries.theme, this.tagEntries.character]).join(' ').replace(',', ' ').trim().replace(/ +/g, ' ');
},
tagsArray() {
return this.tags.toLowerCase().split(' ');
},
previewDimensions() {
if (this.previewWidth && this.previewHeight)
return this.previewHeight + '×' + this.previewWidth;
return '';
},
directURLProblem: function () {
return directURLCheck(this.uploadURL);
},
badDirectURL: function () {
return !!this.directURLProblem;
},
sourceWarning: function () {
var validSourceCount = this.sources.filter(function (i) {
return i.length > 0;
}).length;
return !this.noSource && (validSourceCount === 0);
},
tagCount: function () {
return this.tags.split(' ').filter(function (x) {
return x;
}).length;
},
notEnoughTags: function () {
return this.tagCount < 4;
},
invalidRating: function () {
return !this.rating;
},
preventUpload: function () {
return this.sourceWarning || this.badDirectURL || this.notEnoughTags
|| this.invalidRating;
},
duplicatePath: function () {
return `/posts/${this.duplicateId}`;
}
}
}
</script>

View File

@ -0,0 +1,24 @@
<template>
<button class="toggle-button" :class="{active: value}" @click="value = !value">{{check.name}}</button>
</template>
<script>
export default {
props: ['check', 'checks'],
computed: {
value: {
get() {
if (this.checks[this.tagName] === undefined)
return false;
return this.checks[this.tagName];
},
set(v) {
this.$emit('set', this.tagName, v);
}
},
tagName() {
return this.check.tag || this.check.name.toLowerCase().replace(/ /g, '_');
}
}
}
</script>

View File

@ -0,0 +1,70 @@
<template>
<div>
<div v-show="loading">Fetching tags...</div>
<div class="related-tags flex-wrap">
<div class="related-items" v-for="sTags, i in splitTags" :key="i">
<tag-preview v-for="tag, $idx in sTags" :key="$idx" :tag="tag"></tag-preview>
</div>
</div>
<div>
<a href="#" @click.prevent="close">Close Preview</a>
</div>
</div>
</template>
<script>
import Vue from 'vue';
const tagPreviewTag = Vue.extend({
functional: true,
props: ['tag'],
render: function (h, ctx) {
var tag = ctx.props.tag;
switch (tag.type) {
default:
case 'tag':
return h('a', {
staticClass: 'tag-preview tag-type-' + tag.tagType
}, tag.a);
case 'alias':
return h('span', {staticClass: 'tag-preview tag-preview-alias'}, [
h('del', undefined, [
h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.a)
]), ' → ', h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.b)
]);
case 'implication':
return h('span', {staticClass: 'tag-preview tag-preview-implication'}, [
h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.a), ' ⇐ ', h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.b)
]);
}
}
});
export default {
props: ['tags', 'loading'],
components: {
'tag-preview': tagPreviewTag
},
methods: {
close: function () {
this.$emit('close');
}
},
computed: {
splitTags: function () {
var newTags = this.tags.concat([]);
newTags.sort(function (a, b) {
return a.a === b.a ? 0 : (a.a < b.a ? -1 : 1);
});
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(newTags, 15);
}
}
}
</script>

View File

@ -0,0 +1,89 @@
<template>
<div class="related-tags flex-wrap">
<div class="related-section" v-for="group in tagGroups" :key="group.title">
<div class="related-items" v-for="tags, i in splitTags(group.tags)" :key="i">
<div class="related-title" v-if="i === 0">{{group.title}}</div>
<div class="related-item" v-for="tag in tags" :key="tag[0]">
<a :class="tagClasses(tag)" :href="tagLink(tag)" @click.prevent="toggle(tag)">{{tag[0]}}</a>
</div>
</div>
</div>
</div>
</template>
<script>
function tagSorter(a, b) {
return a[0] > b[0] ? 1 : -1;
}
export default {
props: ['tags', 'related', 'loading'],
data: function () {
return {
uploaded: (window.userUploadTags || []),
recent: (window.userRecentTags || []).sort(tagSorter),
artists: (window.userArtistTags || []).sort(tagSorter)
};
},
methods: {
toggle: function (tag) {
this.$emit('tag-active', tag[0], !this.tagActive(tag));
},
tagLink: function (tag) {
return '/post/index?tags=' + encodeURIComponent(tag[0]);
},
tagActive: function (tag) {
return this.tags.indexOf(tag[0]) !== -1;
},
tagClasses: function (tag) {
var classes = {'tag-active': this.tagActive(tag)};
classes['tag-type-' + tag[2]] = true;
return classes;
},
splitTags: function (tags) {
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(tags, 15);
}
},
computed: {
tagGroups: {
get: function () {
var groups = [];
if (this.uploaded && this.uploaded.length) {
groups.push({
title: "Quick Tags",
tags: this.uploaded
});
}
if (this.recent && this.recent.length) {
groups.push({
title: "Recent",
tags: this.recent
});
}
if (this.artists && this.artists.length) {
groups.push({
title: "Artists",
tags: this.artists
});
}
if (this.related && this.related.length) {
for (var i = 0; i < this.related.length; i++) {
groups.push(this.related[i]);
}
}
if (this.loading) {
groups.push({title: 'Loading Related Tags', tags: [['', '', '']]});
}
return groups;
}
}
}
}
</script>

View File

@ -0,0 +1,42 @@
<template>
<div>
<input type="text" size="50" v-model="realValue" @keyup.enter="add"/>
<button @click="remove" v-if="index !== 0">-</button>
<button @click="add" v-if="last && index < 4">+</button>
</div>
</template>
<script>
export default {
props: ['value', 'index', 'last'],
data() {
return {
backendValue: this.value
};
},
computed: {
'realValue': {
get: function () {
return this.backendValue;
},
set: function (v) {
this.backendValue = v;
this.$emit('input', v);
}
}
},
methods: {
add() {
this.$emit('add');
},
remove() {
this.$emit('delete');
}
},
watch: {
value(v) {
this.backendValue = v;
}
}
}
</script>

View File

@ -156,6 +156,21 @@ class UserPresenter
user.user_name_change_requests.map { |req| template.link_to req.original_name, req }.join(", ").html_safe
end
def favorite_tags_with_types
tag_names = user&.favorite_tags.to_s.split
tag_names = TagAlias.to_aliased(tag_names)
Tag.where(name: tag_names).map {|x| [x.name, x.post_count, x.category]}
end
def recent_tags_with_types
versions = PostArchive.where(updater_id: user.id).where("updated_at > ?", 1.hour.ago).order(id: :desc).limit(150)
tags = versions.flat_map(&:added_tags)
tags = tags.reject { |tag| Tag.is_metatag?(tag) }
tags = tags.group_by(&:itself).transform_values(&:size).sort_by { |tag, count| [-count, tag] }.map(&:first)
tags = tags.take(50)
Tag.where(name: tags).map {|x| [x.name, x.post_count, x.category]}
end
def custom_css
user.custom_style.to_s.split(/\r\n|\r|\n/).map do |line|
if line =~ /\A@import/

View File

@ -1,8 +1,3 @@
<%= render partial: "uploads/upload_partials/css" %>
<%= render partial: "uploads/upload_partials/tag_preview" %>
<%= render partial: "uploads/upload_partials/source" %>
<%= render partial: "uploads/upload_partials/related" %>
<%= render partial: "uploads/upload_partials/uploader" %>
<div id="post-add">
<div style="margin-bottom: 1rem;" class="section">
<h2>Before uploading, read
@ -14,6 +9,7 @@
Unsure what to tag your post
with? <%= link_to "Tagging Checklist", help_pages_path(id: "tagging_checklist") %></p>
</div>
<div id="uploader"></div>
<%# if current_user.is_member_or_lower? && (current_user.upload_limit <= 5 || current_user.post_upload_throttle <= 5) %>
<!-- <div id="post-uploads-remaining" class="section<%# if [current_user.upload_limit, current_user.post_upload_throttle].min <= 0 %> sect_red<%# end %>" style="width:640px;">-->
<!-- <p>-->
@ -32,655 +28,12 @@
<!-- See <%#= link_to "here", controller: "user", action: "upload_limit" %> for more details.-->
<!-- </div>-->
<%# end %>
<post-creator :safe="<%= CurrentUser.safe_mode? %>"></post-creator>
</div>
<!--<script type="text/javascript">-->
<!-- var userUploadTags = <%#= current_user.uploaded_tags_with_types.to_json %>;-->
<!-- var userRecentTags = <%#= current_user.recent_tags_with_types.to_json %>;-->
<!-- var userArtistTags = <%#= @artists.to_json %>;-->
<!--</script>-->
<script>
var userUploadTags = [];
var userRecentTags = [];
var userArtistTags = [];
var safeSite = <%= CurrentUser.safe_mode?.to_json %>;
var userUploadTags = <%= CurrentUser.presenter.favorite_tags_with_types.to_json.html_safe %>;
var userRecentTags = <%= CurrentUser.presenter.recent_tags_with_types.to_json.html_safe %>;
</script>
<script src="/vendor/vue.js"></script>
<script type="text/javascript">
const thumbURLs = [
"/images/notfound-preview.png",
"/images/download-preview.png",
"/images/webm-preview.png",
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="
];
const thumbs = {
webm: "/images/webm-preview.png",
flash: "/images/download-preview.png",
notfound: "/images/notfound-preview.png",
none: 'data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=='
};
const artist_checks = [
{name: 'Unknown Artist'},
{name: 'Anonymous Artist'}];
const sex_checks = [
{name: 'Male'},
{name: 'Female'},
{name: 'Cuntboy'},
{name: 'Dickgirl'},
{name: 'Hermaphrodite', tag: 'herm'},
{name: 'Male-Herm', tag: 'maleherm'},
{name: 'Ambiguous', tag: 'ambiguous_gender'}];
const pairing_checks = [
{name: 'Male/Male'},
{name: 'Male/Female'},
{name: 'Female/Female'},
{name: 'Intersex/Male'},
{name: 'Intersex/Female'},
{name: 'Intersex/Intersex'}
];
const char_count_checks = [
{name: 'Solo'},
{name: 'Duo'},
{name: 'Group'},
{name: 'Zero Pictured'}];
const body_type_checks = [
{name: 'Anthro'},
{name: 'Feral'},
{name: 'Humanoid'},
{name: 'Human'},
{name: 'Taur'}];
function updatePreviewDims(e) {
var img = e.target;
if (thumbURLs.filter(function (x) {
return img.src.indexOf(x) !== -1;
}).length !== 0)
return;
this.previewHeight = img.naturalHeight;
this.previewWidth = img.naturalWidth;
this.overDims = (img.naturalHeight > 15000 || img.naturalWidth > 15000);
updateDimensionTag.call(this);
}
function previewError() {
this.previewWidth = this.previewHeight = 0;
this.overDims = false;
if (this.uploadURL === '' && !this.$refs['post_file']) {
this.previewURL = thumbs.none;
} else {
this.previewURL = thumbs.notfound;
}
}
function updatePreviewFile() {
var self = this;
var reader = new FileReader();
var file = this.$refs['post_file'].files[0];
this.previewHeight = 0;
this.previewWidth = 0;
reader.onload = function (e) {
var src = e.target.result;
if (file.type.match('video/webm'))
src = thumbs.webm;
else if (file.type.match('application/x-shockwave-flash'))
src = thumbs.flash;
self.previewURL = src;
};
reader.readAsDataURL(file);
this.disableURLUpload = true;
}
function updatePreviewURL() {
var self = this;
if (this.uploadURL.length === 0 || (this.$refs['post_file'] && this.$refs['post_file'].files.length > 0)) {
this.disableFileUpload = false;
this.oldDomain = '';
return;
}
this.disableFileUpload = true;
var domain = $j("<a>").prop("href", this.uploadURL).prop("hostname");
if (domain && domain != this.oldDomain) {
$j.getJSON("/upload_whitelist/is_whitelisted", {url: this.uploadURL}, function (data) {
if (data.domain)
self.whitelistWarning(data.is_whitelisted, data.domain);
});
}
this.oldDomain = domain;
var src = thumbs.none;
if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(jpg|jpeg|gif|png)/))
src = this.uploadURL;
else if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(swf)$/))
src = thumbs.flash;
else if (this.uploadURL.match(/^(https?\:\/\/|www).*?\.(webm)$/))
src = thumbs.webm;
this.previewURL = src;
}
function updateDimensionTag() {
var self = this;
if (!(self.previewHeight || self.previewWidth))
return;
var otherTags = ['low_res', 'hi_res', 'superabsurd_res', 'absurd_res'];
var ourTag = function (h, w) {
if (!(h && w)) {
return null;
}
if ((h <= 500) && (w <= 500))
return 'low_res';
if ((h >= 10000) || (w >= 10000))
return 'superabsurb_res';
if ((h >= 2400) || (w >= 3200))
return 'absurd_res';
if ((h >= 1200) || (w >= 1600))
return 'hi_res';
return null;
}(self.previewHeight, self.previewWidth);
var tagIdx = otherTags.indexOf(ourTag);
if (tagIdx > 0)
otherTags.splice(tagIdx, 1);
if (ourTag)
self.pushTag(ourTag, true);
for(var i = 0; i < otherTags.length; i++) {
self.pushTag(otherTags[i], false);
}
}
function updatePreview() {
if (this.$refs['post_file'] && this.$refs['post_file'].files[0])
updatePreviewFile.call(this);
else
updatePreviewURL.call(this);
}
function directURLCheck(url) {
var patterns = [{reason: 'Thumbnail URL', test: /[at]\.facdn\.net/gi},
{reason: 'Sample URL', test: /pximg\.net\/img-master/gi},
// {reason: 'Sample URL', test: /\d+\.media\.tumblr\.com/gi}, // Tumblr broke raws.
{reason: 'Sample URL', test: /d3gz42uwgl1r1y\.cloudfront\.net\/.*\/\d+x\d+\./gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/\w+\.(jpg|png)(:large)?$/gi},
{reason: 'Sample URL', test: /pbs\.twimg\.com\/media\/\w+\?format=/gi},
{reason: 'Sample URL', test: /derpicdn\.net\/.*\/large\./gi},
{reason: 'Sample URL', test: /metapix\.net\/files\/(preview|screen)\//gi},
{reason: 'Sample URL', test: /sofurryfiles\.com\/std\/preview/gi}];
for (var i = 0; i < patterns.length; ++i) {
var pattern = patterns[i];
if (pattern.test.test(url))
return pattern.reason;
}
return '';
}
function clearFileUpload() {
if (!(this.$refs['post_file'] && this.$refs['post_file'].files[0]))
return;
this.$refs['post_file'].value = null;
this.disableURLUpload = this.disableFileUpload = false;
this.previewURL = thumbs.none;
this.previewHeight = this.previewWidth = 0;
this.updatePreview();
}
var source = Vue.extend({
template: '#source-template',
props: ['value', 'index', 'last'],
data: function () {
return {
backendValue: this.value
};
},
computed: {
'realValue': {
get: function () {
return this.backendValue;
},
set: function (v) {
this.backendValue = v;
this.$emit('input', v);
}
}
},
methods: {
add: function () {
this.$emit('add');
},
remove: function () {
this.$emit('delete');
}
},
watch: {
value: function (v) {
this.backendValue = v;
}
}
});
var checkbox = Vue.extend({
template: '#checkbox-template',
props: ['check', 'checks'],
computed: {
value: {
get: function () {
if (this.checks[this.tagName] === undefined)
return false;
return this.checks[this.tagName];
},
set: function (v) {
this.$emit('set', this.tagName, v);
}
},
tagName: function () {
return this.check.tag || this.check.name.toLowerCase().replace(/ /g, '_');
}
}
});
var tagSorter = function (a, b) {
return a[0] > b[0] ? 1 : -1;
};
var relatedTags = Vue.extend({
template: '#related-tags',
props: ['tags', 'related', 'loading'],
data: function () {
return {
uploaded: (userUploadTags || []),
recent: (userRecentTags || []).sort(tagSorter),
artists: (userArtistTags || []).sort(tagSorter)
};
},
methods: {
toggle: function (tag) {
this.$emit('tag-active', tag[0], !this.tagActive(tag));
},
tagLink: function (tag) {
return '/post/index?tags=' + encodeURIComponent(tag[0]);
},
tagActive: function (tag) {
return this.tags.indexOf(tag[0]) !== -1;
},
tagClasses: function (tag) {
var classes = {'tag-active': this.tagActive(tag)};
classes['tag-type-' + tag[2]] = true;
return classes;
},
splitTags: function (tags) {
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(tags, 15);
}
},
computed: {
tagGroups: {
get: function () {
var groups = [];
if (this.uploaded && this.uploaded.length) {
groups.push({
title: "Quick Tags",
tags: this.uploaded
});
}
if (this.recent && this.recent.length) {
groups.push({
title: "Recent",
tags: this.recent
});
}
if (this.artists && this.artists.length) {
groups.push({
title: "Artists",
tags: this.artists
});
}
if (this.related && this.related.length) {
for (var i = 0; i < this.related.length; i++) {
groups.push(this.related[i]);
}
}
if (this.loading) {
groups.push({title: 'Loading Related Tags', tags: [['', '', '']]});
}
return groups;
}
}
}
});
var tagPreviewTag = Vue.extend({
functional: true,
props: ['tag'],
render: function (h, ctx) {
var tag = ctx.props.tag;
switch (tag.type) {
default:
case 'tag':
return h('a', {
staticClass: 'tag-preview tag-type-' + tag.tagType
}, tag.a);
case 'alias':
return h('span', {staticClass: 'tag-preview tag-preview-alias'}, [
h('del', undefined, [
h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.a)
]), ' → ', h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.b)
]);
case 'implication':
return h('span', {staticClass: 'tag-preview tag-preview-implication'}, [
h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.a), ' ⇐ ', h('a', {staticClass: 'tag-type-' + tag.tagType}, tag.b)
]);
}
}
});
var tagPreview = Vue.extend({
template: '#tag-preview',
props: ['tags', 'loading'],
components: {
'tag-preview': tagPreviewTag
},
methods: {
close: function () {
this.$emit('close');
}
},
computed: {
splitTags: function () {
var newTags = this.tags.concat([]);
newTags.sort(function (a, b) {
return a.a === b.a ? 0 : (a.a < b.a ? -1 : 1);
});
var chunkArray = function (arr, size) {
var chunks = [];
for (var i = 0; i < arr.length; i += size) {
chunks.push(arr.slice(i, i + size));
}
return chunks;
};
return chunkArray(newTags, 15);
}
}
});
var editor = Vue.extend({
template: '#post-creator',
props: ['safe'],
components: {
'image-source': source,
'image-checkbox': checkbox,
'related-tags': relatedTags,
'tag-preview': tagPreview
},
data: function () {
var allChecks = {};
var addChecks = function (check) {
if (typeof check['tag'] !== "undefined") {
allChecks[check.tag] = true;
return
}
allChecks[check.name.toLowerCase().replace(' ', '_')] = true;
};
artist_checks.forEach(addChecks);
sex_checks.forEach(addChecks);
pairing_checks.forEach(addChecks);
char_count_checks.forEach(addChecks);
body_type_checks.forEach(addChecks);
return {
showErrors: false,
whitelist: {
visible: false,
allowed: false,
domain: ''
},
submitting: false,
disableFileUpload: false,
disableURLUpload: false,
previewHeight: 0,
previewWidth: 0,
overDims: false,
uploadURL: '',
previewURL: thumbs.none,
oldDomain: '',
noSource: false,
sources: [''],
normalMode: true,
checkboxes: {
artist: artist_checks,
sex: sex_checks,
pairing: pairing_checks,
count: char_count_checks,
body: body_type_checks,
selected: {},
all: allChecks
},
tagEntries: {
character: '',
sex: '',
bodyType: '',
theme: '',
other: ''
},
preview: {
loading: false,
show: false,
tags: []
},
relatedTags: [],
loadingRelated: false,
parentID: '',
description: '',
rating: ''
};
},
methods: {
updatePreview: updatePreview,
updatePreviewDims: updatePreviewDims,
previewError: previewError,
clearFile: clearFileUpload,
whitelistWarning: function (allowed, domain) {
this.whitelist.allowed = allowed;
this.whitelist.domain = domain;
this.whitelist.visible = true;
},
removeSource: function (i) {
this.sources.splice(i, 1);
},
addSource: function () {
if (this.sources.length < 5)
this.sources.push('');
},
setCheck: function (tag, value) {
Vue.set(this.checkboxes.selected, tag, value);
},
submit: function () {
this.showErrors = true;
if (this.preventUpload || this.submitting)
return;
var self = this;
this.submitting = true;
var data = new FormData();
var post_file = this.$refs['post_file'];
if (post_file && post_file.files && post_file.files.length) {
data.append('upload[file]', this.$refs['post_file'].files[0]);
} else {
data.append('upload[direct_url]', this.uploadURL);
}
data.append('upload[tag_string]', this.tags);
data.append('upload[rating]', this.rating);
data.append('upload[source]', this.sources.join('\n'));
// data.append('upload[description]', this.description);
data.append('upload[parent_id]', this.parentID);
jQuery.ajax('/uploads.json', {
contentType: false,
processData: false,
method: 'POST',
type: 'POST',
data: data,
success: function (data) {
self.submitting = false;
notice('Post uploaded successfully.');
location.assign(data.location);
},
error: function (data) {
self.submitting = false;
try {
var data2 = JSON.parse(data.responseText);
if (data2 && data2.location) {
error('Error: Post already exists. <a href="' + data2.location + '">View it.</a>');
} else {
error('Error: ' + data2.reason);
}
} catch (e) {
error('Error: Unknown error! ' + data);
}
}
});
},
pushTag: function (tag, add) {
this.preview.show = false;
var isCheck = typeof this.checkboxes.all[tag] !== "undefined";
// In advanced mode we need to push these into the tags area because there are no checkboxes or other
// tag fields so we can't see them otherwise.
if (isCheck && this.normalMode) {
this.setCheck(tag, add);
return;
}
var tags = this.tagEntries.other ? this.tagEntries.other.trim().split(' ') : [];
var tagIdx = tags.indexOf(tag);
if (add) {
if (tagIdx === -1)
tags.push(tag);
} else {
if (tagIdx === -1)
return;
tags.splice(tagIdx, 1);
}
this.tagEntries.other = tags.join(' ') + ' ';
},
previewFinalTags: function () {
if (this.preview.loading)
return;
if (this.preview.show) {
this.preview.show = false;
return;
}
this.preview.loading = true;
this.preview.show = true;
this.preview.tags = [];
var self = this;
var data = {tags: this.tags};
jQuery.ajax("/tags/preview.json", {
method: 'POST',
type: 'POST',
data: data,
success: function (result) {
self.preview.loading = false;
self.preview.tags = result;
},
error: function (result) {
self.preview.loading = false;
self.preview.tags = [];
self.preview.show = false;
error('Error loading tag preview ' + result);
}
})
},
findRelated: function (type) {
const self = this;
const convertResponse = function (respData) {
var sortedRelated = [];
for (var key in respData) {
if (!respData.hasOwnProperty(key)) {
continue;
}
sortedRelated.push({title: 'Related: ' + key, tags: respData[key].sort(tagSorter)});
}
return sortedRelated;
};
const getSelectedTags = function () {
const field = self.$refs['otherTags'];
if (!field.hasOwnProperty('selectionStart'))
return null;
const length = field.selectionEnd - field.selectionStart;
if (length)
return field.value.substr(field.selectionStart, length);
return null;
};
this.loadingRelated = true;
this.relatedTags = [];
const selectedTags = getSelectedTags();
const params = selectedTags ? {query: selectedTags} : {query: this.tags};
if (type)
params['category'] = type;
$.getJSON("/related_tag/bulk.json", params, function(data) {
self.relatedTags = convertResponse(data);
}).always(function() {
self.loadingRelated = false;
});
}
},
computed: {
tags: function () {
var self = this;
var checked = Object.keys(this.checkboxes.selected).filter(function (x) {
return self.checkboxes.selected[x] === true;
});
return checked.concat([this.tagEntries.other, this.tagEntries.sex, this.tagEntries.bodyType,
this.tagEntries.theme, this.tagEntries.character]).join(' ').replace(',', ' ').trim().replace(/ +/g, ' ');
},
tagsArray: function () {
return this.tags.toLowerCase().split(' ');
},
previewDimensions: function () {
if (this.previewWidth && this.previewHeight)
return this.previewHeight + '×' + this.previewWidth;
return '';
},
directURLProblem: function () {
return directURLCheck(this.uploadURL);
},
badDirectURL: function () {
return !!this.directURLProblem;
},
sourceWarning: function () {
var validSourceCount = this.sources.filter(function (i) {
return i.length > 0;
}).length;
return !this.noSource && (validSourceCount === 0);
},
tagCount: function () {
return this.tags.split(' ').filter(function (x) {
return x;
}).length;
},
notEnoughTags: function () {
return this.tagCount < 4;
},
invalidRating: function () {
return !this.rating;
},
preventUpload: function () {
return this.sourceWarning || this.badDirectURL || this.notEnoughTags
|| this.invalidRating;
}
}
});
var main = new Vue({
el: '#post-add',
components: {
'post-creator': editor
}
});
Danbooru.Uploader.init();
</script>

View File

@ -1,178 +0,0 @@
<style>
.toggle-button {
box-sizing: border-box;
display: inline-block;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: center;
user-select: none;
padding: .175em .5rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0.25rem;
margin-right: 5px;
}
.toggle-button.active {
background-color: #FDF5D9;
}
.flex-grid-outer {
display: flex;
padding: 10px 0;
}
.upload_preview_container.in-editor {
display: none;
}
.upload_preview_container.in-sidebar {
position: sticky;
top: 20px;
}
.flex-grid {
display: flex;
padding: 10px 0;
}
.flex-wrap {
display: flex;
flex-wrap: wrap;
}
.col {
flex: 1 0 0;
margin-right: 5px;
}
.col2 {
flex: 2 1 0;
}
.border-bottom {
border-bottom: 1px solid rgba(0, 0, 0, 0.25);
}
.section-label {
white-space: normal;
}
.come-together-now {
padding-bottom: 0;
}
.over-me {
padding-top: 0;
}
.related-section {
display: flex;
flex-direction: row;
flex: 0 1 10%;
padding: 5px 10px;
}
.related-items {
display: flex;
flex-direction: column;
margin: 0 -5px;
padding: 0 5px;
}
.related-item {
padding: 0 5px;
max-width: 140px;
word-wrap: break-word;
overflow-wrap: break-word;
}
.related-title {
padding: 0 5px;
font-weight: bold;
}
.tag-active {
background: rgb(0, 111, 250);
color: white;
}
.tag-preview {
border: 1px solid rgba(0, 0, 0, 0.15);
background: rgba(1, 1, 1, 0.15);
border-radius: 2px;
padding: 3px;
margin-right: 5px;
box-sizing: border-box;
}
.tag-preview-alias {
background-color: rgba(150, 0, 0, 0.25);
}
.tag-preview-implication {
background-color: rgba(0, 150, 0, 0.25);
}
.tag-textarea {
display: inline-block; /* Why were we even unsetting this? It breaks EVERYTHING. */
font-size: 1rem;
width: 100%;
resize: vertical;
}
/* Need to override this so it shows up at all. */
#whitelist-warning {
display: block;
float: none;
}
@media only screen and (orientation: portrait), (max-width: 1100px) {
#preview-sidebar {
display: none;
}
.upload_preview_container.in-editor {
display: flex;
flex-direction: column-reverse;
}
.upload_preview_container.in-sidebar {
display: none;
}
.upload_preview_dims {
text-align: center;
}
.upload_preview_img {
display: block;
margin-left: auto;
margin-right: auto;
max-height: 500px;
}
.below-upload > .upload_preview_img {
max-height: 150px;
}
.related-section {
flex: 0 1 50%;
}
.flex-grid {
flex-direction: column;
}
input {
max-width: 90%;
}
}
.the_secret_switch {
height: 10px;
width: 100%;
}
</style>

View File

@ -1,12 +0,0 @@
<template id="related-tags">
<div class="related-tags flex-wrap">
<div class="related-section" v-for="group in tagGroups" :key="group.title">
<div class="related-items" v-for="tags, i in splitTags(group.tags)" :key="i">
<div class="related-title" v-if="i === 0">{{group.title}}</div>
<div class="related-item" v-for="tag in tags" :key="tag[0]">
<a :class="tagClasses(tag)" :href="tagLink(tag)" @click.prevent="toggle(tag)">{{tag[0]}}</a>
</div>
</div>
</div>
</div>
</template>

View File

@ -1,7 +0,0 @@
<template id="source-template">
<div>
<input type="text" size="50" v-model="realValue" @keyup.enter="add"/>
<button @click="remove" v-if="index !== 0">-</button>
<button @click="add" v-if="last && index < 4">+</button>
</div>
</template>

View File

@ -1,13 +0,0 @@
<template id="tag-preview">
<div>
<div v-show="loading">Fetching tags...</div>
<div class="related-tags flex-wrap">
<div class="related-items" v-for="sTags, i in splitTags" :key="i">
<tag-preview v-for="tag, $idx in sTags" :key="$idx" :tag="tag"></tag-preview>
</div>
</div>
<div>
<a href="#" @click.prevent="close">Close Preview</a>
</div>
</div>
</template>

View File

@ -1,228 +0,0 @@
<template id="checkbox-template">
<button class="toggle-button" :class="{active: value}" @click="value = !value">{{check.name}}</button>
</template>
<template id="post-creator">
<div class="flex-grid-outer">
<div class="col box-section" style="flex: 2 0 0;">
<div class="the_secret_switch" @click="normalMode = !normalMode"></div>
<% unless @upload.new_record? %>
<div class="box-section" style="margin-bottom: 1rem;">
<p>This post was already uploaded
(<%= link_to "post ##{@upload.id}", {action: "show", id: @upload.id}, target: "_blank" %>).</p>
</div>
<% end %>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_file">Image</label>
</div>
<div class="col2">
<div v-if="!disableFileUpload">
<label>File:
<input type="file" ref="post_file" @change="updatePreview" :disabled="disableFileUpload"/>
</label>
<button @click="clearFile" v-show="disableURLUpload">Clear</button>
</div>
<div v-if="!disableURLUpload">
<div class="box-section sect_red" v-if="badDirectURL">
The direct URL entered has the following problem: {{ directURLProblem }}<br>
You should review <a href="/wiki/show/howto:sites_and_sources">the sourcing guide</a>.
</div>
<label>{{!disableFileUpload ? '(or) ' : '' }}URL:
<input type="text" size="50" v-model="uploadURL" @keyup="updatePreview" :disabled="disableURLUpload"/>
</label>
<div id="whitelist-warning" v-show="whitelist.visible" :class="{'whitelist-warning-allowed': whitelist.allowed, 'whitelist-warning-disallowed': !whitelist.allowed}">
<span v-if="whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are permitted.</span>
<span v-if="!whitelist.allowed">Uploads from <b>{{whitelist.domain}}</b> are not permitted. (<a href='/upload_whitelist'>View whitelisted domains</a>)</span>
</div>
</div>
</div>
</div>
<div class="box-section upload_preview_container in-editor below-upload">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;"/>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_sources">Sources</label>
<div>You should include: A link to the artists page where this was obtained, and a link to the submission page
where this image was obtained. No available source should ONLY be used if the content has never been posted
online anywhere else.
</div>
</div>
<div class="col2">
<div class="box-section sect_red" v-show="showErrors && sourceWarning">A source must be provided or you must
select that there
is no available source.
</div>
<div v-if="!noSource">
<image-source :last="i === (sources.length-1)" :index="i" v-model="sources[i]" v-for="s, i in sources"
@delete="removeSource(i)" @add="addSource" :key="i"></image-source>
</div>
<div>
<label class="section-label"><input type="checkbox" id="no_source" v-model="noSource"/> No available source
/ I am the source.</label>
</div>
</div>
</div>
<template v-if="normalMode">
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="names">Artists</label>
</div>
<div class="col2">
<div>
<textarea class="tag-textarea" v-model="tagEntries.character" id="post_characters" rows="2"
placeholder="Ex: artist_name etc."></textarea>
</div>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.artist" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_sex_tags">Characters</label>
<div>Select (and write in) all that apply. Character sex is based only on what is visible in the image.
Outside information or other images should not be used when deciding what tags are used.
</div>
</div>
<div class="col2">
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.sex" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<hr>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.count" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<hr>
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.pairing" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<textarea class="tag-textarea" rows="2" v-model="tagEntries.sex" id="post_sexes"
placeholder="Ex: character_name solo_focus etc."></textarea>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Body Types and Species</label>
</div>
<div class="col2">
<div class="flex-wrap">
<image-checkbox :check="check" :checks="checkboxes.selected" v-for="check in checkboxes.body" @set="setCheck"
:key="check.name"></image-checkbox>
</div>
<textarea class="tag-textarea" rows="2" v-model="tagEntries.bodyType" id="post_bodyTypes"
placeholder="Ex: bear dragon hyena rat newt etc."></textarea>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Contentious Content</label>
<div>
These allow users to find or blacklist content with ease. Make sure that you are tagging these upon
initial upload.
</div>
</div>
<div class="col2">
<textarea class="tag-textarea" v-model="tagEntries.theme" id="post_themes" rows="2"
placeholder="Ex: cub young gore scat watersports diaper my_little_pony vore not_furry rape etc."></textarea>
</div>
</div>
</template>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_rating_questionable">Rating</label>
<div>Explicit tags include sex, pussy, penis, masturbation, fellatio, etc.
(<%= link_to "help", help_pages_path(title: 'ratings'), target: "_blank" %>)
</div>
</div>
<div class="col2">
<div class="box-section sect_red" v-if="showErrors && invalidRating">
You must select an appropriate rating for this image.
</div>
<div>
<template v-if="!safe">
<button class="toggle-button" :class="{active: rating==='e'}" @click="rating = 'e'">Explicit</button>
<button class="toggle-button" :class="{active: rating==='q'}" @click="rating = 'q'">Questionable</button>
</template>
<button class="toggle-button" :class="{active: rating==='s'}" @click="rating = 's'">Safe</button>
</div>
</div>
</div>
<div class="flex-grid come-together-now">
<div class="col">
<label class="section-label" for="post_tags">Other Tags</label>
<div>
Separate tags with spaces. (<%= link_to "help", help_pages_path(title: 'tags'), target: "_blank" %>)
</div>
</div>
<div class="col2">
<div class="box-section upload_preview_container in-editor">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;"/>
</div>
<div class="box-section sect_red" v-show="showErrors && notEnoughTags">
You must provide at least <b>{{4 - tagCount}}</b> more tags. Tags in other sections count towards this
total.
</div>
<div v-show="!preview.show">
<textarea class="tag-textarea" id="post_tags" v-model="tagEntries.other" rows="5" ref="otherTags"></textarea>
</div>
<div v-show="preview.show">
<tag-preview :tags="preview.tags" :loading="preview.loading" @close="previewFinalTags"></tag-preview>
</div>
<div class="related-tag-functions">
<a href="#" @click.prevent="findRelated()">Related Tags</a> |
<a href="#" @click.prevent="findRelated('artist')">Related Artists</a> |
<a href="#" @click.prevent="findRelated('char')">Related Characters</a> |
<a href="#" @click.prevent="findRelated('copyright')">Related Copyrights</a> |
<a href="#" @click.prevent="previewFinalTags">Preview Final Tags</a>
</div>
</div>
</div>
<div class="flex-grid border-bottom over-me">
<related-tags :tags="tagsArray" :related="relatedTags" :loading="loadingRelated" @tag-active="pushTag"></related-tags>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label">Parent Post ID</label>
</div>
<div class="col2">
<input type="number" v-model.number="parentID" placeholder="Ex. 12345"/>
</div>
</div>
<div class="flex-grid border-bottom">
<div class="col">
<label class="section-label" for="post_description">Description</label>
</div>
<div class="col2">
<textarea class="tag-textarea" id="post_description" v-model="description" rows="5"></textarea>
</div>
</div>
<div class="flex-grid">
<div class="col"></div>
<div class="col2">
<div class="box-section sect_red" v-show="preventUpload && showErrors">
Unmet requirements above prevent the submission of the post.
</div>
<div class="box-section sect_green" v-show="submitting">
Submitting your post, please wait.
</div>
<button @click="submit" :disabled="(showErrors && preventUpload) || submitting" accesskey="s">{{ submitting ? 'Uploading...' :
'Upload' }}
</button>
</div>
</div>
</div>
<div id="preview-sidebar" class="col box-section" style="margin-left: 10px; padding: 10px;">
<div class="upload_preview_container in-sidebar">
<div class="upload_preview_dims">{{ previewDimensions }}</div>
<img class="upload_preview_img" :src="previewURL" style="max-width: 100%;" @load="updatePreviewDims" @error="previewError"/>
</div>
</div>
</div>
</template>

View File

@ -1,5 +1,6 @@
const { environment } = require('@rails/webpacker')
const erb = require('./loaders/erb')
const VueLoaderPlugin = require('vue-loader/lib/plugin')
const webpack = require('webpack');
environment.loaders.append('scss.erb', {
@ -14,6 +15,12 @@ environment.loaders.append('scss.erb', {
]
});
environment.loaders.append('vue', {
test: /\.vue$/,
use: 'vue-loader'
});
environment.plugins.append('VueLoaderPlugin', new VueLoaderPlugin());
environment.loaders.append('erb', erb);
environment.config.output.library = ["Danbooru"];

View File

@ -10,8 +10,10 @@
"qtip2": "^3.0.3",
"rails-erb-loader": "^5.4.2",
"script-loader": "^0.7.2",
"webpack-cli": "^3.0.8",
"vue": "^2.6.10"
"vue": "^2.6.10",
"vue-loader": "^15.7.0",
"vue-template-compiler": "^2.6.10",
"webpack-cli": "^3.0.8"
},
"devDependencies": {
"eslint": "^5.3.0",

138
yarn.lock
View File

@ -31,6 +31,21 @@
webpack "^3.11.0"
webpack-manifest-plugin "^1.3.2"
"@vue/component-compiler-utils@^2.5.1":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-2.6.0.tgz#aa46d2a6f7647440b0b8932434d22f12371e543b"
integrity sha512-IHjxt7LsOFYc0DkTncB7OXJL7UzwOLPPQCfEUNyxL2qt+tF12THV+EO33O1G2Uk4feMSWua3iD39Itszx0f0bw==
dependencies:
consolidate "^0.15.1"
hash-sum "^1.0.2"
lru-cache "^4.1.2"
merge-source-map "^1.1.0"
postcss "^7.0.14"
postcss-selector-parser "^5.0.0"
prettier "1.16.3"
source-map "~0.6.1"
vue-template-es2015-compiler "^1.9.0"
abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
@ -877,6 +892,11 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
bluebird@^3.1.1:
version "3.5.5"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
bluebird@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9"
@ -1183,6 +1203,15 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.4.1:
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
dependencies:
ansi-styles "^3.2.1"
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
chardet@^0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.4.2.tgz#b5473b33dc97c424e5d98dc87d55d4d8a29c8bf2"
@ -1453,6 +1482,13 @@ console-control-strings@^1.0.0, console-control-strings@~1.1.0:
resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e"
integrity sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=
consolidate@^0.15.1:
version "0.15.1"
resolved "https://registry.yarnpkg.com/consolidate/-/consolidate-0.15.1.tgz#21ab043235c71a07d45d9aad98593b0dba56bab7"
integrity sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==
dependencies:
bluebird "^3.1.1"
constants-browserify@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
@ -1640,6 +1676,11 @@ cssesc@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-0.1.0.tgz#c814903e45623371a0477b40109aaafbeeaddbb4"
cssesc@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-2.0.0.tgz#3b13bd1bb1cb36e1bcb5a4dcd27f54c5dcb35703"
integrity sha512-MsCAG1z9lPdoO/IUMLSBWBSVxVtJ1395VGIQ+Fc2gNdkQ1hNDnQdw3YhA71WJCBW1vdwA0cAnk/DnW6bqoEUYg==
cssnano@^3.10.0:
version "3.10.0"
resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-3.10.0.tgz#4f38f6cea2b9b17fa01490f23f1dc68ea65c1c38"
@ -1711,6 +1752,11 @@ date-now@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
de-indent@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d"
integrity sha1-sgOOhG3DO6pXlhKNCAS0VbjB4h0=
debug-loader@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/debug-loader/-/debug-loader-0.0.1.tgz#44dc37e09e3c39e6af334681960f70a534a9d056"
@ -2901,6 +2947,11 @@ hash-base@^3.0.0:
inherits "^2.0.1"
safe-buffer "^5.0.1"
hash-sum@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-1.0.2.tgz#33b40777754c6432573c120cc3808bbd10d47f04"
integrity sha1-M7QHd3VMZDJXPBIMw4CLvRDUfwQ=
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.4"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.4.tgz#8b50e1f35d51bd01e5ed9ece4dbe3549ccfa0a3c"
@ -2917,6 +2968,11 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
he@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
@ -3787,6 +3843,14 @@ lru-cache@^4.0.1, lru-cache@^4.1.1:
pseudomap "^1.0.2"
yallist "^2.1.2"
lru-cache@^4.1.2:
version "4.1.5"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd"
integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==
dependencies:
pseudomap "^1.0.2"
yallist "^2.1.2"
make-dir@^1.0.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c"
@ -3875,6 +3939,13 @@ merge-descriptors@1.0.1:
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
integrity sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
merge-source-map@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/merge-source-map/-/merge-source-map-1.1.0.tgz#2fdde7e6020939f70906a68f2d7ae685e4c8c646"
integrity sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==
dependencies:
source-map "^0.6.1"
methods@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
@ -5134,6 +5205,15 @@ postcss-selector-parser@^2.0.0, postcss-selector-parser@^2.2.2, postcss-selector
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-selector-parser@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-5.0.0.tgz#249044356697b33b64f1a8f7c80922dddee7195c"
integrity sha512-w+zLE5Jhg6Liz8+rQOWEAwtwkyqpfnmsinXjXg6cY7YIONZZtgvE0v2O0uhQBs0peNomOJwWRKt6JBfTdTd3OQ==
dependencies:
cssesc "^2.0.0"
indexes-of "^1.0.1"
uniq "^1.0.1"
postcss-svgo@^2.1.1:
version "2.1.6"
resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-2.1.6.tgz#b6df18aa613b666e133f08adb5219c2684ac108d"
@ -5188,6 +5268,15 @@ postcss@^6.0, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.14,
source-map "^0.6.1"
supports-color "^5.4.0"
postcss@^7.0.14:
version "7.0.17"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.17.tgz#4da1bdff5322d4a0acaab4d87f3e782436bad31f"
integrity sha512-546ZowA+KZ3OasvQZHsbuEpysvwTZNGJv9EfyCQdsIDltPSWHAeTQ5fQy/Npi2ZDtLI3zs7Ps/p6wThErhm9fQ==
dependencies:
chalk "^2.4.2"
source-map "^0.6.1"
supports-color "^6.1.0"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@ -5196,6 +5285,11 @@ prepend-http@^1.0.0:
version "1.0.4"
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
prettier@1.16.3:
version "1.16.3"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.16.3.tgz#8c62168453badef702f34b45b6ee899574a6a65d"
integrity sha512-kn/GU6SMRYPxUakNXhpP0EedT/KmaPzr0H5lIsDogrykbaxOpOfAFfk5XA7DZrJyMAv1wlMV3CPcZruGXVVUZw==
private@^0.1.6, private@^0.1.8:
version "0.1.8"
resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff"
@ -6312,6 +6406,13 @@ supports-color@^5.3.0, supports-color@^5.4.0:
dependencies:
has-flag "^3.0.0"
supports-color@^6.1.0:
version "6.1.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-6.1.0.tgz#0764abc69c63d5ac842dd4867e8d025e880df8f3"
integrity sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==
dependencies:
has-flag "^3.0.0"
svgo@^0.7.0:
version "0.7.2"
resolved "https://registry.yarnpkg.com/svgo/-/svgo-0.7.2.tgz#9f5772413952135c6fefbf40afe6a4faa88b4bb5"
@ -6682,6 +6783,43 @@ vm-browserify@0.0.4:
dependencies:
indexof "0.0.1"
vue-hot-reload-api@^2.3.0:
version "2.3.3"
resolved "https://registry.yarnpkg.com/vue-hot-reload-api/-/vue-hot-reload-api-2.3.3.tgz#2756f46cb3258054c5f4723de8ae7e87302a1ccf"
integrity sha512-KmvZVtmM26BQOMK1rwUZsrqxEGeKiYSZGA7SNWE6uExx8UX/cj9hq2MRV/wWC3Cq6AoeDGk57rL9YMFRel/q+g==
vue-loader@^15.7.0:
version "15.7.0"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.7.0.tgz#27275aa5a3ef4958c5379c006dd1436ad04b25b3"
integrity sha512-x+NZ4RIthQOxcFclEcs8sXGEWqnZHodL2J9Vq+hUz+TDZzBaDIh1j3d9M2IUlTjtrHTZy4uMuRdTi8BGws7jLA==
dependencies:
"@vue/component-compiler-utils" "^2.5.1"
hash-sum "^1.0.2"
loader-utils "^1.1.0"
vue-hot-reload-api "^2.3.0"
vue-style-loader "^4.1.0"
vue-style-loader@^4.1.0:
version "4.1.2"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-4.1.2.tgz#dedf349806f25ceb4e64f3ad7c0a44fba735fcf8"
integrity sha512-0ip8ge6Gzz/Bk0iHovU9XAUQaFt/G2B61bnWa2tCcqqdgfHs1lF9xXorFbE55Gmy92okFT+8bfmySuUOu13vxQ==
dependencies:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-template-compiler@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.10.tgz#323b4f3495f04faa3503337a82f5d6507799c9cc"
integrity sha512-jVZkw4/I/HT5ZMvRnhv78okGusqe0+qH2A0Em0Cp8aq78+NK9TII263CDVz2QXZsIT+yyV/gZc/j/vlwa+Epyg==
dependencies:
de-indent "^1.0.2"
he "^1.1.0"
vue-template-es2015-compiler@^1.9.0:
version "1.9.1"
resolved "https://registry.yarnpkg.com/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz#1ee3bc9a16ecbf5118be334bb15f9c46f82f5825"
integrity sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==
vue@^2.6.10:
version "2.6.10"
resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.10.tgz#a72b1a42a4d82a721ea438d1b6bf55e66195c637"