aboutsummaryrefslogtreecommitdiff
path: root/src/site/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/site/components')
-rw-r--r--src/site/components/grid/Grid.vue166
-rw-r--r--src/site/components/grid/waterfall/Waterfall.vue181
-rw-r--r--src/site/components/grid/waterfall/WaterfallItem.vue60
-rw-r--r--src/site/components/grid/waterfall/old/waterfall-slot.vue76
-rw-r--r--src/site/components/grid/waterfall/old/waterfall.vue442
-rw-r--r--src/site/components/home/links/Links.vue100
-rw-r--r--src/site/components/loading/CubeShadow.vue48
-rw-r--r--src/site/components/loading/Origami.vue121
-rw-r--r--src/site/components/loading/PingPong.vue98
-rw-r--r--src/site/components/loading/RotateSquare.vue87
-rw-r--r--src/site/components/logo/Logo.vue59
-rw-r--r--src/site/components/navbar/Navbar.vue119
-rw-r--r--src/site/components/sidebar/Sidebar.vue43
-rw-r--r--src/site/components/uploader/Uploader.vue251
14 files changed, 1851 insertions, 0 deletions
diff --git a/src/site/components/grid/Grid.vue b/src/site/components/grid/Grid.vue
new file mode 100644
index 0000000..09922c9
--- /dev/null
+++ b/src/site/components/grid/Grid.vue
@@ -0,0 +1,166 @@
+<style lang="scss" scoped>
+ @import '../../styles/_colors.scss';
+ .item-move {
+ transition: all .25s cubic-bezier(.55,0,.1,1);
+ -webkit-transition: all .25s cubic-bezier(.55,0,.1,1);
+ }
+ div.actions {
+ opacity: 0;
+ -webkit-transition: opacity 0.1s linear;
+ -moz-transition: opacity 0.1s linear;
+ -ms-transition: opacity 0.1s linear;
+ -o-transition: opacity 0.1s linear;
+ transition: opacity 0.1s linear;
+ position: absolute;
+ top: 0px;
+ left: 0px;
+ width: 100%;
+ height: calc(100% - 6px);
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ span {
+ padding: 3px;
+
+ &:nth-child(1), &:nth-child(2) {
+ align-items: flex-end;
+ }
+
+ &:nth-child(1), &:nth-child(3) {
+ justify-content: flex-end;
+ }
+ a {
+ width: 30px;
+ height: 30px;
+ color: white;
+ justify-content: center;
+ align-items: center;
+ display: flex;
+ &:before {
+ content: '';
+ width: 30px;
+ height: 30px;
+ border: 1px solid white;
+ border-radius: 50%;
+ position: absolute;
+ }
+ }
+ }
+
+ &.fixed {
+ position: relative;
+ opacity: 1;
+ background: none;
+
+ a {
+ width: auto;
+ height: auto;
+ color: $defaultTextColor;
+ &:before {
+ display: none;
+ }
+ }
+
+ }
+ }
+</style>
+
+<style lang="scss">
+ .waterfall-item:hover {
+ div.actions {
+ opacity: 1
+ }
+ }
+</style>
+
+<template>
+ <Waterfall
+ :gutterWidth="10"
+ :gutterHeight="4">
+ <WaterfallItem v-for="(item, index) in files"
+ v-if="showWaterfall && item.thumb"
+ :key="index"
+ move-class="item-move">
+ <img :src="`${item.thumb}`">
+ <div :class="{ fixed }"
+ class="actions">
+ <b-tooltip label="Link"
+ position="is-top">
+ <a :href="`${item.url}`"
+ target="_blank">
+ <i class="icon-web-code"/>
+ </a>
+ </b-tooltip>
+ <b-tooltip label="Albums"
+ position="is-top">
+ <a @click="manageAlbums(item)">
+ <i class="icon-interface-window"/>
+ </a>
+ </b-tooltip>
+ <b-tooltip label="Tags"
+ position="is-top">
+ <a @click="manageTags(item)">
+ <i class="icon-ecommerce-tag-c"/>
+ </a>
+ </b-tooltip>
+ <b-tooltip label="Delete"
+ position="is-top">
+ <a @click="deleteFile(item, index)">
+ <i class="icon-editorial-trash-a-l"/>
+ </a>
+ </b-tooltip>
+ </div>
+ </WaterfallItem>
+ </Waterfall>
+</template>
+<script>
+import Waterfall from './waterfall/Waterfall.vue';
+import WaterfallItem from './waterfall/WaterfallItem.vue';
+
+export default {
+ components: {
+ Waterfall,
+ WaterfallItem
+ },
+ props: {
+ files: {
+ type: Array,
+ default: null
+ },
+ fixed: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return { showWaterfall: true };
+ },
+ mounted() {},
+ methods: {
+ deleteFile(file, index) {
+ this.$dialog.confirm({
+ title: 'Deleting file',
+ message: 'Are you sure you want to <b>delete</b> this file?',
+ confirmText: 'Delete File',
+ type: 'is-danger',
+ hasIcon: true,
+ onConfirm: async () => {
+ try {
+ const response = await this.axios.delete(`${this.$config.baseURL}/file/${file.id}`);
+ this.showWaterfall = false;
+ this.files.splice(index, 1);
+ this.$nextTick(() => {
+ this.showWaterfall = true;
+ });
+ return this.$toast.open(response.data.message);
+ } catch (error) {
+ return this.$onPromiseError(error);
+ }
+ }
+ });
+ }
+ }
+};
+</script>
diff --git a/src/site/components/grid/waterfall/Waterfall.vue b/src/site/components/grid/waterfall/Waterfall.vue
new file mode 100644
index 0000000..9827075
--- /dev/null
+++ b/src/site/components/grid/waterfall/Waterfall.vue
@@ -0,0 +1,181 @@
+<style>
+ .waterfall {
+ position: relative;
+ }
+</style>
+<template>
+ <div class="waterfall">
+ <slot/>
+ </div>
+</template>
+<script>
+// import {quickSort, getMinIndex, _, sum} from './util'
+
+const quickSort = (arr, type) => {
+ let left = [];
+ let right = [];
+ let povis;
+ if (arr.length <= 1) {
+ return arr;
+ }
+ povis = arr[0];
+ for (let i = 1; i < arr.length; i++) {
+ if (arr[i][type] < povis[type]) {
+ left.push(arr[i]);
+ } else {
+ right.push(arr[i]);
+ }
+ }
+ return quickSort(left, type).concat(povis, quickSort(right, type))
+};
+
+const getMinIndex = arr => {
+ let pos = 0;
+ for (let i = 0; i < arr.length; i++) {
+ if (arr[pos] > arr[i]) {
+ pos = i;
+ }
+ }
+ return pos;
+};
+
+const _ = {
+ on(el, type, func, capture = false) {
+ el.addEventListener(type, func, capture);
+ },
+ off(el, type, func, capture = false) {
+ el.removeEventListener(type, func, capture);
+ }
+};
+
+const sum = arr => arr.reduce((sum, val) => sum + val);
+export default {
+ name: 'Waterfall',
+ props: {
+ gutterWidth: {
+ type: Number,
+ default: 0
+ },
+ gutterHeight: {
+ type: Number,
+ default: 0
+ },
+ resizable: {
+ type: Boolean,
+ default: true
+ },
+ align: {
+ type: String,
+ default: 'center'
+ },
+ fixWidth: {
+ type: Number
+ },
+ minCol: {
+ type: Number,
+ default: 1
+ },
+ maxCol: {
+ type: Number
+ },
+ percent: {
+ type: Array
+ }
+ },
+ data() {
+ return {
+ timer: null,
+ colNum: 0,
+ lastWidth: 0,
+ percentWidthArr: []
+ };
+ },
+ created() {
+ this.$on('itemRender', () => {
+ if (this.timer) {
+ clearTimeout(this.timer);
+ }
+ this.timer = setTimeout(() => {
+ this.render();
+ }, 0);
+ });
+ },
+ mounted() {
+ this.resizeHandle();
+ this.$watch('resizable', this.resizeHandle);
+ },
+ methods: {
+ calulate(arr) {
+ let pageWidth = this.fixWidth ? this.fixWidth : this.$el.offsetWidth;
+ // 百分比布局计算
+ if (this.percent) {
+ this.colNum = this.percent.length;
+ const total = sum(this.percent);
+ this.percentWidthArr = this.percent.map(value => (value / total) * pageWidth);
+ this.lastWidth = 0;
+ // 正常布局计算
+ } else {
+ this.colNum = parseInt(pageWidth / (arr.width + this.gutterWidth));
+ if (this.minCol && this.colNum < this.minCol) {
+ this.colNum = this.minCol;
+ this.lastWidth = 0;
+ } else if (this.maxCol && this.colNum > this.maxCol) {
+ this.colNum = this.maxCol;
+ this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
+ } else {
+ this.lastWidth = pageWidth - (arr.width + this.gutterWidth) * this.colNum + this.gutterWidth;
+ }
+ }
+ },
+ resizeHandle() {
+ if (this.resizable) {
+ _.on(window, 'resize', this.render, false);
+ } else {
+ _.off(window, 'resize', this.render, false);
+ }
+ },
+ render() {
+ // 重新排序
+ let childArr = [];
+ childArr = this.$children.map(child => child.getMeta());
+ childArr = quickSort(childArr, 'order');
+ // 计算列数
+ this.calulate(childArr[0])
+ let offsetArr = Array(this.colNum).fill(0);
+ // 渲染
+ childArr.forEach(child => {
+ let position = getMinIndex(offsetArr);
+ // 百分比布局渲染
+ if (this.percent) {
+ let left = 0;
+ child.el.style.width = `${this.percentWidthArr[position]}px`;
+ if (position === 0) {
+ left = 0;
+ } else {
+ for (let i = 0; i < position; i++) {
+ left += this.percentWidthArr[i];
+ }
+ }
+ child.el.style.left = `${left}px`;
+ // 正常布局渲染
+ } else {
+ if (this.align === 'left') { // eslint-disable-line no-lonely-if
+ child.el.style.left = `${position * (child.width + this.gutterWidth)}px`;
+ } else if (this.align === 'right') {
+ child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth}px`;
+ } else {
+ child.el.style.left = `${position * (child.width + this.gutterWidth) + this.lastWidth / 2}px`;
+ }
+ }
+ if (child.height === 0) {
+ return;
+ }
+ child.el.style.top = `${offsetArr[position]}px`;
+ offsetArr[position] += (child.height + this.gutterHeight);
+ this.$el.style.height = `${Math.max.apply(Math, offsetArr)}px`;
+ });
+ this.$emit('rendered', this);
+ }
+ }
+};
+</script>
diff --git a/src/site/components/grid/waterfall/WaterfallItem.vue b/src/site/components/grid/waterfall/WaterfallItem.vue
new file mode 100644
index 0000000..597cca6
--- /dev/null
+++ b/src/site/components/grid/waterfall/WaterfallItem.vue
@@ -0,0 +1,60 @@
+<style>
+ .waterfall-item {
+ position: absolute;
+ }
+</style>
+<template>
+ <div class="waterfall-item">
+ <slot/>
+ </div>
+</template>
+<script>
+import imagesLoaded from 'imagesloaded';
+export default {
+ name: 'WaterfallItem',
+ props: {
+ order: {
+ type: Number,
+ default: 0
+ },
+ width: {
+ type: Number,
+ default: 150
+ }
+ },
+ data() {
+ return {
+ itemWidth: 0,
+ height: 0
+ };
+ },
+ created() {
+ this.$watch(() => this.height, this.emit);
+ },
+ mounted() {
+ this.$el.style.display = 'none';
+ this.$el.style.width = `${this.width}px`;
+ this.emit();
+ imagesLoaded(this.$el, () => {
+ this.$el.style.left = '-9999px';
+ this.$el.style.top = '-9999px';
+ this.$el.style.display = 'block';
+ this.height = this.$el.offsetHeight;
+ this.itemWidth = this.$el.offsetWidth;
+ });
+ },
+ methods: {
+ emit() {
+ this.$parent.$emit('itemRender');
+ },
+ getMeta() {
+ return {
+ el: this.$el,
+ height: this.height,
+ width: this.itemWidth,
+ order: this.order
+ };
+ }
+ }
+}
+</script>
diff --git a/src/site/components/grid/waterfall/old/waterfall-slot.vue b/src/site/components/grid/waterfall/old/waterfall-slot.vue
new file mode 100644
index 0000000..07ca268
--- /dev/null
+++ b/src/site/components/grid/waterfall/old/waterfall-slot.vue
@@ -0,0 +1,76 @@
+<template>
+ <div class="vue-waterfall-slot" v-show="isShow">
+ <slot></slot>
+ </div>
+</template>
+
+<style>
+.vue-waterfall-slot {
+ position: absolute;
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+</style>
+
+<script>
+
+export default {
+ data: () => ({
+ isShow: false
+ }),
+ props: {
+ width: {
+ required: true,
+ validator: (val) => val >= 0
+ },
+ height: {
+ required: true,
+ validator: (val) => val >= 0
+ },
+ order: {
+ default: 0
+ },
+ moveClass: {
+ default: ''
+ }
+ },
+ methods: {
+ notify () {
+ this.$parent.$emit('reflow', this)
+ },
+ getMeta () {
+ return {
+ vm: this,
+ node: this.$el,
+ order: this.order,
+ width: this.width,
+ height: this.height,
+ moveClass: this.moveClass
+ }
+ }
+ },
+ created () {
+ this.rect = {
+ top: 0,
+ left: 0,
+ width: 0,
+ height: 0
+ }
+ this.$watch(() => (
+ this.width,
+ this.height
+ ), this.notify)
+ },
+ mounted () {
+ this.$parent.$once('reflowed', () => {
+ this.isShow = true
+ })
+ this.notify()
+ },
+ destroyed () {
+ this.notify()
+ }
+}
+
+</script>
diff --git a/src/site/components/grid/waterfall/old/waterfall.vue b/src/site/components/grid/waterfall/old/waterfall.vue
new file mode 100644
index 0000000..84e3c98
--- /dev/null
+++ b/src/site/components/grid/waterfall/old/waterfall.vue
@@ -0,0 +1,442 @@
+<template>
+ <div class="vue-waterfall" :style="style">
+ <slot></slot>
+ </div>
+</template>
+
+<style>
+.vue-waterfall {
+ position: relative;
+ /*overflow: hidden; cause clientWidth = 0 in IE if height not bigger than 0 */
+}
+</style>
+
+<script>
+
+const MOVE_CLASS_PROP = '_wfMoveClass'
+
+export default {
+ props: {
+ autoResize: {
+ default: true
+ },
+ interval: {
+ default: 200,
+ validator: (val) => val >= 0
+ },
+ align: {
+ default: 'left',
+ validator: (val) => ~['left', 'right', 'center'].indexOf(val)
+ },
+ line: {
+ default: 'v',
+ validator: (val) => ~['v', 'h'].indexOf(val)
+ },
+ lineGap: {
+ required: true,
+ validator: (val) => val >= 0
+ },
+ minLineGap: {
+ validator: (val) => val >= 0
+ },
+ maxLineGap: {
+ validator: (val) => val >= 0
+ },
+ singleMaxWidth: {
+ validator: (val) => val >= 0
+ },
+ fixedHeight: {
+ default: false
+ },
+ grow: {
+ validator: (val) => val instanceof Array
+ },
+ watch: {
+ default: () => ({})
+ }
+ },
+ data: () => ({
+ style: {
+ height: '',
+ overflow: ''
+ },
+ token: null
+ }),
+ methods: {
+ reflowHandler,
+ autoResizeHandler,
+ reflow
+ },
+ created () {
+ this.virtualRects = []
+ this.$on('reflow', () => {
+ this.reflowHandler()
+ })
+ this.$watch(() => (
+ this.align,
+ this.line,
+ this.lineGap,
+ this.minLineGap,
+ this.maxLineGap,
+ this.singleMaxWidth,
+ this.fixedHeight,
+ this.watch
+ ), this.reflowHandler)
+ this.$watch('grow', this.reflowHandler)
+ },
+ mounted () {
+ this.$watch('autoResize', this.autoResizeHandler)
+ on(this.$el, getTransitionEndEvent(), tidyUpAnimations, true)
+ this.autoResizeHandler(this.autoResize)
+ },
+ beforeDestroy () {
+ this.autoResizeHandler(false)
+ off(this.$el, getTransitionEndEvent(), tidyUpAnimations, true)
+ }
+}
+
+function autoResizeHandler (autoResize) {
+ if (autoResize === false || !this.autoResize) {
+ off(window, 'resize', this.reflowHandler, false)
+ } else {
+ on(window, 'resize', this.reflowHandler, false)
+ }
+}
+
+function tidyUpAnimations (event) {
+ let node = event.target
+ let moveClass = node[MOVE_CLASS_PROP]
+ if (moveClass) {
+ removeClass(node, moveClass)
+ }
+}
+
+function reflowHandler () {
+ clearTimeout(this.token)
+ this.token = setTimeout(this.reflow, this.interval)
+}
+
+function reflow () {
+ if (!this.$el) { return }
+ let width = this.$el.clientWidth
+ let metas = this.$children.map((slot) => slot.getMeta())
+ metas.sort((a, b) => a.order - b.order)
+ this.virtualRects = metas.map(() => ({}))
+ calculate(this, metas, this.virtualRects)
+ setTimeout(() => {
+ if (isScrollBarVisibilityChange(this.$el, width)) {
+ calculate(this, metas, this.virtualRects)
+ }
+ this.style.overflow = 'hidden'
+ render(this.virtualRects, metas)
+ this.$emit('reflowed', this)
+ }, 0)
+}
+
+function isScrollBarVisibilityChange (el, lastClientWidth) {
+ return lastClientWidth !== el.clientWidth
+}
+
+function calculate (vm, metas, styles) {
+ let options = getOptions(vm)
+ let processor = vm.line === 'h' ? horizontalLineProcessor : verticalLineProcessor
+ processor.calculate(vm, options, metas, styles)
+}
+
+function getOptions (vm) {
+ const maxLineGap = vm.maxLineGap ? +vm.maxLineGap : vm.lineGap
+ return {
+ align: ~['left', 'right', 'center'].indexOf(vm.align) ? vm.align : 'left',
+ line: ~['v', 'h'].indexOf(vm.line) ? vm.line : 'v',
+ lineGap: +vm.lineGap,
+ minLineGap: vm.minLineGap ? +vm.minLineGap : vm.lineGap,
+ maxLineGap: maxLineGap,
+ singleMaxWidth: Math.max(vm.singleMaxWidth || 0, maxLineGap),
+ fixedHeight: !!vm.fixedHeight,
+ grow: vm.grow && vm.grow.map(val => +val)
+ }
+}
+
+var verticalLineProcessor = (() => {
+
+ function calculate (vm, options, metas, rects) {
+ let width = vm.$el.clientWidth
+ let grow = options.grow
+ let strategy = grow
+ ? getRowStrategyWithGrow(width, grow)
+ : getRowStrategy(width, options)
+ let tops = getArrayFillWith(0, strategy.count)
+ metas.forEach((meta, index) => {
+ let offset = tops.reduce((last, top, i) => top < tops[last] ? i : last, 0)
+ let width = strategy.width[offset % strategy.count]
+ let rect = rects[index]
+ rect.top = tops[offset]
+ rect.left = strategy.left + (offset ? sum(strategy.width.slice(0, offset)) : 0)
+ rect.width = width
+ rect.height = meta.height * (options.fixedHeight ? 1 : width / meta.width)
+ tops[offset] = tops[offset] + rect.height
+ })
+ vm.style.height = Math.max.apply(Math, tops) + 'px'
+ }
+
+ function getRowStrategy (width, options) {
+ let count = width / options.lineGap
+ let slotWidth
+ if (options.singleMaxWidth >= width) {
+ count = 1
+ slotWidth = Math.max(width, options.minLineGap)
+ } else {
+ let maxContentWidth = options.maxLineGap * ~~count
+ let minGreedyContentWidth = options.minLineGap * ~~(count + 1)
+ let canFit = maxContentWidth >= width
+ let canFitGreedy = minGreedyContentWidth <= width
+ if (canFit && canFitGreedy) {
+ count = Math.round(count)
+ slotWidth = width / count
+ } else if (canFit) {
+ count = ~~count
+ slotWidth = width / count
+ } else if (canFitGreedy) {
+ count = ~~(count + 1)
+ slotWidth = width / count
+ } else {
+ count = ~~count
+ slotWidth = options.maxLineGap
+ }
+ if (count === 1) {
+ slotWidth = Math.min(width, options.singleMaxWidth)
+ slotWidth = Math.max(slotWidth, options.minLineGap)
+ }
+ }
+ return {
+ width: getArrayFillWith(slotWidth, count),
+ count: count,
+ left: getLeft(width, slotWidth * count, options.align)
+ }
+ }
+
+ function getRowStrategyWithGrow (width, grow) {
+ let total = sum(grow)
+ return {
+ width: grow.map(val => width * val / total),
+ count: grow.length,
+ left: 0
+ }
+ }
+
+ return {
+ calculate
+ }
+
+})()
+
+var horizontalLineProcessor = (() => {
+
+ function calculate (vm, options, metas, rects) {
+ let width = vm.$el.clientWidth
+ let total = metas.length
+ let top = 0
+ let offset = 0
+ while (offset < total) {
+ let strategy = getRowStrategy(width, options, metas, offset)
+ for (let i = 0, left = 0, meta, rect; i < strategy.count; i++) {
+ meta = metas[offset + i]
+ rect = rects[offset + i]
+ rect.top = top
+ rect.left = strategy.left + left
+ rect.width = meta.width * strategy.height / meta.height
+ rect.height = strategy.height
+ left += rect.width
+ }
+ offset += strategy.count
+ top += strategy.height
+ }
+ vm.style.height = top + 'px'
+ }
+
+ function getRowStrategy (width, options, metas, offset) {
+ let greedyCount = getGreedyCount(width, options.lineGap, metas, offset)
+ let lazyCount = Math.max(greedyCount - 1, 1)
+ let greedySize = getContentSize(width, options, metas, offset, greedyCount)
+ let lazySize = getContentSize(width, options, metas, offset, lazyCount)
+ let finalSize = chooseFinalSize(lazySize, greedySize, width)
+ let height = finalSize.height
+ let fitContentWidth = finalSize.width
+ if (finalSize.count === 1) {
+ fitContentWidth = Math.min(options.singleMaxWidth, width)
+ height = metas[offset].height * fitContentWidth / metas[offset].width
+ }
+ return {
+ left: getLeft(width, fitContentWidth, options.align),
+ count: finalSize.count,
+ height: height
+ }
+ }
+
+ function getGreedyCount (rowWidth, rowHeight, metas, offset) {
+ let count = 0
+ for (let i = offset, width = 0; i < metas.length && width <= rowWidth; i++) {
+ width += metas[i].width * rowHeight / metas[i].height
+ count++
+ }
+ return count
+ }
+
+ function getContentSize (rowWidth, options, metas, offset, count) {
+ let originWidth = 0
+ for (let i = count - 1; i >= 0; i--) {
+ let meta = metas[offset + i]
+ originWidth += meta.width * options.lineGap / meta.height
+ }
+ let fitHeight = options.lineGap * rowWidth / originWidth
+ let canFit = (fitHeight <= options.maxLineGap && fitHeight >= options.minLineGap)
+ if (canFit) {
+ return {
+ cost: Math.abs(options.lineGap - fitHeight),
+ count: count,
+ width: rowWidth,
+ height: fitHeight
+ }
+ } else {
+ let height = originWidth > rowWidth ? options.minLineGap : options.maxLineGap
+ return {
+ cost: Infinity,
+ count: count,
+ width: originWidth * height / options.lineGap,
+ height: height
+ }
+ }
+ }
+
+ function chooseFinalSize (lazySize, greedySize, rowWidth) {
+ if (lazySize.cost === Infinity && greedySize.cost === Infinity) {
+ return greedySize.width < rowWidth ? greedySize : lazySize
+ } else {
+ return greedySize.cost >= lazySize.cost ? lazySize : greedySize
+ }
+ }
+
+ return {
+ calculate
+ }
+
+})()
+
+function getLeft (width, contentWidth, align) {
+ switch (align) {
+ case 'right':
+ return width - contentWidth
+ case 'center':
+ return (width - contentWidth) / 2
+ default:
+ return 0
+ }
+}
+
+function sum (arr) {
+ return arr.reduce((sum, val) => sum + val)
+}
+
+function render (rects, metas) {
+ let metasNeedToMoveByTransform = metas.filter((meta) => meta.moveClass)
+ let firstRects = getRects(metasNeedToMoveByTransform)
+ applyRects(rects, metas)
+ let lastRects = getRects(metasNeedToMoveByTransform)
+ metasNeedToMoveByTransform.forEach((meta, i) => {
+ meta.node[MOVE_CLASS_PROP] = meta.moveClass
+ setTransform(meta.node, firstRects[i], lastRects[i])
+ })
+ document.body.clientWidth // forced reflow
+ metasNeedToMoveByTransform.forEach((meta) => {
+ addClass(meta.node, meta.moveClass)
+ clearTransform(meta.node)
+ })
+}
+
+function getRects (metas) {
+ return metas.map((meta) => meta.vm.rect)
+}
+
+function applyRects (rects, metas) {
+ rects.forEach((rect, i) => {
+ let style = metas[i].node.style
+ metas[i].vm.rect = rect
+ for (let prop in rect) {
+ style[prop] = rect[prop] + 'px'
+ }
+ })
+}
+
+function setTransform (node, firstRect, lastRect) {
+ let dx = firstRect.left - lastRect.left
+ let dy = firstRect.top - lastRect.top
+ let sw = firstRect.width / lastRect.width
+ let sh = firstRect.height / lastRect.height
+ node.style.transform =
+ node.style.WebkitTransform = `translate(${dx}px,${dy}px) scale(${sw},${sh})`
+ node.style.transitionDuration = '0s'
+}
+
+function clearTransform (node) {
+ node.style.transform = node.style.WebkitTransform = ''
+ node.style.transitionDuration = ''
+}
+
+function getTransitionEndEvent () {
+ let isWebkitTrans =
+ window.ontransitionend === undefined &&
+ window.onwebkittransitionend !== undefined
+ let transitionEndEvent = isWebkitTrans
+ ? 'webkitTransitionEnd'
+ : 'transitionend'
+ return transitionEndEvent
+}
+
+/**
+ * util
+ */
+
+function getArrayFillWith (item, count) {
+ let getter = (typeof item === 'function') ? () => item() : () => item
+ let arr = []
+ for (let i = 0; i < count; i++) {
+ arr[i] = getter()
+ }
+ return arr
+}
+
+function addClass (elem, name) {
+ if (!hasClass(elem, name)) {
+ let cur = attr(elem, 'class').trim()
+ let res = (cur + ' ' + name).trim()
+ attr(elem, 'class', res)
+ }
+}
+
+function removeClass (elem, name) {
+ let reg = new RegExp('\\s*\\b' + name + '\\b\\s*', 'g')
+ let res = attr(elem, 'class').replace(reg, ' ').trim()
+ attr(elem, 'class', res)
+}
+
+function hasClass (elem, name) {
+ return (new RegExp('\\b' + name + '\\b')).test(attr(elem, 'class'))
+}
+
+function attr (elem, name, value) {
+ if (typeof value !== 'undefined') {
+ elem.setAttribute(name, value)
+ } else {
+ return elem.getAttribute(name) || ''
+ }
+}
+
+function on (elem, type, listener, useCapture = false) {
+ elem.addEventListener(type, listener, useCapture)
+}
+
+function off (elem, type, listener, useCapture = false) {
+ elem.removeEventListener(type, listener, useCapture)
+}
+
+</script>
diff --git a/src/site/components/home/links/Links.vue b/src/site/components/home/links/Links.vue
new file mode 100644
index 0000000..ba1e493
--- /dev/null
+++ b/src/site/components/home/links/Links.vue
@@ -0,0 +1,100 @@
+<style lang="scss" scoped>
+ @import '../../../styles/_colors.scss';
+ .links {
+ margin-bottom: 3em;
+ align-items: stretch;
+ display: flex;
+ justify-content: space-between;
+
+ div.link { cursor: pointer; }
+ .link {
+ background: $backgroundAccent;
+ display: block;
+ width: calc(25% - 2rem);
+ border-radius: 6px;
+ box-shadow: 0 1.5rem 1.5rem -1.25rem rgba(10,10,10,.05);
+ transition-duration: 86ms;
+ transition-property: box-shadow,-webkit-transform;
+ transition-property: box-shadow,transform;
+ transition-property: box-shadow,transform,-webkit-transform;
+ will-change: box-shadow,transform;
+
+ header.bd-footer-star-header {
+ padding: 1.5rem;
+
+ &:hover .bd-footer-subtitle { color: $textColorHighlight; }
+
+ h4.bd-footer-title {
+ color: $textColorHighlight;
+ font-size: 1.5rem;
+ line-height: 1.25;
+ margin-bottom: .5rem;
+ transition-duration: 86ms;
+ transition-property: color;
+ font-weight: 700;
+ }
+
+ p.bd-footer-subtitle {
+ color: $textColor;
+ margin-top: -.5rem;
+ transition-duration: 86ms;
+ transition-property: color;
+ font-weight: 400;
+ }
+ }
+
+ &:hover {
+ box-shadow: 0 3rem 3rem -1.25rem rgba(10,10,10,.1);
+ -webkit-transform: translateY(-.5rem);
+ transform: translateY(-.5rem);
+ }
+ }
+ }
+
+ @media screen and (max-width: 768px) {
+ .links {
+ display: block;
+ padding: 0px 2em;
+ .link {
+ width: 100%;
+ margin-bottom: 1.5em;
+ }
+ }
+ }
+</style>
+<template>
+ <div class="links">
+ <a href="https://github.com/WeebDev/lolisafe"
+ target="_blank"
+ class="link">
+ <header class="bd-footer-star-header">
+ <h4 class="bd-footer-title">GitHub</h4>
+ <p class="bd-footer-subtitle">Deploy your own lolisafe</p>
+ </header>
+ </a>
+ <div class="link">
+ <header class="bd-footer-star-header">
+ <h4 class="bd-footer-title">ShareX</h4>
+ <p class="bd-footer-subtitle">Upload from your Desktop</p>
+ </header>
+ </div>
+ <a href="https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj"
+ target="_blank"
+ class="link">
+ <header class="bd-footer-star-header">
+ <h4 class="bd-footer-title">Extension</h4>
+ <p class="bd-footer-subtitle">Upload from any website</p>
+ </header>
+ </a>
+ <router-link to="faq"
+ class="link">
+ <header class="bd-footer-star-header">
+ <h4 class="bd-footer-title">FAQ</h4>
+ <p class="bd-footer-subtitle">dunno</p>
+ </header>
+ </router-link>
+ </div>
+</template>
+<script>
+export default {}
+</script>
diff --git a/src/site/components/loading/CubeShadow.vue b/src/site/components/loading/CubeShadow.vue
new file mode 100644
index 0000000..af31dac
--- /dev/null
+++ b/src/site/components/loading/CubeShadow.vue
@@ -0,0 +1,48 @@
+<template>
+ <div :style="styles"
+ class="spinner spinner--cube-shadow" />
+</template>
+
+<script>
+export default {
+ props: {
+ size: {
+ type: String,
+ default: '60px'
+ },
+ background: {
+ type: String,
+ default: '#9C27B0'
+ },
+ duration: {
+ type: String,
+ default: '1.8s'
+ }
+ },
+ computed: {
+ styles() {
+ return {
+ width: this.size,
+ height: this.size,
+ backgroundColor: this.background,
+ animationDuration: this.duration
+ };
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+ .spinner{
+ animation: cube-shadow-spinner 1.8s cubic-bezier(0.75, 0, 0.5, 1) infinite;
+ }
+ @keyframes cube-shadow-spinner {
+ 50% {
+ border-radius: 50%;
+ transform: scale(0.5) rotate(360deg);
+ }
+ 100% {
+ transform: scale(1) rotate(720deg);
+ }
+ }
+</style>
diff --git a/src/site/components/loading/Origami.vue b/src/site/components/loading/Origami.vue
new file mode 100644
index 0000000..d1b523d
--- /dev/null
+++ b/src/site/components/loading/Origami.vue
@@ -0,0 +1,121 @@
+<template>
+ <div :style="styles"
+ class="spinner spinner-origami">
+ <div :style="innerStyles"
+ class="spinner-inner loading">
+ <span class="slice" />
+ <span class="slice" />
+ <span class="slice" />
+ <span class="slice" />
+ <span class="slice" />
+ <span class="slice" />
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ props: {
+ size: {
+ type: String,
+ default: '40px'
+ }
+ },
+ computed: {
+ innerStyles() {
+ let size = parseInt(this.size);
+ return { transform: `scale(${(size / 60)})` };
+ },
+ styles() {
+ return {
+ width: this.size,
+ height: this.size
+ };
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import '../../styles/colors.scss';
+
+@for $i from 1 through 6 {
+ @keyframes origami-show-#{$i}{
+ from{
+ transform: rotateZ(60* $i + deg) rotateY(-90deg) rotateX(0deg);
+ border-left-color: #31855e;
+ }
+ }
+ @keyframes origami-hide-#{$i}{
+ to{
+ transform: rotateZ(60* $i + deg) rotateY(-90deg) rotateX(0deg);
+ border-left-color: #31855e;
+ }
+ }
+
+ @keyframes origami-cycle-#{$i} {
+
+ $startIndex: $i*5;
+ $reverseIndex: (80 - $i*5);
+
+ #{$startIndex * 1%} {
+ transform: rotateZ(60* $i + deg) rotateY(90deg) rotateX(0deg);
+ border-left-color: #31855e;
+ }
+ #{$startIndex + 5%},
+ #{$reverseIndex * 1%} {
+ transform: rotateZ(60* $i + deg) rotateY(0) rotateX(0deg);
+ border-left-color: #41b883;
+ }
+
+ #{$reverseIndex + 5%},
+ 100%{
+ transform: rotateZ(60* $i + deg) rotateY(90deg) rotateX(0deg);
+ border-left-color: #31855e;
+ }
+ }
+}
+
+.spinner{
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ * {
+ line-height: 0;
+ box-sizing: border-box;
+ }
+}
+.spinner-inner{
+ display: block;
+ width: 60px;
+ height: 68px;
+ .slice {
+ border-top: 18px solid transparent;
+ border-right: none;
+ border-bottom: 16px solid transparent;
+ border-left: 30px solid #f7484e;
+ position: absolute;
+ top: 0px;
+ left: 50%;
+ transform-origin: left bottom;
+ border-radius: 3px 3px 0 0;
+ }
+
+ @for $i from 1 through 6 {
+ .slice:nth-child(#{$i}) {
+ transform: rotateZ(60* $i + deg) rotateY(0deg) rotateX(0);
+ animation: .15s linear .9 - $i*.08s origami-hide-#{$i} both 1;
+ }
+ }
+
+ &.loading{
+ @for $i from 1 through 6 {
+ .slice:nth-child(#{$i}) {
+ transform: rotateZ(60* $i + deg) rotateY(90deg) rotateX(0);
+ animation: 2s origami-cycle-#{$i} linear infinite both;
+ }
+ }
+ }
+
+}
+</style>
diff --git a/src/site/components/loading/PingPong.vue b/src/site/components/loading/PingPong.vue
new file mode 100644
index 0000000..ac33e28
--- /dev/null
+++ b/src/site/components/loading/PingPong.vue
@@ -0,0 +1,98 @@
+<template>
+ <div :style="styles"
+ class="spinner spinner--ping-pong">
+ <div :style="innerStyles"
+ class="spinner-inner">
+ <div class="board">
+ <div class="left"/>
+ <div class="right"/>
+ <div class="ball"/>
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+export default {
+ props: {
+ size: {
+ type: String,
+ default: '60px'
+ }
+ },
+ computed: {
+ innerStyles() {
+ let size = parseInt(this.size);
+ return { transform: `scale(${size / 250})` };
+ },
+ styles() {
+ return {
+ width: this.size,
+ height: this.size
+ };
+ }
+ }
+}
+</script>
+
+<style lang="scss" scoped>
+ .spinner{
+ overflow: hidden;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ * {
+ line-height: 0;
+ box-sizing: border-box;
+ }
+ }
+ .board {
+ width:250px;
+ position: relative;
+ }
+ .left,
+ .right {
+ height:50px;
+ width:15px;
+ background:#41b883;
+ display: inline-block;
+ position:absolute;
+ }
+ .left {
+ left:0;
+ animation: pingpong-position1 2s linear infinite;
+ }
+ .right {
+ right:0;
+ animation: pingpong-position2 2s linear infinite;
+ }
+ .ball{
+ width:15px;
+ height:15px;
+ border-radius:50%;
+ background:#f7484e;
+ position:absolute;
+ animation: pingpong-bounce 2s linear infinite;
+ }
+ @keyframes pingpong-position1 {
+ 0% {top:-60px;}
+ 25% {top:0;}
+ 50% {top:60px;}
+ 75% {top:-60px;}
+ 100% {top:-60px;}
+ }
+ @keyframes pingpong-position2 {
+ 0% {top:60px;}
+ 25% {top:0;}
+ 50% {top:-60px;}
+ 75% {top:-60px;}
+ 100% {top:60px;}
+ }
+ @keyframes pingpong-bounce {
+ 0% {top:-35px;left:10px;}
+ 25% {top:25px;left:225px;}
+ 50% {top:75px;left:10px;}
+ 75% {top:-35px;left:225px;}
+ 100% {top:-35px;left:10px;}
+ }
+</style>
diff --git a/src/site/components/loading/RotateSquare.vue b/src/site/components/loading/RotateSquare.vue
new file mode 100644
index 0000000..4da8300
--- /dev/null
+++ b/src/site/components/loading/RotateSquare.vue
@@ -0,0 +1,87 @@
+<template>
+ <div :style="styles"
+ class="spinner spinner--rotate-square-2" />
+</template>
+
+<script>
+export default {
+ props: {
+ size: {
+ type: String,
+ default: '40px'
+ }
+ },
+ computed: {
+ styles() {
+ return {
+ width: this.size,
+ height: this.size,
+ display: 'inline-block'
+ };
+ }
+ }
+};
+</script>
+
+<style lang="scss" scoped>
+@import '../../styles/colors.scss';
+
+.spinner {
+ position: relative;
+ * {
+ line-height: 0;
+ box-sizing: border-box;
+ }
+ &:before {
+ content: '';
+ width: 100%;
+ height: 20%;
+ min-width: 5px;
+ background: #000;
+ opacity: 0.1;
+ position: absolute;
+ bottom: 0%;
+ left: 0;
+ border-radius: 50%;
+ animation: rotate-square-2-shadow .5s linear infinite;
+ }
+ &:after {
+ content: '';
+ width: 100%;
+ height: 100%;
+ background: $basePink;
+ animation: rotate-square-2-animate .5s linear infinite;
+ position: absolute;
+ bottom:40%;
+ left: 0;
+ border-radius: 3px;
+ }
+}
+
+@keyframes rotate-square-2-animate {
+ 17% {
+ border-bottom-right-radius: 3px;
+ }
+ 25% {
+ transform: translateY(20%) rotate(22.5deg);
+ }
+ 50% {
+ transform: translateY(40%) scale(1, .9) rotate(45deg);
+ border-bottom-right-radius: 50%;
+ }
+ 75% {
+ transform: translateY(20%) rotate(67.5deg);
+ }
+ 100% {
+ transform: translateY(0) rotate(90deg);
+ }
+}
+@keyframes rotate-square-2-shadow {
+ 0%, 100% {
+ transform: scale(1, 1);
+ }
+ 50% {
+ transform: scale(1.2, 1);
+ }
+}
+</style>
diff --git a/src/site/components/logo/Logo.vue b/src/site/components/logo/Logo.vue
new file mode 100644
index 0000000..d594c7e
--- /dev/null
+++ b/src/site/components/logo/Logo.vue
@@ -0,0 +1,59 @@
+<style lang="scss" scoped>
+ @import '../../styles/_colors.scss';
+ #logo {
+ -webkit-animation-delay: 0.5s;
+ animation-delay: 0.5s;
+ -webkit-animation-duration: 1.5s;
+ animation-duration: 1.5s;
+ -webkit-animation-fill-mode: both;
+ animation-fill-mode: both;
+ -webkit-animation-name: floatUp;
+ animation-name: floatUp;
+ -webkit-animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
+ animation-timing-function: cubic-bezier(0, 0.71, 0.29, 1);
+ border-radius: 24px;
+ display: inline-block;
+ height: 240px;
+ position: relative;
+ vertical-align: top;
+ width: 240px;
+ box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
+ background: $backgroundAccent;
+ pointer-events: none;
+ }
+
+ #logo img {
+ height: 200px;
+ margin-top: 20px;
+ }
+
+ @keyframes floatUp {
+ 0% {
+ opacity: 0;
+ box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0), 0 0 0 rgba(10, 10, 10, 0);
+ -webkit-transform: scale(0.86);
+ transform: scale(0.86);
+ }
+ 25% { opacity: 100; }
+ 67% {
+ box-shadow: 0 0 0 rgba(10, 10, 10, 0), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+ 100% {
+ box-shadow: 0 20px 60px rgba(10, 10, 10, 0.05), 0 5px 10px rgba(10, 10, 10, 0.1), 0 1px 1px rgba(10, 10, 10, 0.2);
+ -webkit-transform: scale(1);
+ transform: scale(1);
+ }
+ }
+</style>
+
+<template>
+ <p id="logo">
+ <img src="../../../../public/images/logo.png">
+ </p>
+</template>
+
+<script>
+export default {};
+</script>
diff --git a/src/site/components/navbar/Navbar.vue b/src/site/components/navbar/Navbar.vue
new file mode 100644
index 0000000..108c150
--- /dev/null
+++ b/src/site/components/navbar/Navbar.vue
@@ -0,0 +1,119 @@
+<style lang="scss" scoped>
+ @import '../../styles/colors.scss';
+ nav.navbar {
+ background: transparent;
+ box-shadow: none;
+
+ .navbar-brand {
+ width: 100%;
+ align-items: flex-start;
+ padding: 1em;
+
+ div.spacer { flex: 1 0 10px; }
+ a.navbar-item {
+ color: $defaultTextColor;
+ font-size: 16px;
+ font-weight: 700;
+ text-decoration-style: solid;
+ }
+ a.navbar-item:hover, a.navbar-item.is-active, a.navbar-link:hover, a.navbar-link.is-active {
+ text-decoration: underline;
+ background: transparent;
+ }
+
+ i {
+ font-size: 2em;
+ &.hidden {
+ width: 0px;
+ height: 1.5em;
+ pointer-events: none;
+ }
+ }
+ }
+
+ &.isWhite {
+ .navbar-brand {
+ a.navbar-item {
+ color: white;
+ }
+ }
+ }
+ }
+</style>
+
+<template>
+ <nav :class="{ isWhite }"
+ class="navbar is-transparent">
+ <div class="navbar-brand">
+ <router-link to="/"
+ class="navbar-item no-active">
+ <i class="icon-ecommerce-safebox"/> {{ config.serviceName }}
+ </router-link>
+
+ <!--
+ <template v-if="loggedIn">
+ <router-link
+ to="/dashboard/uploads"
+ class="navbar-item no-active"
+ exact><i class="hidden"/>Uploads</router-link>
+
+ <router-link
+ to="/dashboard/albums"
+ class="navbar-item no-active"
+ exact><i class="hidden"/>Albums</router-link>
+
+ <router-link
+ to="/dashboard/tags"
+ class="navbar-item no-active"
+ exact><i class="hidden"/>Tags</router-link>
+
+ <router-link
+ to="/dashboard/settings"
+ class="navbar-item no-active"
+ exact><i class="hidden"/>Settings</router-link>
+ </template>
+ -->
+
+ <div class="spacer" />
+
+ <router-link v-if="!loggedIn"
+ class="navbar-item"
+ to="/login"><i class="hidden"/>Login</router-link>
+
+ <router-link v-else
+ to="/dashboard"
+ class="navbar-item no-active"
+ exact><i class="hidden"/>Dashboard</router-link>
+ </div>
+ </nav>
+</template>
+
+<script>
+export default {
+ props: {
+ isWhite: {
+ type: Boolean,
+ default: false
+ }
+ },
+ data() {
+ return { hamburger: false };
+ },
+ computed: {
+ loggedIn() {
+ return this.$store.state.loggedIn;
+ },
+ user() {
+ return this.$store.state.user;
+ },
+ config() {
+ return this.$store.state.config;
+ }
+ },
+ methods: {
+ logOut() {
+ this.$emit('logout');
+ }
+ }
+};
+</script>
diff --git a/src/site/components/sidebar/Sidebar.vue b/src/site/components/sidebar/Sidebar.vue
new file mode 100644
index 0000000..861ebea
--- /dev/null
+++ b/src/site/components/sidebar/Sidebar.vue
@@ -0,0 +1,43 @@
+<style lang="scss" scoped>
+ @import '../../styles/colors.scss';
+ .dashboard-menu {
+ a {
+ display: block;
+ font-weight: 700;
+ color: #868686;
+ position: relative;
+ padding-left: 40px;
+ height: 35px;
+ &:hover, &:first-child {
+ color: $defaultTextColor;
+ }
+
+ i {
+ position: absolute;
+ font-size: 1.5em;
+ top: -4px;
+ left: 5px;
+ }
+ }
+
+ hr { margin-top: 0.6em; }
+ }
+</style>
+<template>
+ <div class="dashboard-menu">
+ <router-link to="/"><i class="icon-ecommerce-safebox"/>lolisafe</router-link>
+ <hr>
+ <a><i class="icon-interface-cloud-upload"/>Upload files</a>
+ <hr>
+ <router-link to="/dashboard"><i class="icon-com-pictures"/>Files</router-link>
+ <router-link to="/dashboard/albums"><i class="icon-interface-window"/>Albums</router-link>
+ <router-link to="/dashboard/tags"><i class="icon-ecommerce-tag-c"/>Tags</router-link>
+ <hr>
+ <router-link to="/dashboard/settings"><i class="icon-setting-gear-a"/>Settings</router-link>
+ </div>
+</template>
+<script>
+export default {
+
+}
+</script>
diff --git a/src/site/components/uploader/Uploader.vue b/src/site/components/uploader/Uploader.vue
new file mode 100644
index 0000000..fcb79cb
--- /dev/null
+++ b/src/site/components/uploader/Uploader.vue
@@ -0,0 +1,251 @@
+<template>
+ <div :class="{ hasFiles: files.length > 0 }"
+ class="uploader-wrapper">
+ <b-select v-if="loggedIn"
+ v-model="selectedAlbum"
+ placeholder="Upload to album"
+ size="is-medium"
+ expanded>
+ <option
+ v-for="album in albums"
+ :value="album.id"
+ :key="album.id">
+ {{ album.name }}
+ </option>
+ </b-select>
+ <dropzone v-if="showDropzone"
+ id="dropzone"
+ ref="el"
+ :options="dropzoneOptions"
+ :include-styling="false"
+ @vdropzone-success="dropzoneSuccess"
+ @vdropzone-error="dropzoneError"
+ @vdropzone-files-added="dropzoneFilesAdded" />
+
+ <div id="template"
+ ref="template">
+ <div class="dz-preview dz-file-preview">
+ <div class="dz-details">
+ <div class="dz-filename"><span data-dz-name/></div>
+ <div class="dz-size"><span data-dz-size/></div>
+ </div>
+ <div class="result">
+ <div class="copyLink">
+ <b-tooltip label="Copy link">
+ <i class="icon-web-code"/>
+ </b-tooltip>
+ </div>
+ <div class="openLink">
+ <b-tooltip label="Open file">
+ <a class="link"
+ target="_blank">
+ <i class="icon-web-url"/>
+ </a>
+ </b-tooltip>
+ </div>
+ </div>
+ <div class="error">
+ <div>
+ <span>
+ <span class="error-message"
+ data-dz-errormessage/>
+ <i class="icon-web-warning"/>
+ </span>
+ </div>
+ </div>
+ <div class="dz-progress">
+ <span class="dz-upload"
+ data-dz-uploadprogress/>
+ </div>
+ <!--
+ <div class="dz-error-message"><span data-dz-errormessage/></div>
+ <div class="dz-success-mark"><i class="fa fa-check"/></div>
+ <div class="dz-error-mark"><i class="fa fa-close"/></div>
+ -->
+ </div>
+ </div>
+ </div>
+</template>
+
+<script>
+import Dropzone from 'nuxt-dropzone';
+import '../../styles/dropzone.scss';
+
+export default {
+ components: { Dropzone },
+ data() {
+ return {
+ files: [],
+ dropzoneOptions: {},
+ showDropzone: false,
+ albums: [],
+ selectedAlbum: null
+ };
+ },
+ computed: {
+ config() {
+ return this.$store.state.config;
+ },
+ token() {
+ return this.$store.state.token;
+ },
+ loggedIn() {
+ return this.$store.state.loggedIn;
+ }
+ },
+ watch: {
+ loggedIn() {
+ this.getAlbums();
+ },
+ selectedAlbum() {
+ this.updateDropzoneConfig();
+ }
+ },
+ mounted() {
+ this.dropzoneOptions = {
+ url: `${this.$config.baseURL}/upload`,
+ autoProcessQueue: true,
+ addRemoveLinks: false,
+ parallelUploads: 5,
+ uploadMultiple: false,
+ maxFiles: 1000,
+ createImageThumbnails: false,
+ paramName: 'file',
+ chunking: true,
+ retryChunks: true,
+ retryChunksLimit: 3,
+ parallelChunkUploads: false,
+ chunkSize: this.config.chunkSize * 1000000,
+ chunksUploaded: this.dropzoneChunksUploaded,
+ maxFilesize: this.config.maxFileSize,
+ previewTemplate: this.$refs.template.innerHTML,
+ dictDefaultMessage: 'Drag & Drop your files or click to browse',
+ headers: { Accept: 'application/vnd.lolisafe.json' }
+ };
+ this.showDropzone = true;
+ if (this.loggedIn) this.getAlbums();
+ },
+ methods: {
+ /*
+ Get all available albums so the user can upload directly to one (or several soon™) of them.
+ */
+ async getAlbums() {
+ try {
+ const response = await this.axios.get(`${this.$config.baseURL}/albums/dropdown`);
+ this.albums = response.data.albums;
+ this.updateDropzoneConfig();
+ } catch (error) {
+ this.$onPromiseError(error);
+ }
+ },
+
+ /*
+ This method needs to be called after the token or selectedAlbum changes
+ since dropzone doesn't seem to update the config values unless you force it.
+ Tch.
+ */
+ updateDropzoneConfig() {
+ this.$refs.el.setOption('headers', {
+ Accept: 'application/vnd.lolisafe.json',
+ Authorization: this.token ? `Bearer ${this.token}` : '',
+ albumId: this.selectedAlbum ? this.selectedAlbum : null
+ });
+ },
+
+ /*
+ Dropzone stuff
+ */
+ dropzoneFilesAdded(files) {
+ // console.log(files);
+ },
+ dropzoneSuccess(file, response) {
+ this.processResult(file, response);
+ },
+ dropzoneError(file, message, xhr) {
+ this.$showToast('There was an error uploading this file. Check the console.', true, 5000);
+ console.error(file, message, xhr);
+ },
+ dropzoneChunksUploaded(file, done) {
+ const response = JSON.parse(file.xhr.response);
+ if (!response.url) {
+ console.error('There was a problem uploading the file?');
+ return done();
+ }
+
+ this.processResult(file, response);
+ this.$forceUpdate();
+ return done();
+ },
+
+ /*
+ If upload/s was/were successfull we modify the template so that the buttons for
+ copying the returned url or opening it in a new window appear.
+ */
+ processResult(file, response) {
+ if (!response.url) return;
+ file.previewTemplate.querySelector('.link').setAttribute('href', response.url);
+ file.previewTemplate.querySelector('.copyLink').addEventListener('click', () => {
+ this.$showToast('Link copied!', false, 1000);
+ this.$clipboard(response.url);
+ });
+ }
+ }
+};
+</script>
+<style lang="scss" scoped>
+ #template { display: none; }
+ .uploader-wrapper {
+ display: block;
+ width: 400px;
+ margin: 0 auto;
+ max-width: 100%;
+ transition-duration: 86ms;
+ transition-property: box-shadow,-webkit-transform;
+ transition-property: box-shadow,transform;
+ transition-property: box-shadow,transform,-webkit-transform;
+ will-change: box-shadow,transform;
+
+ &:hover, &.hasFiles {
+ box-shadow: 0 1rem 3rem 0rem rgba(10, 10, 10, 0.25);
+ -webkit-transform: translateY(-.5rem);
+ transform: translateY(-.5rem);
+ }
+ }
+</style>
+<style lang="scss">
+ @import '../../styles/colors.scss';
+ .filepond--panel-root {
+ background: transparent;
+ border: 2px solid #2c3340;
+ }
+ .filepond--drop-label {
+ color: #c7ccd8;
+ pointer-events: none;
+ }
+
+ .filepond--item-panel {
+ background-color: #767b8b;
+ }
+
+ .filepond--root .filepond--drip-blob {
+ background-color: #7f8a9a
+ }
+
+ .filepond--drip {
+ background: black;
+ }
+
+ div.uploader-wrapper {
+ div.control {
+ margin-bottom: 5px;
+ span.select {
+ select {
+ border: 2px solid #2c3340;
+ background: rgba(0, 0, 0, 0.15);
+ border-radius: .3em;
+ color: $uploaderDropdownColor;
+ }
+ }
+ }
+ }
+</style>