Switch to Nuxt.js
This commit is contained in:
parent
8e1711ed6c
commit
430af8306b
|
@ -2,6 +2,7 @@
|
|||
node_modules/
|
||||
_dist/
|
||||
.ream/
|
||||
.nuxt/
|
||||
|
||||
# Log files
|
||||
logs/
|
||||
|
@ -15,3 +16,4 @@ logs/
|
|||
config.js
|
||||
database.db
|
||||
uploads/
|
||||
src/oldsite
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
import autoprefixer from 'autoprefixer';
|
||||
import serveStatic from 'serve-static';
|
||||
import path from 'path';
|
||||
import config from './config';
|
||||
|
||||
export default {
|
||||
server: {
|
||||
port: config.server.ports.frontend
|
||||
},
|
||||
srcDir: 'src/site/',
|
||||
head: {
|
||||
meta: [
|
||||
{ charset: 'utf-8' },
|
||||
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }
|
||||
],
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700' }
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
'~/plugins/vue-axios',
|
||||
'~/plugins/buefy',
|
||||
'~/plugins/v-clipboard',
|
||||
'~/plugins/vue-analytics',
|
||||
'~/plugins/vue-isyourpasswordsafe',
|
||||
'~/plugins/vue-timeago'
|
||||
],
|
||||
serverMiddleware: [
|
||||
{ path: '/', handler: serveStatic(path.join(__dirname, 'uploads')) }
|
||||
],
|
||||
css: [],
|
||||
build: {
|
||||
extractCSS: true,
|
||||
postcss: [
|
||||
autoprefixer
|
||||
]
|
||||
}
|
||||
};
|
55
package.json
55
package.json
|
@ -12,8 +12,9 @@
|
|||
"scripts": {
|
||||
"api": "nodemon src/start api",
|
||||
"site": "node src/start site",
|
||||
"build": "ream build",
|
||||
"start": "cross-env NODE_ENV=production node src/start"
|
||||
"build": "nuxt build",
|
||||
"start": "cross-env NODE_ENV=production node src/start && nuxt start",
|
||||
"nuxt": "nuxt --port 5002"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -48,67 +49,43 @@
|
|||
"moment": "^2.22.1",
|
||||
"multer": "^1.3.0",
|
||||
"nuxt-dropzone": "^0.2.7",
|
||||
"nuxt-edge": "^2.0.0-25621471.65432e6",
|
||||
"one-liner": "^1.3.0",
|
||||
"path": "^0.12.7",
|
||||
"pg": "^7.4.3",
|
||||
"randomstring": "^1.1.5",
|
||||
"serve-static": "^1.13.2",
|
||||
"sharp": "^0.20.3",
|
||||
"v-clipboard": "^1.0.4",
|
||||
"vue-analytics": "^5.9.1",
|
||||
"vue-axios": "^2.0.2",
|
||||
"vue-isyourpasswordsafe": "^1.0.1",
|
||||
"vue-lazyload": "^1.2.2",
|
||||
"vue-plyr": "^2.1.1",
|
||||
"vue-timeago": "^3.4.4",
|
||||
"vuex": "^3.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-eslint": "^8.2.2",
|
||||
"autoprefixer": "^9.1.5",
|
||||
"babel-eslint": "^9.0.0",
|
||||
"cross-env": "^5.1.4",
|
||||
"eslint": "^4.19.1",
|
||||
"eslint-config-aqua": "^3.0.0",
|
||||
"eslint-plugin-vue": "^4.4.0",
|
||||
"node-sass": "^4.7.2",
|
||||
"eslint": "^5.6.0",
|
||||
"eslint-config-aqua": "^4.4.1",
|
||||
"eslint-plugin-vue": "^5.0.0-beta.3",
|
||||
"node-sass": "^4.9.3",
|
||||
"nodemon": "^1.17.5",
|
||||
"postcss-nested": "^3.0.0",
|
||||
"ream": "^3.2.7",
|
||||
"sass-loader": "^6.0.7",
|
||||
"sass-loader": "^7.1.0",
|
||||
"vue-eslint-parser": "^2.0.3"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "vue-eslint-parser",
|
||||
"parserOptions": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"extends": [
|
||||
"plugin:vue/recommended",
|
||||
"aqua"
|
||||
"aqua/vue",
|
||||
"aqua/node"
|
||||
],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"rules": {
|
||||
"func-names": 0,
|
||||
"capitalized-comments": 0,
|
||||
"max-len": 0,
|
||||
"id-length": 0,
|
||||
"no-warning-comments": 0,
|
||||
"vue/html-indent": [
|
||||
"error",
|
||||
"tab"
|
||||
],
|
||||
"vue/max-attributes-per-line": [
|
||||
2,
|
||||
{
|
||||
"singleline": 1,
|
||||
"multiline": {
|
||||
"max": 1,
|
||||
"allowFirstLine": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"vue/attribute-hyphenation": 0
|
||||
"vue/attribute-hyphenation": 0,
|
||||
"quote-props": 0
|
||||
}
|
||||
},
|
||||
"keywords": [
|
||||
|
|
|
@ -0,0 +1,380 @@
|
|||
const Route = require('../../structures/Route');
|
||||
const config = require('../../../../config');
|
||||
const path = require('path');
|
||||
const multer = require('multer');
|
||||
const Util = require('../../utils/Util');
|
||||
const db = require('knex')(config.server.database);
|
||||
const moment = require('moment');
|
||||
const log = require('../../utils/Log');
|
||||
const jetpack = require('fs-jetpack');
|
||||
const Busboy = require('busboy');
|
||||
const fs = require('fs');
|
||||
// WE SHOULD ALSO STRIP EXIF UNLESS THE USER SPECIFIED THEY WANT IT.
|
||||
// https://github.com/WeebDev/lolisafe/issues/110
|
||||
class uploadPOST extends Route {
|
||||
constructor() {
|
||||
super('/upload', 'post', { bypassAuth: true });
|
||||
}
|
||||
|
||||
async run(req, res) {
|
||||
const user = Util.isAuthorized(req);
|
||||
if (!user && !config.uploads.allowAnonymousUploads) return res.status(401).json({ message: 'Not authorized to use this resource' });
|
||||
|
||||
/*
|
||||
const albumId = req.body.albumId || req.headers.albumId;
|
||||
if (this.albumId && !this.user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
|
||||
if (this.albumId && this.user) {
|
||||
const album = await db.table('albums').where({ id: this.albumId, userId: this.user.id }).first();
|
||||
if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
|
||||
}
|
||||
*/
|
||||
return this.uploadFile(req, res, user);
|
||||
}
|
||||
|
||||
async processFile(req, res, user, file) {
|
||||
/*
|
||||
Check if the user is trying to upload to an album
|
||||
*/
|
||||
const albumId = req.body.albumId || req.headers.albumId;
|
||||
if (albumId && !user) return res.status(401).json({ message: 'Only registered users can upload files to an album' });
|
||||
if (albumId && user) {
|
||||
const album = await db.table('albums').where({ id: albumId, userId: user.id }).first();
|
||||
if (!album) return res.status(401).json({ message: 'Album doesn\'t exist or it doesn\'t belong to the user' });
|
||||
}
|
||||
|
||||
let upload = file.data;
|
||||
/*
|
||||
If it's a chunked upload but this is not the last part of the chunk, just green light.
|
||||
Otherwise, put the file together and process it
|
||||
*/
|
||||
if (file.body.uuid) {
|
||||
if (file.body.chunkindex < file.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if
|
||||
/*
|
||||
We got a chunk that is not the last part, send smoke signal that we received it.
|
||||
*/
|
||||
return res.json({ message: 'Successfully uploaded chunk' });
|
||||
} else {
|
||||
/*
|
||||
Seems we finally got the last part of a chunk upload
|
||||
*/
|
||||
const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder);
|
||||
const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', file.body.uuid);
|
||||
const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' });
|
||||
const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.'));
|
||||
|
||||
const tempFile = {
|
||||
filename: Util.getUniqueFilename(originalname),
|
||||
originalname,
|
||||
size: file.body.totalfilesize
|
||||
};
|
||||
|
||||
for (const chunkFile of chunkFiles) {
|
||||
try {
|
||||
const data = await jetpack.readAsync(chunkFile, 'buffer'); // eslint-disable-line no-await-in-loop
|
||||
await jetpack.appendAsync(path.join(uploadsDir, tempFile.filename), data); // eslint-disable-line no-await-in-loop
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
upload = tempFile;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(upload);
|
||||
const hash = await Util.getFileHash(upload.filename); // eslint-disable-line no-await-in-loop
|
||||
const exists = await db.table('files') // eslint-disable-line no-await-in-loop
|
||||
.where(function() {
|
||||
if (!user) this.whereNull('userId'); // eslint-disable-line no-invalid-this
|
||||
else this.where('userId', user.id); // eslint-disable-line no-invalid-this
|
||||
})
|
||||
.where({
|
||||
hash,
|
||||
size: upload.size
|
||||
})
|
||||
.first();
|
||||
|
||||
if (exists) {
|
||||
res.json({
|
||||
message: 'Successfully uploaded file',
|
||||
name: exists.name,
|
||||
size: exists.size,
|
||||
url: `${config.filesServeLocation}/${exists.name}`
|
||||
});
|
||||
|
||||
return Util.deleteFile(upload.filename);
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
try {
|
||||
await db.table('files').insert({
|
||||
userId: user ? user.id : null,
|
||||
name: upload.filename,
|
||||
original: upload.originalname,
|
||||
type: upload.mimetype || '',
|
||||
size: upload.size,
|
||||
hash,
|
||||
ip: req.ip,
|
||||
albumId: albumId ? albumId : null,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('There was an error saving the file to the database');
|
||||
console.log(error);
|
||||
return res.status(500).json({ message: 'There was an error uploading the file.' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: 'Successfully uploaded file',
|
||||
name: upload.filename,
|
||||
size: upload.size,
|
||||
url: `${config.filesServeLocation}/${upload.filename}`
|
||||
});
|
||||
|
||||
if (albumId) {
|
||||
try {
|
||||
db.table('albums').where('id', albumId).update('editedAt', now);
|
||||
} catch (error) {
|
||||
log.error('There was an error updating editedAt on an album');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// return Util.generateThumbnail(file.filename);
|
||||
}
|
||||
|
||||
uploadFile(req, res, user) {
|
||||
const busboy = new Busboy({
|
||||
headers: req.headers,
|
||||
limits: {
|
||||
fileSize: config.uploads.uploadMaxSize * (1000 * 1000),
|
||||
files: 1
|
||||
}
|
||||
});
|
||||
|
||||
const fileToUpload = {
|
||||
data: {},
|
||||
body: {}
|
||||
};
|
||||
|
||||
/*
|
||||
Note: For this to work on every case, whoever is uploading a chunk
|
||||
should really send the body first and the file last. Otherwise lolisafe
|
||||
may not catch the field on time and the chunk may end up being saved
|
||||
as a standalone file, completely broken.
|
||||
*/
|
||||
busboy.on('field', (fieldname, val) => {
|
||||
if (/^dz/.test(fieldname)) {
|
||||
fileToUpload.body[fieldname.substring(2)] = val;
|
||||
} else {
|
||||
fileToUpload.body[fieldname] = val;
|
||||
}
|
||||
});
|
||||
|
||||
/*
|
||||
Hey ther's a file! Let's upload it.
|
||||
*/
|
||||
busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
|
||||
let name, saveTo;
|
||||
|
||||
/*
|
||||
Let check whether the file is part of a chunk upload or if it's a standalone one.
|
||||
If the former, we should store them separately and join all the pieces after we
|
||||
receive the last one.
|
||||
*/
|
||||
if (!fileToUpload.body.uuid) {
|
||||
name = Util.getUniqueFilename(filename);
|
||||
if (!name) return res.status(500).json({ message: 'There was a problem allocating a filename for your upload' });
|
||||
saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, name);
|
||||
} else {
|
||||
name = `${filename}.${fileToUpload.body.chunkindex}`;
|
||||
const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid);
|
||||
jetpack.dir(chunkDir);
|
||||
saveTo = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', fileToUpload.body.uuid, name);
|
||||
}
|
||||
|
||||
/*
|
||||
Let's save some metadata for the db.
|
||||
*/
|
||||
fileToUpload.data = { filename: name, originalname: filename, encoding, mimetype };
|
||||
const stream = fs.createWriteStream(saveTo);
|
||||
|
||||
file.on('data', data => {
|
||||
fileToUpload.data.size = data.length;
|
||||
});
|
||||
|
||||
/*
|
||||
The file that is being uploaded is bigger than the limit specified on the config file
|
||||
and thus we should close the stream and delete the file.
|
||||
*/
|
||||
file.on('limit', () => {
|
||||
file.unpipe(stream);
|
||||
stream.end();
|
||||
jetpack.removeAsync(saveTo);
|
||||
res.status(400).json({ message: 'The file is too big.' });
|
||||
});
|
||||
|
||||
file.pipe(stream);
|
||||
});
|
||||
|
||||
busboy.on('error', err => {
|
||||
log.error('There was an error uploading a file');
|
||||
console.error(err);
|
||||
return res.status(500).json({ message: 'There was an error uploading the file.' });
|
||||
});
|
||||
|
||||
busboy.on('finish', () => this.processFile(req, res, user, fileToUpload));
|
||||
req.pipe(busboy);
|
||||
|
||||
// return req.pipe(busboy);
|
||||
|
||||
/*
|
||||
return upload(this.req, this.res, async err => {
|
||||
if (err) {
|
||||
log.error('There was an error uploading a file');
|
||||
console.error(err);
|
||||
return this.res.status(500).json({ message: 'There was an error uploading the file.' });
|
||||
}
|
||||
|
||||
log.info('---');
|
||||
console.log(this.req.file);
|
||||
log.info('---');
|
||||
|
||||
let file = this.req.file;
|
||||
if (this.req.body.uuid) {
|
||||
// If it's a chunked upload but this is not the last part of the chunk, just green light.
|
||||
// Otherwise, put the file together and process it
|
||||
if (this.req.body.chunkindex < this.req.body.totalchunkcount - 1) { // eslint-disable-line no-lonely-if
|
||||
log.info('Hey this is a chunk, sweet.');
|
||||
return this.res.json({ message: 'Successfully uploaded chunk' });
|
||||
} else {
|
||||
log.info('Hey this is the last part of a chunk, sweet.');
|
||||
|
||||
const uploadsDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder);
|
||||
const chunkedFileDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', this.req.body.uuid);
|
||||
const chunkFiles = await jetpack.findAsync(chunkedFileDir, { matching: '*' });
|
||||
const originalname = chunkFiles[0].substring(0, chunkFiles[0].lastIndexOf('.'));
|
||||
|
||||
const tempFile = {
|
||||
filename: Util.getUniqueFilename(originalname),
|
||||
originalname,
|
||||
size: this.req.body.totalfilesize
|
||||
};
|
||||
|
||||
for (const chunkFile of chunkFiles) {
|
||||
try {
|
||||
const data = await jetpack.readAsync(chunkFile, 'buffer'); // eslint-disable-line no-await-in-loop
|
||||
await jetpack.appendAsync(path.join(uploadsDir, tempFile.filename), data); // eslint-disable-line no-await-in-loop
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
file = tempFile;
|
||||
}
|
||||
}
|
||||
|
||||
const { user } = this;
|
||||
// console.log(file);
|
||||
if (!file.filename) return log.error('This file doesnt have a filename!');
|
||||
// console.log(file);
|
||||
const hash = await Util.getFileHash(file.filename); // eslint-disable-line no-await-in-loop
|
||||
const exists = await db.table('files') // eslint-disable-line no-await-in-loop
|
||||
.where(function() {
|
||||
if (!user) this.whereNull('userId'); // eslint-disable-line no-invalid-this
|
||||
else this.where('userId', user.id); // eslint-disable-line no-invalid-this
|
||||
})
|
||||
.where({
|
||||
hash,
|
||||
size: file.size
|
||||
})
|
||||
.first();
|
||||
|
||||
if (exists) {
|
||||
this.res.json({
|
||||
message: 'Successfully uploaded file',
|
||||
name: exists.name,
|
||||
size: exists.size,
|
||||
url: `${config.filesServeLocation}/${exists.name}`
|
||||
});
|
||||
|
||||
return Util.deleteFile(file.filename);
|
||||
}
|
||||
|
||||
const now = moment.utc().toDate();
|
||||
try {
|
||||
await db.table('files').insert({
|
||||
userId: this.user ? this.user.id : null,
|
||||
name: file.filename,
|
||||
original: file.originalname,
|
||||
type: file.mimetype || '',
|
||||
size: file.size,
|
||||
hash,
|
||||
ip: this.req.ip,
|
||||
albumId: this.albumId ? this.albumId : null,
|
||||
createdAt: now,
|
||||
editedAt: now
|
||||
});
|
||||
} catch (error) {
|
||||
log.error('There was an error saving the file to the database');
|
||||
console.log(error);
|
||||
return this.res.status(500).json({ message: 'There was an error uploading the file.' });
|
||||
}
|
||||
|
||||
this.res.json({
|
||||
message: 'Successfully uploaded file',
|
||||
name: file.filename,
|
||||
size: file.size,
|
||||
url: `${config.filesServeLocation}/${file.filename}`
|
||||
});
|
||||
|
||||
if (this.albumId) {
|
||||
try {
|
||||
db.table('albums').where('id', this.albumId).update('editedAt', now);
|
||||
} catch (error) {
|
||||
log.error('There was an error updating editedAt on an album');
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
// return Util.generateThumbnail(file.filename);
|
||||
});
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
const upload = multer({
|
||||
limits: config.uploads.uploadMaxSize,
|
||||
fileFilter(req, file, cb) {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (Util.isExtensionBlocked(ext)) return cb('This file extension is not allowed');
|
||||
|
||||
// Remove those pesky dz prefixes. Thanks to BobbyWibowo.
|
||||
for (const key in req.body) {
|
||||
if (!/^dz/.test(key)) continue;
|
||||
req.body[key.replace(/^dz/, '')] = req.body[key];
|
||||
delete req.body[key];
|
||||
}
|
||||
|
||||
return cb(null, true);
|
||||
},
|
||||
storage: multer.diskStorage({
|
||||
destination(req, file, cb) {
|
||||
if (!req.body.uuid) return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder));
|
||||
// Hey, we have chunks
|
||||
|
||||
const chunkDir = path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder, 'chunks', req.body.uuid);
|
||||
jetpack.dir(chunkDir);
|
||||
return cb(null, chunkDir);
|
||||
return cb(null, path.join(__dirname, '..', '..', '..', '..', config.uploads.uploadFolder));
|
||||
},
|
||||
filename(req, file, cb) {
|
||||
// if (req.body.uuid) return cb(null, `${file.originalname}.${req.body.chunkindex}`);
|
||||
const filename = Util.getUniqueFilename(file.originalname);
|
||||
// if (!filename) return cb('Could not allocate a unique file name');
|
||||
return cb(null, filename);
|
||||
}
|
||||
})
|
||||
}).single('file');
|
||||
*/
|
||||
module.exports = uploadPOST;
|
208
src/site/App.vue
208
src/site/App.vue
|
@ -1,208 +0,0 @@
|
|||
<template>
|
||||
<div id="app"
|
||||
@dragover="isDrag = true"
|
||||
@dragend="isDrag = false"
|
||||
@dragleave="isDrag = false"
|
||||
@drop="isDrag = false">
|
||||
<router-view :key="$route.fullPath"/>
|
||||
|
||||
<!--
|
||||
<div v-if="!ready"
|
||||
id="loading">
|
||||
<div class="background"/>
|
||||
<Loading class="square"/>
|
||||
</div>
|
||||
-->
|
||||
<div v-if="false"
|
||||
id="drag-overlay">
|
||||
<div class="background"/>
|
||||
<div class="drop">
|
||||
Drop your files here
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import Fuse from 'fuse.js';
|
||||
import Logo from './components/logo/Logo.vue';
|
||||
import Loading from './components/loading/CubeShadow.vue';
|
||||
|
||||
const protectedRoutes = [
|
||||
'/dashboard',
|
||||
'/dashboard/albums',
|
||||
'/dashboard/settings'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Loading,
|
||||
Logo
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
pageTitle: '',
|
||||
ready: false,
|
||||
isDrag: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
user() {
|
||||
return this.$store.state.user;
|
||||
},
|
||||
loggedIn() {
|
||||
return this.$store.state.loggedIn;
|
||||
},
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(`%c Running lolisafe %c v${this.config.version} %c`, 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 'background:#ff015b; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 'background:transparent');
|
||||
this.$store.commit('config', Vue.prototype.$config);
|
||||
this.ready = true;
|
||||
},
|
||||
metaInfo() { // eslint-disable-line complexity
|
||||
return {
|
||||
title: this.pageTitle || 'A small safe worth protecting.',
|
||||
titleTemplate: '%s | lolisafe',
|
||||
link: [
|
||||
{ rel: 'stylesheet', href: 'https://fonts.googleapis.com/css?family=Nunito:300,400,600,700', body: true },
|
||||
// { rel: 'stylesheet', href: 'https://cdn.materialdesignicons.com/2.1.99/css/materialdesignicons.min.css', body: true },
|
||||
|
||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: '/public/images/icons/apple-touch-icon.png' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '32x32', href: '/public/images/icons/favicon-32x32.png' },
|
||||
{ rel: 'icon', type: 'image/png', sizes: '16x16', href: '/public/images/icons/favicon-16x16.png' },
|
||||
{ rel: 'manifest', href: '/public/images/icons/manifest.json' },
|
||||
{ rel: 'mask-icon', color: '#FF015B', href: '/public/images/icons/safari-pinned-tab.svg' },
|
||||
{ rel: 'shortcut icon', href: '/public/images/icons/favicon.ico' },
|
||||
{ rel: 'chrome-webstore-item', href: 'https://chrome.google.com/webstore/detail/lolisafe-uploader/enkkmplljfjppcdaancckgilmgoiofnj' },
|
||||
{ type: 'application/json+oembed', href: '/public/oembed.json' }
|
||||
],
|
||||
meta: [
|
||||
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
|
||||
|
||||
{ vmid: 'description', name: 'description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
|
||||
{ vmid: 'keywords', name: 'keywords', content: 'lolisafe, file, upload, uploader, vue, node, open source, free' },
|
||||
|
||||
{ vmid: 'apple-mobile-web-app-title', name: 'apple-mobile-web-app-title', content: 'lolisafe' },
|
||||
{ vmid: 'application-name', name: 'application-name', content: 'lolisafe' },
|
||||
{ vmid: 'msapplication-config', name: 'msapplication-config', content: '/public/images/icons/browserconfig.xml' },
|
||||
|
||||
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary_large_image' },
|
||||
{ vmid: 'twitter:site', name: 'twitter:site', content: '@its_pitu' },
|
||||
{ vmid: 'twitter:creator', name: 'twitter:creator', content: '@its_pitu' },
|
||||
{ vmid: 'twitter:title', name: 'twitter:title', content: `lolisafe` },
|
||||
{ vmid: 'twitter:description', name: 'twitter:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
|
||||
{ vmid: 'twitter:image', name: 'twitter:image', content: '/public/images/share.jpg' },
|
||||
|
||||
{ vmid: 'og:url', property: 'og:url', content: 'https://lolisafe.moe' },
|
||||
{ vmid: 'og:type', property: 'og:type', content: 'website' },
|
||||
{ vmid: 'og:title', property: 'og:title', content: `lolisafe` },
|
||||
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
|
||||
{ vmid: 'og:image', property: 'og:image', content: '/public/images/share.jpg' },
|
||||
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: '/public/images/share.jpg' },
|
||||
{ vmid: 'og:site_name', property: 'og:site_name', content: 'lolisafe' }
|
||||
]
|
||||
};
|
||||
},
|
||||
created() {
|
||||
/*
|
||||
Register our global handles
|
||||
*/
|
||||
const App = this; // eslint-disable-line consistent-this
|
||||
this.$store.commit('config', Vue.prototype.$config);
|
||||
Vue.prototype.$search = function(term, list, options) {
|
||||
return new Promise(resolve => {
|
||||
const run = new Fuse(list, options);
|
||||
const results = run.search(term);
|
||||
return resolve(results);
|
||||
});
|
||||
};
|
||||
|
||||
Vue.prototype.$onPromiseError = function(error, logout = false) {
|
||||
App.processCatch(error, logout);
|
||||
};
|
||||
|
||||
Vue.prototype.$showToast = function(text, error, duration) {
|
||||
App.showToast(text, error, duration);
|
||||
};
|
||||
|
||||
Vue.prototype.$logOut = function() {
|
||||
App.$store.commit('user', null);
|
||||
App.$store.commit('loggedIn', false);
|
||||
App.$store.commit('token', null);
|
||||
};
|
||||
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
if (this.$store.state.loggedIn) return next();
|
||||
if (process.browser) {
|
||||
if (localStorage && localStorage.getItem('ls-token')) return this.tryToLogin(next, `/login?redirect=${to.path}`);
|
||||
}
|
||||
|
||||
for (const match of to.matched) {
|
||||
if (protectedRoutes.includes(match.path)) {
|
||||
if (this.$store.state.loggedIn === false) return next(`/login?redirect=${to.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
if (process.browser) this.tryToLogin();
|
||||
},
|
||||
methods: {
|
||||
showToast(text, error, duration) {
|
||||
this.$toast.open({
|
||||
duration: duration || 2500,
|
||||
message: text,
|
||||
position: 'is-bottom',
|
||||
type: error ? 'is-danger' : 'is-success'
|
||||
});
|
||||
},
|
||||
processCatch(error, logout) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
this.showToast(error.response.data.message, true, 5000);
|
||||
if (error.response.status === 429) return;
|
||||
if (error.response.status === 502) return;
|
||||
if (logout) {
|
||||
this.$logOut();
|
||||
setTimeout(() => this.$router.push('/'), 3000);
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
this.showToast('Something went wrong, please check the console :(', true, 5000);
|
||||
}
|
||||
},
|
||||
tryToLogin(next, destination) {
|
||||
if (process.browser) this.$store.commit('token', localStorage.getItem('ls-token'));
|
||||
this.axios.get(`${this.$config.baseURL}/verify`).then(res => {
|
||||
this.$store.commit('user', res.data.user);
|
||||
this.$store.commit('loggedIn', true);
|
||||
if (next) return next();
|
||||
return null;
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.status === 520) return;
|
||||
if (error.response && error.response.status === 429) {
|
||||
setTimeout(() => {
|
||||
this.tryToLogin(next, destination);
|
||||
}, 1000);
|
||||
return next(false);
|
||||
} else {
|
||||
this.$store.commit('user', null);
|
||||
this.$store.commit('loggedIn', false);
|
||||
this.$store.commit('token', null);
|
||||
if (next && destination) return next(destination);
|
||||
if (next) return next('/');
|
||||
return null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "./styles/style.scss";
|
||||
@import "./styles/icons.min.css";
|
||||
</style>
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/_colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
.item-move {
|
||||
transition: all .25s cubic-bezier(.55,0,.1,1);
|
||||
-webkit-transition: all .25s cubic-bezier(.55,0,.1,1);
|
||||
|
@ -99,25 +99,25 @@
|
|||
position="is-top">
|
||||
<a :href="`${item.url}`"
|
||||
target="_blank">
|
||||
<i class="icon-web-code"/>
|
||||
<i class="icon-web-code" />
|
||||
</a>
|
||||
</b-tooltip>
|
||||
<b-tooltip label="Albums"
|
||||
position="is-top">
|
||||
<a @click="manageAlbums(item)">
|
||||
<i class="icon-interface-window"/>
|
||||
<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"/>
|
||||
<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"/>
|
||||
<i class="icon-editorial-trash-a-l" />
|
||||
</a>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
@ -155,6 +155,11 @@ export default {
|
|||
data() {
|
||||
return { showWaterfall: true };
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
deleteFile(file, index) {
|
||||
this.$dialog.confirm({
|
||||
|
@ -165,7 +170,7 @@ export default {
|
|||
hasIcon: true,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const response = await this.axios.delete(`${this.$config.baseURL}/file/${file.id}`);
|
||||
const response = await this.axios.delete(`${this.config.baseURL}/file/${file.id}`);
|
||||
this.showWaterfall = false;
|
||||
this.files.splice(index, 1);
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -5,20 +5,19 @@
|
|||
</style>
|
||||
<template>
|
||||
<div class="waterfall">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
// import {quickSort, getMinIndex, _, sum} from './util'
|
||||
|
||||
const quickSort = (arr, type) => {
|
||||
let left = [];
|
||||
let right = [];
|
||||
let povis;
|
||||
const left = [];
|
||||
const right = [];
|
||||
if (arr.length <= 1) {
|
||||
return arr;
|
||||
}
|
||||
povis = arr[0];
|
||||
const povis = arr[0];
|
||||
for (let i = 1; i < arr.length; i++) {
|
||||
if (arr[i][type] < povis[type]) {
|
||||
left.push(arr[i]);
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</style>
|
||||
<template>
|
||||
<div class="waterfall-item">
|
||||
<slot/>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
<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>
|
|
@ -1,442 +0,0 @@
|
|||
<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>
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../../styles/_colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
.links {
|
||||
margin-bottom: 3em;
|
||||
align-items: stretch;
|
||||
|
@ -96,5 +96,5 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {}
|
||||
export default {};
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/_colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
#logo {
|
||||
-webkit-animation-delay: 0.5s;
|
||||
animation-delay: 0.5s;
|
||||
|
@ -50,7 +50,7 @@
|
|||
|
||||
<template>
|
||||
<p id="logo">
|
||||
<img src="../../public/images/logo.png">
|
||||
<img src="~/assets/images/logo.png">
|
||||
</p>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
nav.navbar {
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
|
@ -47,7 +47,7 @@
|
|||
<div class="navbar-brand">
|
||||
<router-link to="/"
|
||||
class="navbar-item no-active">
|
||||
<i class="icon-ecommerce-safebox"/> {{ config.serviceName }}
|
||||
<i class="icon-ecommerce-safebox" /> {{ config.serviceName }}
|
||||
</router-link>
|
||||
|
||||
<!--
|
||||
|
@ -78,12 +78,12 @@
|
|||
|
||||
<router-link v-if="!loggedIn"
|
||||
class="navbar-item"
|
||||
to="/login"><i class="hidden"/>Login</router-link>
|
||||
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>
|
||||
exact><i class="hidden" />Dashboard</router-link>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
.dashboard-menu {
|
||||
a {
|
||||
display: block;
|
||||
|
@ -25,19 +25,17 @@
|
|||
</style>
|
||||
<template>
|
||||
<div class="dashboard-menu">
|
||||
<router-link to="/"><i class="icon-ecommerce-safebox"/>lolisafe</router-link>
|
||||
<router-link to="/"><i class="icon-ecommerce-safebox" />lolisafe</router-link>
|
||||
<hr>
|
||||
<a><i class="icon-interface-cloud-upload"/>Upload files</a>
|
||||
<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>
|
||||
<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>
|
||||
<router-link to="/dashboard/settings"><i class="icon-setting-gear-a" />Settings</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
|
||||
}
|
||||
export default {};
|
||||
</script>
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
expanded>
|
||||
<option
|
||||
v-for="album in albums"
|
||||
:value="album.id"
|
||||
:key="album.id">
|
||||
:key="album.id"
|
||||
:value="album.id">
|
||||
{{ album.name }}
|
||||
</option>
|
||||
</b-select>
|
||||
|
@ -29,20 +29,20 @@
|
|||
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 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"/>
|
||||
<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"/>
|
||||
<i class="icon-web-url" />
|
||||
</a>
|
||||
</b-tooltip>
|
||||
</div>
|
||||
|
@ -51,14 +51,14 @@
|
|||
<div>
|
||||
<span>
|
||||
<span class="error-message"
|
||||
data-dz-errormessage/>
|
||||
<i class="icon-web-warning"/>
|
||||
data-dz-errormessage />
|
||||
<i class="icon-web-warning" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dz-progress">
|
||||
<span class="dz-upload"
|
||||
data-dz-uploadprogress/>
|
||||
data-dz-uploadprogress />
|
||||
</div>
|
||||
<!--
|
||||
<div class="dz-error-message"><span data-dz-errormessage/></div>
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
<script>
|
||||
import Dropzone from 'nuxt-dropzone';
|
||||
import '../../styles/dropzone.scss';
|
||||
import '~/assets/styles/dropzone.scss';
|
||||
|
||||
export default {
|
||||
components: { Dropzone },
|
||||
|
@ -107,7 +107,7 @@ export default {
|
|||
},
|
||||
mounted() {
|
||||
this.dropzoneOptions = {
|
||||
url: `${this.$config.baseURL}/upload`,
|
||||
url: `${this.config.baseURL}/upload`,
|
||||
autoProcessQueue: true,
|
||||
addRemoveLinks: false,
|
||||
parallelUploads: 5,
|
||||
|
@ -135,7 +135,7 @@ export default {
|
|||
*/
|
||||
async getAlbums() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.$config.baseURL}/albums/dropdown`);
|
||||
const response = await this.axios.get(`${this.config.baseURL}/albums/dropdown`);
|
||||
this.albums = response.data.albums;
|
||||
this.updateDropzoneConfig();
|
||||
} catch (error) {
|
||||
|
@ -218,7 +218,7 @@ export default {
|
|||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
.filepond--panel-root {
|
||||
background: transparent;
|
||||
border: 2px solid #2c3340;
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui" />
|
||||
<!--ream-head-placeholder-->
|
||||
<!--ream-styles-placeholder-->
|
||||
</head>
|
||||
<body>
|
||||
<!--ream-app-placeholder-->
|
||||
<!--ream-scripts-placeholder-->
|
||||
</body>
|
||||
</html>
|
|
@ -1,49 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import VueMeta from 'vue-meta';
|
||||
import axios from 'axios';
|
||||
import VueAxios from 'vue-axios';
|
||||
import Buefy from 'buefy';
|
||||
import VueTimeago from 'vue-timeago';
|
||||
import VueLazyload from 'vue-lazyload';
|
||||
import VueAnalytics from 'vue-analytics';
|
||||
import Clipboard from 'v-clipboard';
|
||||
import VueIsYourPasswordSafe from 'vue-isyourpasswordsafe';
|
||||
|
||||
import router from './router';
|
||||
import store from './store';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
Vue.use(VueMeta);
|
||||
Vue.use(VueLazyload);
|
||||
Vue.use(VueAnalytics, {
|
||||
id: 'UA-000000000-0',
|
||||
debug: {
|
||||
enabled: !isProduction,
|
||||
sendHitTask: isProduction
|
||||
}
|
||||
});
|
||||
Vue.use(VueIsYourPasswordSafe, {
|
||||
minLength: 6,
|
||||
maxLength: 64
|
||||
});
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.use(Buefy);
|
||||
Vue.use(VueTimeago, {
|
||||
name: 'timeago',
|
||||
locale: 'en-US',
|
||||
locales: { 'en-US': require('vue-timeago/locales/en-US.json') }
|
||||
});
|
||||
Vue.use(Clipboard);
|
||||
|
||||
Vue.axios.defaults.headers.common.Accept = 'application/vnd.lolisafe.json';
|
||||
Vue.prototype.$config = require('./config');
|
||||
|
||||
export default () => {
|
||||
return {
|
||||
root: () => import('./App.vue'),
|
||||
router,
|
||||
store
|
||||
};
|
||||
};
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<nuxt />
|
||||
</template>
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import Fuse from 'fuse.js';
|
||||
|
||||
const protectedRoutes = [
|
||||
'/dashboard',
|
||||
'/dashboard/albums',
|
||||
'/dashboard/settings'
|
||||
];
|
||||
|
||||
export default {
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
console.log(`%c lolisafe %c v${this.config.version} %c`, 'background:#35495e ; padding: 1px; border-radius: 3px 0 0 3px; color: #fff', 'background:#ff015b; padding: 1px; border-radius: 0 3px 3px 0; color: #fff', 'background:transparent');
|
||||
},
|
||||
created() {
|
||||
Vue.prototype.$search = (term, list, options) => {
|
||||
return new Promise(resolve => {
|
||||
const run = new Fuse(list, options);
|
||||
const results = run.search(term);
|
||||
return resolve(results);
|
||||
});
|
||||
};
|
||||
|
||||
Vue.prototype.$onPromiseError = (error, logout = false) => {
|
||||
this.processCatch(error, logout);
|
||||
};
|
||||
|
||||
Vue.prototype.$showToast = (text, error, duration) => {
|
||||
this.showToast(text, error, duration);
|
||||
};
|
||||
|
||||
Vue.prototype.$logOut = () => {
|
||||
this.$store.commit('user', null);
|
||||
this.$store.commit('loggedIn', false);
|
||||
this.$store.commit('token', null);
|
||||
};
|
||||
|
||||
this.$router.beforeEach((to, from, next) => {
|
||||
if (this.$store.state.loggedIn) return next();
|
||||
if (process.browser) {
|
||||
if (localStorage && localStorage.getItem('lolisafe-token')) return this.tryToLogin(next, `/login?redirect=${to.path}`);
|
||||
}
|
||||
|
||||
for (const match of to.matched) {
|
||||
if (protectedRoutes.includes(match.path)) {
|
||||
if (this.$store.state.loggedIn === false) return next(`/login?redirect=${to.path}`);
|
||||
}
|
||||
}
|
||||
|
||||
return next();
|
||||
});
|
||||
if (process.browser) this.tryToLogin();
|
||||
},
|
||||
methods: {
|
||||
showToast(text, error, duration) {
|
||||
this.$toast.open({
|
||||
duration: duration || 2500,
|
||||
message: text,
|
||||
position: 'is-bottom',
|
||||
type: error ? 'is-danger' : 'is-success'
|
||||
});
|
||||
},
|
||||
processCatch(error, logout) {
|
||||
if (error.response && error.response.data && error.response.data.message) {
|
||||
this.showToast(error.response.data.message, true, 5000);
|
||||
if (error.response.status === 429) return;
|
||||
if (error.response.status === 502) return;
|
||||
if (logout) {
|
||||
this.$logOut();
|
||||
setTimeout(() => this.$router.push('/'), 3000);
|
||||
}
|
||||
} else {
|
||||
console.error(error);
|
||||
this.showToast('Something went wrong, please check the console :(', true, 5000);
|
||||
}
|
||||
},
|
||||
tryToLogin(next, destination) {
|
||||
if (process.browser) this.$store.commit('token', localStorage.getItem('lolisafe-token'));
|
||||
this.axios.get(`${this.config.baseURL}/verify`).then(res => {
|
||||
this.$store.commit('user', res.data.user);
|
||||
this.$store.commit('loggedIn', true);
|
||||
if (next) return next();
|
||||
return null;
|
||||
}).catch(error => {
|
||||
if (error.response && error.response.status === 520) return;
|
||||
if (error.response && error.response.status === 429) {
|
||||
setTimeout(() => {
|
||||
this.tryToLogin(next, destination);
|
||||
}, 1000);
|
||||
return next(false);
|
||||
}
|
||||
this.$store.commit('user', null);
|
||||
this.$store.commit('loggedIn', false);
|
||||
this.$store.commit('token', null);
|
||||
if (next && destination) return next(destination);
|
||||
if (next) return next('/');
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "~/assets/styles/style.scss";
|
||||
@import "~assets/styles/icons.min.css";
|
||||
</style>
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import "../styles/_colors.scss";
|
||||
@import "~/assets/styles/_colors.scss";
|
||||
h2 {
|
||||
font-weight: 100;
|
||||
color: $textColor;
|
||||
|
@ -10,7 +10,7 @@
|
|||
|
||||
<template>
|
||||
<section class="hero is-fullheight">
|
||||
<Navbar :isWhite="true"/>
|
||||
<Navbar :isWhite="true" />
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<h2>404エラ</h2>
|
||||
|
@ -20,7 +20,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from '../components/navbar/Navbar.vue';
|
||||
import Navbar from '~/components/navbar/Navbar.vue';
|
||||
|
||||
export default {
|
||||
components: { Navbar },
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
section { background-color: $backgroundLight1 !important; }
|
||||
|
||||
section.hero div.hero-body.align-top {
|
||||
|
@ -14,7 +14,7 @@
|
|||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
@ -49,24 +49,25 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Grid from '../components/grid/Grid.vue';
|
||||
import Loading from '../components/loading/CubeShadow.vue';
|
||||
import Grid from '~/components/grid/Grid.vue';
|
||||
import Loading from '~/components/loading/CubeShadow.vue';
|
||||
import axios from 'axios';
|
||||
import config from '../config.js';
|
||||
import config from '~/config.js';
|
||||
|
||||
export default {
|
||||
components: { Grid, Loading },
|
||||
async getInitialData({ route, store }) {
|
||||
async asyncData({ params, error }) {
|
||||
try {
|
||||
const res = await axios.get(`${config.baseURL}/album/${route.params.identifier}`);
|
||||
const downloadLink = res.data.downloadEnabled ? `${config.baseURL}/album/${route.params.identifier}/zip` : null;
|
||||
const res = await axios.get(`${config.baseURL}/album/${params.identifier}`);
|
||||
const downloadLink = res.data.downloadEnabled ? `${config.baseURL}/album/${params.identifier}/zip` : null;
|
||||
return {
|
||||
name: res.data.name,
|
||||
downloadEnabled: res.data.downloadEnabled,
|
||||
files: res.data.files,
|
||||
downloadLink
|
||||
};
|
||||
} catch (error) {
|
||||
} catch (err) {
|
||||
/*
|
||||
return {
|
||||
name: null,
|
||||
downloadEnabled: false,
|
||||
|
@ -74,13 +75,20 @@ export default {
|
|||
downloadLink: null,
|
||||
error: error.response.status
|
||||
};
|
||||
*/
|
||||
error({ statusCode: 404, message: 'Post not found' });
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
metaInfo() {
|
||||
if (!this.files) {
|
||||
if (this.files) {
|
||||
return {
|
||||
title: `${this.name ? this.name : ''}`,
|
||||
meta: [
|
||||
|
@ -98,31 +106,31 @@ export default {
|
|||
{ vmid: 'og:image:secure_url', property: 'og:image:secure_url', content: `${this.files.length > 0 ? this.files[0].thumbSquare : '/public/images/share.jpg'}` }
|
||||
]
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
title: `${this.name ? this.name : ''}`,
|
||||
meta: [
|
||||
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
|
||||
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
||||
{ vmid: 'twitter:title', name: 'twitter:title', content: 'lolisafe' },
|
||||
{ vmid: 'twitter:description', name: 'twitter:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
|
||||
{ vmid: 'og:url', property: 'og:url', content: `${config.URL}/a/${this.$route.params.identifier}` },
|
||||
{ vmid: 'og:title', property: 'og:title', content: 'lolisafe' },
|
||||
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' }
|
||||
]
|
||||
};
|
||||
}
|
||||
return {
|
||||
title: `${this.name ? this.name : ''}`,
|
||||
meta: [
|
||||
{ vmid: 'theme-color', name: 'theme-color', content: '#30a9ed' },
|
||||
{ vmid: 'twitter:card', name: 'twitter:card', content: 'summary' },
|
||||
{ vmid: 'twitter:title', name: 'twitter:title', content: 'lolisafe' },
|
||||
{ vmid: 'twitter:description', name: 'twitter:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' },
|
||||
{ vmid: 'og:url', property: 'og:url', content: `${config.URL}/a/${this.$route.params.identifier}` },
|
||||
{ vmid: 'og:title', property: 'og:title', content: 'lolisafe' },
|
||||
{ vmid: 'og:description', property: 'og:description', content: 'A modern and self-hosted file upload service that can handle anything you throw at it. Fast uploads, file manager and sharing capabilities all crafted with a beautiful user experience in mind.' }
|
||||
]
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
/*
|
||||
if (this.error) {
|
||||
if (this.error === 404) {
|
||||
this.$toast.open('Album not found', true, 3000);
|
||||
setTimeout(() => this.$router.push('/404'), 3000);
|
||||
return;
|
||||
} else {
|
||||
this.$toast.open(`Error code ${this.error}`, true, 3000);
|
||||
}
|
||||
this.$toast.open(`Error code ${this.error}`, true, 3000);
|
||||
}
|
||||
*/
|
||||
this.$ga.page({
|
||||
page: `/a/${this.$route.params.identifier}`,
|
||||
title: `Album | ${this.name}`,
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
section { background-color: $backgroundLight1 !important; }
|
||||
section.hero div.hero-body {
|
||||
align-items: baseline;
|
||||
|
@ -118,7 +118,7 @@
|
|||
div.column > h2.subtitle { padding-top: 1px; }
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
|
||||
.b-table {
|
||||
.table-wrapper {
|
||||
|
@ -147,7 +147,7 @@
|
|||
<b-input v-model="newAlbumName"
|
||||
placeholder="Album name..."
|
||||
type="text"
|
||||
@keyup.enter.native="createAlbum"/>
|
||||
@keyup.enter.native="createAlbum" />
|
||||
<p class="control">
|
||||
<button class="button is-primary"
|
||||
@click="createAlbum">Create album</button>
|
||||
|
@ -227,14 +227,14 @@
|
|||
label="Allow download"
|
||||
centered>
|
||||
<b-switch v-model="props.row.enableDownload"
|
||||
@input="linkOptionsChanged(props.row)"/>
|
||||
@input="linkOptionsChanged(props.row)" />
|
||||
</b-table-column>
|
||||
|
||||
<b-table-column field="enabled"
|
||||
label="Enabled"
|
||||
centered>
|
||||
<b-switch v-model="props.row.enabled"
|
||||
@input="linkOptionsChanged(props.row)"/>
|
||||
@input="linkOptionsChanged(props.row)" />
|
||||
</b-table-column>
|
||||
|
||||
<!--
|
||||
|
@ -252,7 +252,7 @@
|
|||
</template>
|
||||
<template slot="empty">
|
||||
<div class="has-text-centered">
|
||||
<i class="icon-misc-mood-sad"/>
|
||||
<i class="icon-misc-mood-sad" />
|
||||
</div>
|
||||
<div class="has-text-centered">
|
||||
Nothing here
|
||||
|
@ -281,12 +281,10 @@
|
|||
|
||||
<script>
|
||||
import Sidebar from '../../components/sidebar/Sidebar.vue';
|
||||
import Grid from '../../components/grid/Grid.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sidebar,
|
||||
Grid
|
||||
Sidebar
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -313,7 +311,7 @@ export default {
|
|||
methods: {
|
||||
async linkOptionsChanged(link) {
|
||||
try {
|
||||
const response = await this.axios.post(`${this.$config.baseURL}/album/link/edit`,
|
||||
const response = await this.axios.post(`${this.config.baseURL}/album/link/edit`,
|
||||
{
|
||||
identifier: link.identifier,
|
||||
enableDownload: link.enableDownload,
|
||||
|
@ -327,7 +325,7 @@ export default {
|
|||
async createLink(album) {
|
||||
album.isCreatingLink = true;
|
||||
try {
|
||||
const response = await this.axios.post(`${this.$config.baseURL}/album/link/new`,
|
||||
const response = await this.axios.post(`${this.config.baseURL}/album/link/new`,
|
||||
{ albumId: album.id });
|
||||
this.$toast.open(response.data.message);
|
||||
album.links.push({
|
||||
|
@ -346,7 +344,7 @@ export default {
|
|||
async createAlbum() {
|
||||
if (!this.newAlbumName || this.newAlbumName === '') return;
|
||||
try {
|
||||
const response = await this.axios.post(`${this.$config.baseURL}/album/new`,
|
||||
const response = await this.axios.post(`${this.config.baseURL}/album/new`,
|
||||
{ name: this.newAlbumName });
|
||||
this.newAlbumName = null;
|
||||
this.$toast.open(response.data.message);
|
||||
|
@ -358,7 +356,7 @@ export default {
|
|||
},
|
||||
async getAlbums() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.$config.baseURL}/albums/mini`);
|
||||
const response = await this.axios.get(`${this.config.baseURL}/albums/mini`);
|
||||
for (const album of response.data.albums) {
|
||||
album.isDetailsOpen = false;
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
section { background-color: $backgroundLight1 !important; }
|
||||
section.hero div.hero-body {
|
||||
align-items: baseline;
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
</style>
|
||||
|
||||
|
||||
|
@ -25,7 +25,7 @@
|
|||
<hr>
|
||||
-->
|
||||
<Grid v-if="files.length"
|
||||
:files="files"/>
|
||||
:files="files" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -34,8 +34,8 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from '../../components/sidebar/Sidebar.vue';
|
||||
import Grid from '../../components/grid/Grid.vue';
|
||||
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
||||
import Grid from '~/components/grid/Grid.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -45,6 +45,11 @@ export default {
|
|||
data() {
|
||||
return { files: [] };
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
metaInfo() {
|
||||
return { title: 'Uploads' };
|
||||
},
|
||||
|
@ -59,7 +64,7 @@ export default {
|
|||
methods: {
|
||||
async getFiles() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.$config.baseURL}/files`);
|
||||
const response = await this.axios.get(`${this.config.baseURL}/files`);
|
||||
this.files = response.data.files;
|
||||
console.log(this.files);
|
||||
} catch (error) {
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
section { background-color: $backgroundLight1 !important; }
|
||||
section.hero div.hero-body {
|
||||
align-items: baseline;
|
||||
|
@ -10,7 +10,7 @@
|
|||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
</style>
|
||||
|
||||
|
||||
|
@ -20,7 +20,7 @@
|
|||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<Sidebar/>
|
||||
<Sidebar />
|
||||
</div>
|
||||
<div class="column">
|
||||
<!--
|
||||
|
@ -45,18 +45,11 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from '../../components/sidebar/Sidebar.vue';
|
||||
import Grid from '../../components/grid/Grid.vue';
|
||||
// import Waterfall from '../../components/waterfall/Waterfall.vue';
|
||||
// import WaterfallItem from '../../components/waterfall/WaterfallItem.vue';
|
||||
import Sidebar from '~/components/sidebar/Sidebar.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sidebar,
|
||||
Grid
|
||||
// Waterfall,
|
||||
// WaterfallSlot
|
||||
// WaterfallItem
|
||||
Sidebar
|
||||
},
|
||||
data() {
|
||||
return {
|
|
@ -1,30 +1,29 @@
|
|||
<style lang="scss" scoped>
|
||||
@import "../styles/_colors.scss";
|
||||
@import "~/assets/styles/_colors.scss";
|
||||
div.home {
|
||||
color: $textColor;
|
||||
// background-color: #1e2430;
|
||||
}
|
||||
.columns {
|
||||
.column {
|
||||
&.centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.columns {
|
||||
.column {
|
||||
&.centered {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
color: $textColorHighlight;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
strong {
|
||||
h4 {
|
||||
color: $textColorHighlight;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.25em;
|
||||
font-weight: 600;
|
||||
line-height: 1.5;
|
||||
|
||||
strong {
|
||||
color: $textColorHighlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -32,7 +31,7 @@
|
|||
<template>
|
||||
<div class="home">
|
||||
<section class="hero is-fullheight has-text-centered">
|
||||
<Navbar :isWhite="true"/>
|
||||
<Navbar :isWhite="true" />
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
|
@ -64,10 +63,10 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from '../components/navbar/Navbar.vue';
|
||||
import Logo from '../components/logo/Logo.vue';
|
||||
import Uploader from '../components/uploader/Uploader.vue';
|
||||
import Links from '../components/home/links/Links.vue';
|
||||
import Navbar from '~/components/navbar/Navbar.vue';
|
||||
import Logo from '~/components/logo/Logo.vue';
|
||||
import Uploader from '~/components/uploader/Uploader.vue';
|
||||
import Links from '~/components/home/links/Links.vue';
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
@ -20,14 +20,14 @@
|
|||
<b-input v-model="username"
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
@keyup.enter.native="login"/>
|
||||
@keyup.enter.native="login" />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-input v-model="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
password-reveal
|
||||
@keyup.enter.native="login"/>
|
||||
@keyup.enter.native="login" />
|
||||
</b-field>
|
||||
|
||||
<p class="control has-addons is-pulled-right">
|
||||
|
@ -70,7 +70,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from '../../components/navbar/Navbar.vue';
|
||||
import Navbar from '~/components/navbar/Navbar.vue';
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
|
@ -107,7 +107,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.axios.post(`${this.$config.baseURL}/auth/login`, {
|
||||
this.axios.post(`${this.config.baseURL}/auth/login`, {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(res => {
|
|
@ -1,5 +1,5 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
@import '~/assets/styles/_colors.scss';
|
||||
</style>
|
||||
|
||||
<template>
|
||||
|
@ -19,20 +19,20 @@
|
|||
<b-field>
|
||||
<b-input v-model="username"
|
||||
type="text"
|
||||
placeholder="Username"/>
|
||||
placeholder="Username" />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-input v-model="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
password-reveal/>
|
||||
password-reveal />
|
||||
</b-field>
|
||||
<b-field>
|
||||
<b-input v-model="rePassword"
|
||||
type="password"
|
||||
placeholder="Re-type Password"
|
||||
password-reveal
|
||||
@keyup.enter.native="register"/>
|
||||
@keyup.enter.native="register" />
|
||||
</b-field>
|
||||
|
||||
<p class="control has-addons is-pulled-right">
|
||||
|
@ -50,7 +50,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import Navbar from '../../components/navbar/Navbar.vue';
|
||||
import Navbar from '~/components/navbar/Navbar.vue';
|
||||
|
||||
export default {
|
||||
name: 'Register',
|
||||
|
@ -63,6 +63,11 @@ export default {
|
|||
isLoading: false
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
config() {
|
||||
return this.$store.state.config;
|
||||
}
|
||||
},
|
||||
metaInfo() {
|
||||
return { title: 'Register' };
|
||||
},
|
||||
|
@ -81,7 +86,7 @@ export default {
|
|||
return;
|
||||
}
|
||||
this.isLoading = true;
|
||||
this.axios.post(`${this.$config.baseURL}/auth/register`, {
|
||||
this.axios.post(`${this.config.baseURL}/auth/register`, {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(response => {
|
|
@ -0,0 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import Buefy from 'buefy';
|
||||
|
||||
Vue.use(Buefy);
|
|
@ -0,0 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import Clipboard from 'v-clipboard';
|
||||
|
||||
Vue.use(Clipboard);
|
|
@ -0,0 +1,12 @@
|
|||
import Vue from 'vue';
|
||||
import VueAnalytics from 'vue-analytics';
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
Vue.use(VueAnalytics, {
|
||||
id: 'UA-000000000-0',
|
||||
debug: {
|
||||
enabled: !isProduction,
|
||||
sendHitTask: isProduction
|
||||
}
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import axios from 'axios';
|
||||
import VueAxios from 'vue-axios';
|
||||
|
||||
Vue.use(VueAxios, axios);
|
||||
Vue.axios.defaults.headers.common.Accept = 'application/vnd.lolisafe.json';
|
|
@ -0,0 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import VueIsYourPasswordSafe from 'vue-isyourpasswordsafe';
|
||||
|
||||
Vue.use(VueIsYourPasswordSafe, {
|
||||
minLength: 6,
|
||||
maxLength: 64
|
||||
});
|
|
@ -0,0 +1,8 @@
|
|||
import Vue from 'vue';
|
||||
import VueTimeago from 'vue-timeago';
|
||||
|
||||
Vue.use(VueTimeago, {
|
||||
name: 'timeago',
|
||||
locale: 'en-US',
|
||||
locales: { 'en-US': require('vue-timeago/locales/en-US.json') }
|
||||
});
|
|
@ -1,21 +0,0 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
const router = new Router({
|
||||
mode: 'history',
|
||||
routes: [
|
||||
{ path: '/', component: () => import('../views/Home.vue') },
|
||||
{ path: '/login', component: () => import('../views/Auth/Login.vue') },
|
||||
{ path: '/register', component: () => import('../views/Auth/Register.vue') },
|
||||
{ path: '/dashboard', component: () => import('../views/Dashboard/Uploads.vue') },
|
||||
{ path: '/dashboard/albums', component: () => import('../views/Dashboard/Albums.vue') },
|
||||
{ path: '/dashboard/settings', component: () => import('../views/Dashboard/Settings.vue') },
|
||||
{ path: '/a/:identifier', component: () => import('../views/PublicAlbum.vue'), props: true },
|
||||
{ path: '/404', component: () => import('../views/NotFound.vue') },
|
||||
{ path: '*', component: () => import('../views/NotFound.vue') }
|
||||
]
|
||||
});
|
||||
|
||||
export default router;
|
|
@ -1,8 +1,6 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
const state = {
|
||||
loggedIn: false,
|
||||
user: {},
|
||||
|
@ -18,19 +16,19 @@ const mutations = {
|
|||
user(state, payload) {
|
||||
if (!payload) {
|
||||
state.user = {};
|
||||
localStorage.removeItem('ls-user');
|
||||
localStorage.removeItem('lolisafe-user');
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('ls-user', JSON.stringify(payload));
|
||||
localStorage.setItem('lolisafe-user', JSON.stringify(payload));
|
||||
state.user = payload;
|
||||
},
|
||||
token(state, payload) {
|
||||
if (!payload) {
|
||||
localStorage.removeItem('ls-token');
|
||||
localStorage.removeItem('lolisafe-token');
|
||||
state.token = null;
|
||||
return;
|
||||
}
|
||||
localStorage.setItem('ls-token', payload);
|
||||
localStorage.setItem('lolisafe-token', payload);
|
||||
setAuthorizationHeader(payload);
|
||||
state.token = payload;
|
||||
},
|
||||
|
@ -39,13 +37,22 @@ const mutations = {
|
|||
}
|
||||
};
|
||||
|
||||
const actions = {
|
||||
nuxtServerInit({ commit }, { req }) {
|
||||
const config = require('~/config.js');
|
||||
commit('config', config);
|
||||
}
|
||||
};
|
||||
|
||||
const setAuthorizationHeader = payload => {
|
||||
console.log('hihi');
|
||||
Vue.axios.defaults.headers.common.Authorization = payload ? `Bearer ${payload}` : '';
|
||||
};
|
||||
|
||||
const store = new Vuex.Store({
|
||||
const store = () => new Vuex.Store({
|
||||
state,
|
||||
mutations
|
||||
mutations,
|
||||
actions
|
||||
});
|
||||
|
||||
export default store;
|
||||
|
|
|
@ -1,172 +0,0 @@
|
|||
<style lang="scss" scoped>
|
||||
@import '../../styles/colors.scss';
|
||||
section { background-color: $backgroundLight1 !important; }
|
||||
section.hero div.hero-body {
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
div.view-container {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
div.album {
|
||||
display: flex;
|
||||
margin-bottom: 10px;
|
||||
|
||||
div.thumb {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
-webkit-box-shadow: $boxShadowLight;
|
||||
box-shadow: $boxShadowLight;
|
||||
}
|
||||
|
||||
div.info {
|
||||
margin-left: 15px;
|
||||
h4 {
|
||||
font-size: 1.5rem;
|
||||
a {
|
||||
color: $defaultTextColor;
|
||||
font-weight: 400;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
}
|
||||
span { display: block; }
|
||||
span:nth-child(3) {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
|
||||
div.latest {
|
||||
flex-grow: 1;
|
||||
justify-content: flex-end;
|
||||
display: flex;
|
||||
margin-left: 15px;
|
||||
|
||||
div.more {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
a {
|
||||
line-height: 1rem;
|
||||
color: $defaultTextColor;
|
||||
&:hover { text-decoration: underline; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
@import '../../styles/colors.scss';
|
||||
</style>
|
||||
|
||||
|
||||
<template>
|
||||
<section class="hero is-fullheight">
|
||||
<div class="hero-body">
|
||||
<div class="container">
|
||||
<div class="columns">
|
||||
<div class="column is-narrow">
|
||||
<Sidebar/>
|
||||
</div>
|
||||
<div class="column">
|
||||
|
||||
<h1 class="title">{{ albumName }}</h1>
|
||||
<hr>
|
||||
|
||||
<div class="view-container">
|
||||
<div v-for="album in albums"
|
||||
:key="album.id"
|
||||
class="album">
|
||||
<div class="thumb">
|
||||
<figure class="image is-64x64 thumb">
|
||||
<img src="../../assets/images/blank.png">
|
||||
</figure>
|
||||
</div>
|
||||
<div class="info">
|
||||
<h4>
|
||||
<router-link :to="`/dashboard/albums/${album.id}`">{{ album.name }}</router-link>
|
||||
</h4>
|
||||
<span>Updated <timeago :since="album.editedAt" /></span>
|
||||
<span>{{ album.fileCount || 0 }} files</span>
|
||||
</div>
|
||||
<div class="latest">
|
||||
<div v-for="file of album.files"
|
||||
:key="file.id"
|
||||
class="thumb">
|
||||
<figure class="image is-64x64">
|
||||
<a :href="file.url"
|
||||
target="_blank">
|
||||
<img :src="file.thumbSquare">
|
||||
</a>
|
||||
</figure>
|
||||
</div>
|
||||
<div v-if="album.fileCount > 5"
|
||||
class="thumb more no-background">
|
||||
<router-link :to="`/dashboard/albums/${album.id}`">{{ album.fileCount - 5 }}+ more</router-link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Sidebar from '../../components/sidebar/Sidebar.vue';
|
||||
import Grid from '../../components/grid/Grid.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Sidebar,
|
||||
Grid
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
albums: [],
|
||||
newAlbumName: null
|
||||
};
|
||||
},
|
||||
metaInfo() {
|
||||
return { title: 'Uploads' };
|
||||
},
|
||||
mounted() {
|
||||
this.getAlbums();
|
||||
this.$ga.page({
|
||||
page: '/dashboard/albums',
|
||||
title: 'Albums',
|
||||
location: window.location.href
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
async createAlbum() {
|
||||
if (!this.newAlbumName || this.newAlbumName === '') return;
|
||||
try {
|
||||
const response = await this.axios.post(`${this.$config.baseURL}/album/new`,
|
||||
{ name: this.newAlbumName });
|
||||
this.newAlbumName = null;
|
||||
this.$toast.open(response.data.message);
|
||||
this.getAlbums();
|
||||
return;
|
||||
} catch (error) {
|
||||
this.$onPromiseError(error);
|
||||
}
|
||||
},
|
||||
async getAlbums() {
|
||||
try {
|
||||
const response = await this.axios.get(`${this.$config.baseURL}/albums/mini`);
|
||||
this.albums = response.data.albums;
|
||||
console.log(this.albums);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
86
src/start.js
86
src/start.js
|
@ -1,23 +1,103 @@
|
|||
const Backend = require('./api/structures/Server');
|
||||
const express = require('express');
|
||||
const compression = require('compression');
|
||||
const ream = require('ream');
|
||||
// const ream = require('ream');
|
||||
const config = require('../config');
|
||||
const path = require('path');
|
||||
const log = require('./api/utils/Log');
|
||||
const dev = process.env.NODE_ENV !== 'production';
|
||||
const oneliner = require('one-liner');
|
||||
const jetpack = require('fs-jetpack');
|
||||
// const { Nuxt, Builder } = require('nuxt-edge');
|
||||
// const nuxtConfig = require('./nuxt/nuxt.config.js');
|
||||
|
||||
function startProduction() {
|
||||
startAPI();
|
||||
startSite();
|
||||
// startSite();
|
||||
// startNuxt();
|
||||
}
|
||||
|
||||
function startAPI() {
|
||||
writeFrontendConfig();
|
||||
new Backend().start();
|
||||
}
|
||||
|
||||
async function startNuxt() {
|
||||
/*
|
||||
Make sure the frontend has enough data to prepare the service
|
||||
*/
|
||||
writeFrontendConfig();
|
||||
|
||||
/*
|
||||
Starting Nuxt's custom server powered by express
|
||||
*/
|
||||
|
||||
const app = express();
|
||||
|
||||
/*
|
||||
Instantiate Nuxt.js
|
||||
*/
|
||||
nuxtConfig.dev = true;
|
||||
const nuxt = new Nuxt(nuxtConfig);
|
||||
|
||||
/*
|
||||
Start the server or build it if we're on dev mode
|
||||
*/
|
||||
|
||||
if (nuxtConfig.dev) {
|
||||
try {
|
||||
await new Builder(nuxt).build();
|
||||
} catch (error) {
|
||||
log.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Render every route with Nuxt.js
|
||||
*/
|
||||
app.use(nuxt.render);
|
||||
|
||||
/*
|
||||
Start the server and listen to the configured port
|
||||
*/
|
||||
app.listen(config.server.ports.frontend, '127.0.0.1');
|
||||
log.info(`> Frontend ready and listening on port ${config.server.ports.frontend}`);
|
||||
|
||||
/*
|
||||
Starting Nuxt's custom server powered by express
|
||||
*/
|
||||
/*
|
||||
const app = express();
|
||||
app.set('port', config.server.ports.frontend);
|
||||
|
||||
// Configure dev enviroment
|
||||
nuxtConfig.dev = dev;
|
||||
|
||||
// Init Nuxt.js
|
||||
const nuxt = new Nuxt(nuxtConfig);
|
||||
|
||||
// Build only in dev mode
|
||||
if (nuxtConfig.dev) {
|
||||
const builder = new Builder(nuxt);
|
||||
await builder.build();
|
||||
}
|
||||
|
||||
// Give nuxt middleware to express
|
||||
app.use(nuxt.render);
|
||||
|
||||
if (config.serveFilesWithNode) {
|
||||
app.use('/', express.static(`./${config.uploads.uploadFolder}`));
|
||||
}
|
||||
|
||||
// Listen the server
|
||||
app.listen(config.server.ports.frontend, '127.0.0.1');
|
||||
app.on('renderer-ready', () => log.info(`> Frontend ready and listening on port ${config.server.ports.frontend}`));
|
||||
// log.success(`> Frontend ready and listening on port ${config.server.ports.frontend}`);
|
||||
// console.log(`Server listening on http://${host}:${port}`); // eslint-disable-line no-console
|
||||
*/
|
||||
}
|
||||
|
||||
function startSite() {
|
||||
/*
|
||||
Make sure the frontend has enough data to prepare the service
|
||||
|
@ -74,5 +154,5 @@ function writeFrontendConfig() {
|
|||
const args = process.argv[2];
|
||||
if (!args) startProduction();
|
||||
else if (args === 'api') startAPI();
|
||||
else if (args === 'site') startSite();
|
||||
else if (args === 'site') startNuxt();
|
||||
else process.exit(0);
|
||||
|
|
Loading…
Reference in New Issue