mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-11-16 11:42:23 +08:00
Added publish providers and templates
This commit is contained in:
parent
0b0bec15e2
commit
872f557d03
|
@ -23,6 +23,9 @@ var proxyTable = config.dev.proxyTable
|
||||||
var app = express()
|
var app = express()
|
||||||
var compiler = webpack(webpackConfig)
|
var compiler = webpack(webpackConfig)
|
||||||
|
|
||||||
|
// StackEdit custom middlewares
|
||||||
|
require('./server')(app);
|
||||||
|
|
||||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||||
publicPath: webpackConfig.output.publicPath,
|
publicPath: webpackConfig.output.publicPath,
|
||||||
quiet: true
|
quiet: true
|
||||||
|
|
38
build/server.js
Normal file
38
build/server.js
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
var qs = require('qs');
|
||||||
|
var request = require('request');
|
||||||
|
|
||||||
|
function githubToken(clientId, code) {
|
||||||
|
return new Promise(function (resolve, reject) {
|
||||||
|
request({
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://github.com/login/oauth/access_token',
|
||||||
|
qs: {
|
||||||
|
client_id: clientId,
|
||||||
|
client_secret: process.env.GITHUB_SECRET,
|
||||||
|
code: code
|
||||||
|
},
|
||||||
|
}, function(err, res, body) {
|
||||||
|
if (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
console.log(body)
|
||||||
|
var token = qs.parse(body).access_token;
|
||||||
|
if (token) {
|
||||||
|
resolve(token);
|
||||||
|
} else {
|
||||||
|
reject(res.statusCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = function (app) {
|
||||||
|
app.get('/oauth2/githubToken', function (req, res) {
|
||||||
|
githubToken(req.query.clientId, req.query.code)
|
||||||
|
.then(function (token) {
|
||||||
|
res.send(token);
|
||||||
|
}, function (err) {
|
||||||
|
res.status(400).send(err ? err.message || err.toString() : 'bad_code');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
|
@ -32,6 +32,7 @@
|
||||||
"normalize-scss": "^7.0.0",
|
"normalize-scss": "^7.0.0",
|
||||||
"prismjs": "^1.6.0",
|
"prismjs": "^1.6.0",
|
||||||
"raw-loader": "^0.5.1",
|
"raw-loader": "^0.5.1",
|
||||||
|
"request": "^2.82.0",
|
||||||
"vue": "^2.3.3",
|
"vue": "^2.3.3",
|
||||||
"vuex": "^2.3.1"
|
"vuex": "^2.3.1"
|
||||||
},
|
},
|
||||||
|
|
13
src/assets/iconBlogger.svg
Normal file
13
src/assets/iconBlogger.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 44 KiB |
9
src/assets/iconDropbox.svg
Normal file
9
src/assets/iconDropbox.svg
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 42.4 39.5">
|
||||||
|
<g fill="#007EE5">
|
||||||
|
<path d="M12.5 0L0 8.1l8.7 7 12.5-7.8"/>
|
||||||
|
<path d="M0 21.9l12.5 8.2 8.7-7.3-12.5-7.7m12.5 7.7l8.8 7.3L42.4 22l-8.6-6.9m8.6-7L30 0l-8.8 7.3 12.6 7.8"/>
|
||||||
|
<path d="M21.3 24.4l-8.8 7.3-3.7-2.5V32l12.5 7.5L33.8 32v-2.8L30 31.7"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 338 B |
7
src/assets/iconGithub.svg
Normal file
7
src/assets/iconGithub.svg
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 58">
|
||||||
|
<g fill="none" fill-rule="evenodd">
|
||||||
|
<path d="m1324.62 140c-16.355 0-29.616 13.219-29.616 29.527 0 13.04 8.485 24.11 20.256 28.01 1.482.27 2.02-.642 2.02-1.425 0-.7-.025-2.557-.04-5.02-8.238 1.784-9.976-3.958-9.976-3.958-1.347-3.411-3.289-4.317-3.289-4.317-2.689-1.832.204-1.796.204-1.796 2.973.21 4.536 3.043 4.536 3.043 2.642 4.511 6.931 3.208 8.62 2.454.269-1.909 1.033-3.21 1.88-3.948-6.576-.745-13.491-3.279-13.491-14.592 0-3.223 1.155-5.858 3.049-7.922-.305-.747-1.322-3.748.289-7.814 0 0 2.487-.794 8.145 3.03 2.362-.656 4.896-.982 7.415-.995 2.515.013 5.05.339 7.415.995 5.655-3.821 8.136-3.03 8.136-3.03 1.616 4.065.6 7.07.295 7.814 1.898 2.064 3.045 4.7 3.045 7.922 0 11.343-6.925 13.838-13.524 14.569 1.064.912 2.01 2.713 2.01 5.468 0 3.946-.036 7.13-.036 8.098 0 .79.533 1.709 2.036 1.421 11.758-3.913 20.238-14.971 20.238-28.01 0-16.309-13.262-29.527-29.62-29.527" transform="translate(-1295-140)" fill="#181616"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1011 B |
8
src/assets/iconGoogleDrive.svg
Normal file
8
src/assets/iconGoogleDrive.svg
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 133156 115341">
|
||||||
|
<g>
|
||||||
|
<polygon style="fill:#3777E3" points="22194,115341 44385,76894 133156,76894 110963,115341 "/>
|
||||||
|
<polygon style="fill:#FFCF63" points="88772,76894 133156,76894 88772,0 44385,0 "/>
|
||||||
|
<polygon style="fill:#11A861" points="0,76894 22194,115341 66578,38447 44385,0 "/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 363 B |
12
src/assets/iconGooglePhotos.svg
Normal file
12
src/assets/iconGooglePhotos.svg
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 511">
|
||||||
|
<path d="M255.912,0.08c1.4,0.8 2.6,2 3.7,3.2c41.3,41.5 82.7,83 123.899,124.6c-26,25.6 -51.6,51.6 -77.399,77.3c-9.7,9.8 -19.601,19.4 -29.2,29.4c-7.2,-17.4 -14.1,-34.9 -21,-52.4c0,-18.2 0.1,-36.4 0,-54.7c-0.1,-42.4 -0.2,-84.9 0,-127.4l0,0Z" style="fill:#dc4b3e;fill-rule:nonzero;stroke:#dd4b39;stroke-width:0.09px;"/>
|
||||||
|
<path d="M127.812,127.48l128.1,0c0.1,18.3 0,36.5 0,54.7c-7.1,17.2 -14,34.5 -20.8,51.9c-2.2,-1.2 -3.8,-3 -5.5,-4.8l-101.4,-101.4l-0.4,-0.4Z" style="fill:#ff9e0e;fill-rule:nonzero;stroke:#ef851c;stroke-width:0.09px;"/>
|
||||||
|
<path d="M383.511,127.88l0.4,-0.3c-0.1,42.6 -0.1,85.3 0,127.9l-55.1,0c-17.2,-7.2 -34.601,-13.8 -51.9,-20.9c9.6,-10 19.5,-19.6 29.2,-29.4c25.801,-25.7 51.4,-51.7 77.4,-77.3l0,0Z" style="fill:#af195a;fill-rule:nonzero;stroke:#7e3794;stroke-width:0.09px;"/>
|
||||||
|
<path d="M106.912,148.98c7.2,-6.9 13.9,-14.3 21.3,-21.1l101.4,101.4c1.7,1.8 3.3,3.6 5.5,4.8c-2.3,1.7 -5.2,2.3 -7.8,3.5c-14.801,6 -29.801,11.6 -44.5,18c-18.301,-0.2 -36.601,-0.1 -54.9,-0.1c-42.6,-0.1 -85.2,0.2 -127.8,-0.1c35.5,-35.6 71.2,-71 106.8,-106.4l0,0Z" style="fill:#ffc112;fill-rule:nonzero;stroke:#ffbb1b;stroke-width:0.09px;"/>
|
||||||
|
<path d="M127.912,255.48c18.3,0 36.6,-0.1 54.9,0.1c17.3,7.1 34.6,13.8 51.899,20.8c-28.399,28.8 -57.099,57.2 -85.599,85.9c-7.2,6.8 -13.7,14.3 -21.3,20.7c0,-42.5 -0.1,-85 0.1,-127.5Z" style="fill:#17a05e;fill-rule:nonzero;stroke:#1a8763;stroke-width:0.09px;"/>
|
||||||
|
<path d="M328.812,255.48l55.1,0c42.5,0.1 85.1,-0.1 127.6,0.1c-27.3,27.7 -55,55.1 -82.399,82.6c-15.2,15.1 -30.2,30.399 -45.4,45.3c-34,-34.4 -68.5,-68.4 -102.6,-102.8c-1.4,-1.5 -2.9,-2.8 -4.601,-3.8c2.9,-1.801 6.101,-2.7 9.2,-4c14.4,-5.8 28.799,-11.4 43.1,-17.4l0,0Z" style="fill:#4587f4;fill-rule:nonzero;stroke:#427fed;stroke-width:0.09px;"/>
|
||||||
|
<path d="M234.712,276.38c7.3,17.399 13.9,35 21.2,52.399c-0.1,18.2 0,36.5 -0.1,54.7l0,88c-0.2,13.1 0.3,26.2 -0.2,39.2c-2.101,-1 -3.4,-2.9 -5.101,-4.5c-40.899,-41.099 -81.699,-82.199 -122.699,-123.199c7.6,-6.4 14.1,-13.9 21.3,-20.7c28.5,-28.7 57.2,-57.1 85.6,-85.9Z" style="fill:#8dc44d;fill-rule:nonzero;stroke:#65b045;stroke-width:0.09px;"/>
|
||||||
|
<path d="M276.511,276.88c1.7,1 3.2,2.3 4.601,3.8c34.1,34.4 68.6,68.4 102.6,102.8c-42.7,-0.1 -85.3,0.1 -127.899,0c0.1,-18.2 0,-36.5 0.1,-54.7c6.699,-17.3 13.899,-34.5 20.598,-51.9l0,0Z" style="fill:#3569d6;fill-rule:nonzero;stroke:#43459d;stroke-width:0.09px;"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
19
src/assets/iconStackedit.svg
Normal file
19
src/assets/iconStackedit.svg
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 126 126">
|
||||||
|
<path d="M103.5,0c3.762,0.003 7.395,0.971 10.725,2.716c0.966,0.506 1.808,1.221 2.712,1.831l-53.937,40.453l-53.937,-40.453c4.402,-3.289 8.02,-4.273 13.437,-4.547l81,0Z" style="fill:#ffe600;fill-rule:nonzero;"/>
|
||||||
|
<path d="M9.063,4.547l53.937,40.453l-58.04,72.55c-3.44,-4.417 -4.681,-8.528 -4.96,-14.05l0,-81c0.064,-5.221 1.801,-10.265 5.138,-14.312c0.914,-1.109 2.033,-2.033 3.05,-3.05l0.875,-0.591l0,0Z" style="fill:#bbd500;fill-rule:nonzero;"/>
|
||||||
|
<path d="M63,45l58.04,72.549l-0.178,0.263c-4.901,5.465 -10.068,7.82 -17.362,8.188l-81,0c-5.221,-0.065 -10.265,-1.801 -14.312,-5.138c-1.109,-0.915 -2.033,-2.034 -3.05,-3.05l-0.177,-0.262l58.039,-72.55l0,0Z" style="fill:#ff8a00;fill-rule:nonzero;"/>
|
||||||
|
<path d="M116.937,4.547c3.844,2.631 6.684,6.83 8.051,11.262c0.441,1.427 0.673,2.914 0.896,4.391c0.114,0.759 0.077,1.533 0.116,2.3l0,81c-0.023,3.748 -0.939,7.415 -2.716,10.725c-0.632,1.178 -1.496,2.216 -2.245,3.325l-58.039,-72.55l53.937,-40.453l0,0Z" style="fill:#75b7fd;fill-rule:nonzero;"/>
|
||||||
|
<path d="M32.063,12l61.874,0c7.767,0 14.063,6.296 14.063,14.063l0,61.874c0,7.767 -6.296,14.063 -14.063,14.063l-61.875,0c-7.766,0 -14.062,-6.296 -14.062,-14.063l0,-61.875c0,-7.766 6.296,-14.062 14.062,-14.062l0.001,0Z" style="fill:#fff;fill-rule:nonzero;"/>
|
||||||
|
<g>
|
||||||
|
<g>
|
||||||
|
<rect x="40.711" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
|
||||||
|
<rect x="56.263" y="24.501" width="7.026" height="50.66" style="fill:#737373;"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<path d="M71.278,44.466l0,-8.841l-38.556,0l0,8.841l38.556,0Z" style="fill:#737373;fill-rule:nonzero;"/>
|
||||||
|
<rect x="32.722" y="55.195" width="38.556" height="8.842" style="fill:#737373;fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -2,6 +2,7 @@
|
||||||
<div v-if="ready" class="app" :class="{'app--loading': loading}">
|
<div v-if="ready" class="app" :class="{'app--loading': loading}">
|
||||||
<layout></layout>
|
<layout></layout>
|
||||||
<modal v-if="showModal"></modal>
|
<modal v-if="showModal"></modal>
|
||||||
|
<notification></notification>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="app__spash-screen"></div>
|
<div v-else class="app__spash-screen"></div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -11,6 +12,7 @@ import Vue from 'vue';
|
||||||
import { mapState } from 'vuex';
|
import { mapState } from 'vuex';
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
|
import Notification from './Notification';
|
||||||
|
|
||||||
// Global directives
|
// Global directives
|
||||||
Vue.directive('focus', {
|
Vue.directive('focus', {
|
||||||
|
@ -23,6 +25,7 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
|
Notification,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
|
@ -32,7 +35,7 @@ export default {
|
||||||
return !this.$store.getters['content/current'].id;
|
return !this.$store.getters['content/current'].id;
|
||||||
},
|
},
|
||||||
showModal() {
|
showModal() {
|
||||||
return !!this.$store.state.modal.config;
|
return !!this.$store.getters['modal/config'];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
|
<div class="explorer-node" :class="{'explorer-node--selected': isSelected, 'explorer-node--open': isOpen, 'explorer-node--drag-target': isDragTargetFolder}" @dragover.prevent @dragenter.stop="setDragTarget(node.item.id)" @dragleave.stop="isDragTarget && setDragTargetId()" @drop.prevent.stop="onDrop">
|
||||||
<div v-if="isEditing" class="explorer-node__item-editor" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
|
<div class="explorer-node__item-editor" v-if="isEditing" :class="['explorer-node__item-editor--' + node.item.type]" :style="{'padding-left': leftPadding}">
|
||||||
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
<input type="text" class="text-input" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc="submitEdit(true)" v-model="editingNodeName">
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="explorer-node__item" :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
|
<div class="explorer-node__item" v-else :class="['explorer-node__item--' + node.item.type]" :style="{'padding-left': leftPadding}" @click="select(node.item.id)" draggable="true" @dragstart.stop="setDragSourceId" @dragend.stop="setDragTargetId()">
|
||||||
{{node.item.name}}
|
{{node.item.name}}
|
||||||
|
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
|
<div class="explorer-node__children" v-if="node.isFolder && isOpen">
|
||||||
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
<explorer-node v-for="node in node.folders" :key="node.item.id" :node="node" :depth="depth + 1"></explorer-node>
|
||||||
|
@ -112,10 +113,11 @@ export default {
|
||||||
this.$store.commit('explorer/setNewItem', null);
|
this.$store.commit('explorer/setNewItem', null);
|
||||||
},
|
},
|
||||||
submitEdit(cancel) {
|
submitEdit(cancel) {
|
||||||
const id = this.$store.getters['explorer/editingNode'].item.id;
|
const editingNode = this.$store.getters['explorer/editingNode'];
|
||||||
|
const id = editingNode.item.id;
|
||||||
const value = this.editingValue;
|
const value = this.editingValue;
|
||||||
if (!cancel && id && value) {
|
if (!cancel && id && value) {
|
||||||
this.$store.commit('file/patchItem', {
|
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
||||||
id,
|
id,
|
||||||
name: value.slice(0, 250),
|
name: value.slice(0, 250),
|
||||||
});
|
});
|
||||||
|
@ -166,6 +168,7 @@ $item-font-size: 14px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
padding-right: 5px;
|
||||||
|
|
||||||
.explorer-node--selected > & {
|
.explorer-node--selected > & {
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
@ -179,6 +182,13 @@ $item-font-size: 14px;
|
||||||
.explorer__tree--new-item & {
|
.explorer__tree--new-item & {
|
||||||
opacity: 0.33;
|
opacity: 0.33;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explorer-node__location {
|
||||||
|
float: right;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
margin: 2px 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.explorer-node__item--folder,
|
.explorer-node__item--folder,
|
||||||
|
|
|
@ -98,7 +98,6 @@ export default {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
-webkit-flex: none;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,10 +3,22 @@
|
||||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||||
|
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
||||||
|
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
||||||
|
<google-drive-sync-modal v-else-if="config.type === 'googleDriveSync'"></google-drive-sync-modal>
|
||||||
|
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
||||||
|
<dropbox-sync-modal v-else-if="config.type === 'dropboxSync'"></dropbox-sync-modal>
|
||||||
|
<dropbox-publish-modal v-else-if="config.type === 'dropboxPublish'"></dropbox-publish-modal>
|
||||||
|
<github-sync-modal v-else-if="config.type === 'githubSync'"></github-sync-modal>
|
||||||
|
<github-publish-modal v-else-if="config.type === 'githubPublish'"></github-publish-modal>
|
||||||
|
<gist-sync-modal v-else-if="config.type === 'gistSync'"></gist-sync-modal>
|
||||||
|
<gist-publish-modal v-else-if="config.type === 'gistPublish'"></gist-publish-modal>
|
||||||
|
<blogger-publish-modal v-else-if="config.type === 'bloggerPublish'"></blogger-publish-modal>
|
||||||
|
<blogger-page-publish-modal v-else-if="config.type === 'bloggerPagePublish'"></blogger-page-publish-modal>
|
||||||
<div v-else class="modal__inner-1">
|
<div v-else class="modal__inner-1">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="modal__content" v-html="config.content"></div>
|
<div class="modal__content" v-html="config.content"></div>
|
||||||
|
@ -20,35 +32,56 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapMutations } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import FilePropertiesModal from './FilePropertiesModal';
|
|
||||||
import SettingsModal from './SettingsModal';
|
|
||||||
import TemplatesModal from './TemplatesModal';
|
|
||||||
import LinkModal from './LinkModal';
|
|
||||||
import ImageModal from './ImageModal';
|
|
||||||
import GooglePhotoModal from './GooglePhotoModal';
|
|
||||||
import HtmlExportModal from './HtmlExportModal';
|
|
||||||
import editorEngineSvc from '../services/editorEngineSvc';
|
import editorEngineSvc from '../services/editorEngineSvc';
|
||||||
|
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||||
|
import SettingsModal from './modals/SettingsModal';
|
||||||
|
import TemplatesModal from './modals/TemplatesModal';
|
||||||
|
import HtmlExportModal from './modals/HtmlExportModal';
|
||||||
|
import LinkModal from './modals/LinkModal';
|
||||||
|
import ImageModal from './modals/ImageModal';
|
||||||
|
import GooglePhotoModal from './modals/GooglePhotoModal';
|
||||||
|
import SyncManagementModal from './modals/SyncManagementModal';
|
||||||
|
import PublishManagementModal from './modals/PublishManagementModal';
|
||||||
|
import GoogleDriveSyncModal from './modals/GoogleDriveSyncModal';
|
||||||
|
import GoogleDrivePublishModal from './modals/GoogleDrivePublishModal';
|
||||||
|
import DropboxSyncModal from './modals/DropboxSyncModal';
|
||||||
|
import DropboxPublishModal from './modals/DropboxPublishModal';
|
||||||
|
import GithubSyncModal from './modals/GithubSyncModal';
|
||||||
|
import GithubPublishModal from './modals/GithubPublishModal';
|
||||||
|
import GistSyncModal from './modals/GistSyncModal';
|
||||||
|
import GistPublishModal from './modals/GistPublishModal';
|
||||||
|
import BloggerPublishModal from './modals/BloggerPublishModal';
|
||||||
|
import BloggerPagePublishModal from './modals/BloggerPagePublishModal';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
FilePropertiesModal,
|
FilePropertiesModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
TemplatesModal,
|
TemplatesModal,
|
||||||
|
HtmlExportModal,
|
||||||
LinkModal,
|
LinkModal,
|
||||||
ImageModal,
|
ImageModal,
|
||||||
GooglePhotoModal,
|
GooglePhotoModal,
|
||||||
HtmlExportModal,
|
SyncManagementModal,
|
||||||
|
PublishManagementModal,
|
||||||
|
GoogleDriveSyncModal,
|
||||||
|
GoogleDrivePublishModal,
|
||||||
|
DropboxSyncModal,
|
||||||
|
DropboxPublishModal,
|
||||||
|
GithubSyncModal,
|
||||||
|
GithubPublishModal,
|
||||||
|
GistSyncModal,
|
||||||
|
GistPublishModal,
|
||||||
|
BloggerPublishModal,
|
||||||
|
BloggerPagePublishModal,
|
||||||
},
|
},
|
||||||
computed: mapState('modal', [
|
computed: mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
methods: {
|
methods: {
|
||||||
...mapMutations('modal', [
|
|
||||||
'setConfig',
|
|
||||||
]),
|
|
||||||
onEscape() {
|
onEscape() {
|
||||||
this.setConfig();
|
this.config.reject();
|
||||||
editorEngineSvc.clEditor.focus();
|
editorEngineSvc.clEditor.focus();
|
||||||
},
|
},
|
||||||
onFocusInOut(evt) {
|
onFocusInOut(evt) {
|
||||||
|
@ -69,7 +102,7 @@ export default {
|
||||||
}
|
}
|
||||||
target = target.parentNode;
|
target = target.parentNode;
|
||||||
}
|
}
|
||||||
this.setConfig();
|
this.config.reject();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -78,7 +111,7 @@ export default {
|
||||||
window.addEventListener('focusout', this.onFocusInOut);
|
window.addEventListener('focusout', this.onFocusInOut);
|
||||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
const eltToFocus = this.$el.querySelector('input.text-input')
|
||||||
|| this.$el.querySelector('.textfield')
|
|| this.$el.querySelector('.textfield')
|
||||||
|| this.$el.querySelector('button.button');
|
|| this.$el.querySelector('.button');
|
||||||
if (eltToFocus) {
|
if (eltToFocus) {
|
||||||
eltToFocus.focus();
|
eltToFocus.focus();
|
||||||
}
|
}
|
||||||
|
@ -103,7 +136,6 @@ export default {
|
||||||
|
|
||||||
.modal__inner-1 {
|
.modal__inner-1 {
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
display: table;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
min-width: 320px;
|
min-width: 320px;
|
||||||
max-width: 500px;
|
max-width: 500px;
|
||||||
|
@ -142,10 +174,30 @@ export default {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal__image {
|
||||||
|
float: left;
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
margin: 1.5em 1.5em 0.5em 0;
|
||||||
|
|
||||||
|
& + *::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal__error {
|
.modal__error {
|
||||||
color: #de2c00;
|
color: #de2c00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal__tip {
|
||||||
|
background-color: transparentize(#ffd600, 0.85);
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
margin: 1.2em 0;
|
||||||
|
padding: 0.75em 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
.modal__button-bar {
|
.modal__button-bar {
|
||||||
margin-top: 1.75rem;
|
margin-top: 1.75rem;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
|
@ -2,27 +2,38 @@
|
||||||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||||
<icon-folder-multiple></icon-folder-multiple>
|
<icon-folder-open></icon-folder-open>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--button">
|
||||||
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
|
<button class="navigation-bar__button navigation-bar__button--stackedit button" @click="toggleSideBar()">
|
||||||
<icon-stackedit></icon-stackedit>
|
<icon-provider provider-id="stackedit"></icon-provider>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
|
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
||||||
<div class="navigation-bar__spinner" v-show="showSpinner">
|
<div class="navigation-bar__spinner">
|
||||||
<div class="spinner"></div>
|
<div v-show="showSpinner" class="spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
|
<div class="navigation-bar__title navigation-bar__title--fake text-input"></div>
|
||||||
<div class="navigation-bar__title navigation-bar__title--text text-input" v-bind:style="{maxWidth: styles.titleMaxWidth + 'px'}">{{title}}</div>
|
<div class="navigation-bar__title navigation-bar__title--text text-input" :style="{width: titleWidth + 'px'}">{{title}}</div>
|
||||||
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" v-bind:style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" v-on:mouseenter="titleHover = true" v-on:mouseleave="titleHover = false" v-model="title">
|
<input class="navigation-bar__title navigation-bar__title--input text-input" :class="{'navigation-bar__title--focus': titleFocus, 'navigation-bar__title--scrolling': titleScrolling}" :style="{width: titleWidth + 'px'}" @focus="editTitle(true)" @blur="editTitle(false)" @keyup.enter="submitTitle()" @keyup.esc="submitTitle(true)" @mouseenter="titleHover = true" @mouseleave="titleHover = false" v-model="title">
|
||||||
<button v-if="!offline && isSyncPossible" class="navigation-bar__button button" :disabled="isSyncRequested" @click="requestSync">
|
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
|
||||||
<icon-sync></icon-sync>
|
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in syncLocations" :key="location.id" :href="location.url" target="_blank">
|
||||||
</button>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
<button v-if="offline && isSyncPossible" class="navigation-bar__button navigation-bar__button--sync-off button" disabled="disabled">
|
</a>
|
||||||
<icon-sync-off></icon-sync-off>
|
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="!offline && isSyncPossible" :disabled="isSyncRequested" @click="requestSync">
|
||||||
</button>
|
<icon-sync></icon-sync>
|
||||||
|
</button>
|
||||||
|
<button class="navigation-bar__button navigation-bar__button--sync-off button" v-if="offline && isSyncPossible" disabled="disabled">
|
||||||
|
<icon-sync-off></icon-sync-off>
|
||||||
|
</button>
|
||||||
|
<a class="navigation-bar__button navigation-bar__button--location button" :class="{'navigation-bar__button--blink': location.id === currentLocation.id}" v-for="location in publishLocations" :key="location.id" :href="location.url" target="_blank">
|
||||||
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
|
</a>
|
||||||
|
<button class="navigation-bar__button navigation-bar__button--publish button" v-if="publishLocations.length" :disabled="isPublishRequested || offline" @click="requestPublish">
|
||||||
|
<icon-upload></icon-upload>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
<div class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
||||||
|
@ -69,6 +80,7 @@
|
||||||
import { mapState, mapGetters, mapActions } from 'vuex';
|
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
|
import publishSvc from '../services/publishSvc';
|
||||||
import animationSvc from '../services/animationSvc';
|
import animationSvc from '../services/animationSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -84,10 +96,18 @@ export default {
|
||||||
]),
|
]),
|
||||||
...mapState('queue', [
|
...mapState('queue', [
|
||||||
'isSyncRequested',
|
'isSyncRequested',
|
||||||
|
'isPublishRequested',
|
||||||
|
'currentLocation',
|
||||||
]),
|
]),
|
||||||
...mapGetters('layout', [
|
...mapGetters('layout', [
|
||||||
'styles',
|
'styles',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('syncLocation', {
|
||||||
|
syncLocations: 'current',
|
||||||
|
}),
|
||||||
|
...mapGetters('publishLocation', {
|
||||||
|
publishLocations: 'current',
|
||||||
|
}),
|
||||||
isSyncPossible() {
|
isSyncPossible() {
|
||||||
return this.$store.getters['data/loginToken'] ||
|
return this.$store.getters['data/loginToken'] ||
|
||||||
this.$store.getters['syncLocation/current'].length;
|
this.$store.getters['syncLocation/current'].length;
|
||||||
|
@ -134,6 +154,11 @@ export default {
|
||||||
syncSvc.requestSync();
|
syncSvc.requestSync();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
requestPublish() {
|
||||||
|
if (!this.isPublishRequested) {
|
||||||
|
publishSvc.requestPublish();
|
||||||
|
}
|
||||||
|
},
|
||||||
pagedownClick(name) {
|
pagedownClick(name) {
|
||||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||||
},
|
},
|
||||||
|
@ -183,6 +208,10 @@ export default {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-bar__hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-bar__inner--left {
|
.navigation-bar__inner--left {
|
||||||
float: left;
|
float: left;
|
||||||
|
|
||||||
|
@ -201,23 +230,30 @@ export default {
|
||||||
|
|
||||||
.navigation-bar__inner--edit-buttons {
|
.navigation-bar__inner--edit-buttons {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
|
|
||||||
|
.navigation-bar__button {
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-bar__inner--title * {
|
||||||
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar__button {
|
.navigation-bar__button {
|
||||||
width: 34px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 7px;
|
padding: 0 8px;
|
||||||
|
|
||||||
/* prevent from seeing wrapped buttons */
|
/* prevent from seeing wrapped buttons */
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
|
|
||||||
.navigation-bar__inner--button & {
|
.navigation-bar__inner--button & {
|
||||||
padding: 6px;
|
padding: 0 4px;
|
||||||
width: 38px;
|
width: 38px;
|
||||||
|
|
||||||
&.navigation-bar__button--stackedit {
|
&.navigation-bar__button--stackedit {
|
||||||
opacity: 0.8;
|
opacity: 0.8;
|
||||||
padding: 4px;
|
|
||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
|
@ -228,6 +264,10 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-bar__title {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-bar__title,
|
.navigation-bar__title,
|
||||||
.navigation-bar__button {
|
.navigation-bar__button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -236,6 +276,13 @@ export default {
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-bar__button--sync,
|
||||||
|
.navigation-bar__button--sync-off,
|
||||||
|
.navigation-bar__button--publish {
|
||||||
|
padding: 0 6px;
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-bar__button[disabled] {
|
.navigation-bar__button[disabled] {
|
||||||
&,
|
&,
|
||||||
&:active,
|
&:active,
|
||||||
|
@ -250,7 +297,7 @@ export default {
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
color: #f20;
|
color: $error-color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -264,6 +311,30 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-bar__button--location {
|
||||||
|
width: 20px;
|
||||||
|
padding: 0 2px 12px;
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigation-bar__button--blink {
|
||||||
|
animation: blink 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes blink {
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
filter: contrast(0.8) brightness(1.25);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-bar__title--fake {
|
.navigation-bar__title--fake {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: -9999px;
|
left: -9999px;
|
||||||
|
@ -283,7 +354,8 @@ export default {
|
||||||
|
|
||||||
.navigation-bar__title--input,
|
.navigation-bar__title--input,
|
||||||
.navigation-bar__inner--edit-buttons,
|
.navigation-bar__inner--edit-buttons,
|
||||||
.navigation-bar__inner--button {
|
.navigation-bar__inner--button,
|
||||||
|
.navigation-bar__spinner {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
.navigation-bar--editor & {
|
.navigation-bar--editor & {
|
||||||
|
@ -291,6 +363,14 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.navigation-bar__button {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
.navigation-bar--editor & {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.navigation-bar__title--input {
|
.navigation-bar__title--input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
|
@ -299,16 +379,17 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar__spinner {
|
|
||||||
margin: 10px 5px 0 15px;
|
|
||||||
color: rgba(255, 255, 255, 0.33);
|
|
||||||
}
|
|
||||||
|
|
||||||
$r: 9px;
|
$r: 9px;
|
||||||
$d: $r * 2;
|
$d: $r * 2;
|
||||||
$b: $d/10;
|
$b: $d/10;
|
||||||
$t: 1500ms;
|
$t: 1500ms;
|
||||||
|
|
||||||
|
.navigation-bar__spinner {
|
||||||
|
width: $d;
|
||||||
|
margin: 10px 5px 0 10px;
|
||||||
|
color: rgba(255, 255, 255, 0.67);
|
||||||
|
}
|
||||||
|
|
||||||
.spinner {
|
.spinner {
|
||||||
width: $d;
|
width: $d;
|
||||||
height: $d;
|
height: $d;
|
||||||
|
|
52
src/components/Notification.vue
Normal file
52
src/components/Notification.vue
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
<template>
|
||||||
|
<div class="notification">
|
||||||
|
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
||||||
|
<div class="notification__icon flex flex--column flex--center">
|
||||||
|
<icon-information v-if="item.type === 'info'"></icon-information>
|
||||||
|
<icon-alert v-else-if="item.type === 'error'"></icon-alert>
|
||||||
|
</div>
|
||||||
|
<div class="notification__content">
|
||||||
|
{{item.content}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: mapState('notification', [
|
||||||
|
'items',
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import 'common/variables.scss';
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__item {
|
||||||
|
margin: 10px;
|
||||||
|
padding: 10px 15px;
|
||||||
|
line-height: 1.4;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.9em;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification__icon {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -12,110 +12,14 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar__inner">
|
<div class="side-bar__inner">
|
||||||
<!-- Main menu -->
|
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||||
<div v-if="panel === 'menu'" class="side-bar__panel side-bar__panel--menu">
|
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||||
<menu-entry v-if="!loginToken" @click.native="signin">
|
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||||
<icon-login slot="icon"></icon-login>
|
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||||
<div>Sign in with Google</div>
|
<more-menu v-else-if="panel === 'more'"></more-menu>
|
||||||
<span>Back up and sync all your files, folders and settings.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<hr v-if="!loginToken">
|
|
||||||
<menu-entry @click.native="setPanel('sync')">
|
|
||||||
<icon-sync slot="icon"></icon-sync>
|
|
||||||
<div>Synchronize</div>
|
|
||||||
<span>Open, save, collaborate in the Cloud.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('publish')">
|
|
||||||
<icon-upload slot="icon"></icon-upload>
|
|
||||||
<div>Publish</div>
|
|
||||||
<span>Export to the web.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="fileProperties">
|
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
|
||||||
<div>File properties</div>
|
|
||||||
<span>Add publication metadata and configure extensions.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('toc')">
|
|
||||||
<icon-toc slot="icon"></icon-toc>
|
|
||||||
Table of contents
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('help')">
|
|
||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
|
||||||
Markdown cheat sheet
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="importFile">
|
|
||||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
|
||||||
Import from disk
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="setPanel('export')">
|
|
||||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
|
||||||
Export to disk
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="setPanel('more')">
|
|
||||||
More...
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<!-- Sync menu -->
|
|
||||||
<div v-else-if="panel === 'sync'" class="side-bar__panel side-bar__panel--menu">
|
|
||||||
<div v-for="token in googleDriveTokens" :key="token.sub">
|
|
||||||
<menu-entry @click.native="openGoogleDrive(token)">
|
|
||||||
<icon-google-drive slot="icon"></icon-google-drive>
|
|
||||||
<div>Open from Google Drive</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="saveGoogleDrive(token)">
|
|
||||||
<icon-google-drive slot="icon"></icon-google-drive>
|
|
||||||
<div>Save on Google Drive</div>
|
|
||||||
<span>{{token.name}}</span>
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
|
||||||
</div>
|
|
||||||
<menu-entry @click.native="addGoogleDriveAccount">
|
|
||||||
<icon-google-drive slot="icon"></icon-google-drive>
|
|
||||||
<span>Add Google Drive account</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<!-- More menu -->
|
|
||||||
<div v-else-if="panel === 'more'" class="side-bar__panel side-bar__panel--menu">
|
|
||||||
<menu-entry @click.native="settings">
|
|
||||||
<icon-settings slot="icon"></icon-settings>
|
|
||||||
<div>Settings</div>
|
|
||||||
<span>Tweak application and keyboard shortcuts.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="templates">
|
|
||||||
<icon-code-braces slot="icon"></icon-code-braces>
|
|
||||||
<div>Templates</div>
|
|
||||||
<span>Configure Handlebars templates for your exports.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="reset">
|
|
||||||
<icon-logout slot="icon"></icon-logout>
|
|
||||||
<div>Reset application</div>
|
|
||||||
<span>Sign out and clean local data.</span>
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<!-- Export menu -->
|
|
||||||
<div v-else-if="panel === 'export'" class="side-bar__panel side-bar__panel--menu">
|
|
||||||
<menu-entry @click.native="exportMarkdown">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
Export as Markdown
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="exportHtml">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
Export as HTML
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="exportPdf">
|
|
||||||
<icon-download slot="icon"></icon-download>
|
|
||||||
Export as PDF
|
|
||||||
</menu-entry>
|
|
||||||
</div>
|
|
||||||
<!-- Help -->
|
|
||||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||||
</div>
|
</div>
|
||||||
<!-- TOC -->
|
|
||||||
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
|
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
|
||||||
<toc>
|
<toc>
|
||||||
</toc>
|
</toc>
|
||||||
|
@ -125,16 +29,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import Toc from './Toc';
|
import Toc from './Toc';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './menus/MenuEntry';
|
||||||
|
import MainMenu from './menus/MainMenu';
|
||||||
|
import SyncMenu from './menus/SyncMenu';
|
||||||
|
import PublishMenu from './menus/PublishMenu';
|
||||||
|
import ExportMenu from './menus/ExportMenu';
|
||||||
|
import MoreMenu from './menus/MoreMenu';
|
||||||
import markdownSample from '../data/markdownSample.md';
|
import markdownSample from '../data/markdownSample.md';
|
||||||
import markdownConversionSvc from '../services/markdownConversionSvc';
|
import markdownConversionSvc from '../services/markdownConversionSvc';
|
||||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
|
||||||
import googleDriveProvider from '../services/providers/googleDriveProvider';
|
|
||||||
import syncSvc from '../services/syncSvc';
|
|
||||||
import localDbSvc from '../services/localDbSvc';
|
|
||||||
import exportSvc from '../services/exportSvc';
|
|
||||||
|
|
||||||
const panelNames = {
|
const panelNames = {
|
||||||
menu: 'Menu',
|
menu: 'Menu',
|
||||||
|
@ -150,27 +54,22 @@ export default {
|
||||||
components: {
|
components: {
|
||||||
Toc,
|
Toc,
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
|
MainMenu,
|
||||||
|
SyncMenu,
|
||||||
|
PublishMenu,
|
||||||
|
ExportMenu,
|
||||||
|
MoreMenu,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('data', [
|
|
||||||
'loginToken',
|
|
||||||
]),
|
|
||||||
panel() {
|
panel() {
|
||||||
return this.$store.getters['data/localSettings'].sideBarPanel;
|
return this.$store.getters['data/localSettings'].sideBarPanel;
|
||||||
},
|
},
|
||||||
panelName() {
|
panelName() {
|
||||||
return panelNames[this.panel];
|
return panelNames[this.panel];
|
||||||
},
|
},
|
||||||
googleDriveTokens() {
|
|
||||||
const googleToken = this.$store.getters['data/googleTokens'];
|
|
||||||
return Object.keys(googleToken)
|
|
||||||
.map(sub => googleToken[sub])
|
|
||||||
.filter(token => token.isDrive)
|
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('data', [
|
...mapActions('data', [
|
||||||
|
@ -179,47 +78,6 @@ export default {
|
||||||
...mapActions('data', {
|
...mapActions('data', {
|
||||||
setPanel: 'setSideBarPanel',
|
setPanel: 'setSideBarPanel',
|
||||||
}),
|
}),
|
||||||
signin() {
|
|
||||||
return googleHelper.signin()
|
|
||||||
.then(() => syncSvc.requestSync());
|
|
||||||
},
|
|
||||||
importFile() {
|
|
||||||
return this.$store.dispatch('modal/notImplemented');
|
|
||||||
},
|
|
||||||
fileProperties() {
|
|
||||||
return this.$store.dispatch('modal/open', 'fileProperties')
|
|
||||||
.then(properties => this.$store.dispatch('content/patchCurrent', { properties }));
|
|
||||||
},
|
|
||||||
settings() {
|
|
||||||
return this.$store.dispatch('modal/open', 'settings')
|
|
||||||
.then(settings => this.$store.dispatch('data/setSettings', settings));
|
|
||||||
},
|
|
||||||
templates() {
|
|
||||||
return this.$store.dispatch('modal/open', 'templates')
|
|
||||||
.then(templates => this.$store.dispatch('data/setTemplates', templates));
|
|
||||||
},
|
|
||||||
reset() {
|
|
||||||
return this.$store.dispatch('modal/reset')
|
|
||||||
.then(() => localDbSvc.removeDb());
|
|
||||||
},
|
|
||||||
addGoogleDriveAccount() {
|
|
||||||
return googleHelper.addGoogleDriveAccount();
|
|
||||||
},
|
|
||||||
openGoogleDrive(token) {
|
|
||||||
return googleHelper.openPicker(token, 'doc')
|
|
||||||
.then(files => this.$store.dispatch('queue/enqueue',
|
|
||||||
() => googleDriveProvider.openFiles(token, files)));
|
|
||||||
},
|
|
||||||
exportMarkdown() {
|
|
||||||
const currentFile = this.$store.getters['file/current'];
|
|
||||||
return exportSvc.exportToDisk(currentFile.id, 'md');
|
|
||||||
},
|
|
||||||
exportHtml() {
|
|
||||||
return this.$store.dispatch('modal/open', 'htmlExport');
|
|
||||||
},
|
|
||||||
exportPdf() {
|
|
||||||
return this.$store.dispatch('modal/notImplemented');
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -233,6 +91,15 @@ export default {
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
* + hr {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr + hr {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,7 +120,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-bar__panel--menu {
|
.side-bar__panel--menu {
|
||||||
padding: 10px;
|
padding: 10px 10px 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.side-bar__panel--help {
|
.side-bar__panel--help {
|
||||||
|
@ -275,4 +142,16 @@ export default {
|
||||||
background-color: rgba(0, 0, 0, 0.05);
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.side-bar__warning {
|
||||||
|
padding: 10px;
|
||||||
|
margin: 0 -10px;
|
||||||
|
color: darken($error-color, 10);
|
||||||
|
background-color: transparentize($error-color, 0.925);
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 10px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,14 +1,24 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="toc">
|
<div class="toc">
|
||||||
<div class="toc__inner">
|
<div class="toc__mask" :style="{top: (maskY - 5) + 'px'}"></div>
|
||||||
</div>
|
<div class="toc__inner"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
import editorSvc from '../services/editorSvc';
|
import editorSvc from '../services/editorSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
maskY: -999,
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('layout', [
|
||||||
|
'styles',
|
||||||
|
]),
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const tocElt = this.$el.querySelector('.toc__inner');
|
const tocElt = this.$el.querySelector('.toc__inner');
|
||||||
|
|
||||||
|
@ -50,6 +60,27 @@ export default {
|
||||||
tocElt.addEventListener('mousemove', (e) => {
|
tocElt.addEventListener('mousemove', (e) => {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Change mask postion on scroll
|
||||||
|
const updateMaskY = () => {
|
||||||
|
const scrollPosition = editorSvc.getScrollPosition();
|
||||||
|
const sectionDesc = editorSvc.sectionDescList[scrollPosition.sectionIdx];
|
||||||
|
this.maskY = sectionDesc.tocDimension.startOffset +
|
||||||
|
(scrollPosition.posInSection * sectionDesc.tocDimension.height);
|
||||||
|
};
|
||||||
|
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
editorSvc.editorElt.parentNode.addEventListener('scroll', () => {
|
||||||
|
if (this.styles.showEditor) {
|
||||||
|
updateMaskY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
editorSvc.previewElt.parentNode.addEventListener('scroll', () => {
|
||||||
|
if (!this.styles.showEditor) {
|
||||||
|
updateMaskY();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -99,4 +130,13 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toc__mask {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.05);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -143,6 +143,22 @@ textarea {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-entry__radio,
|
||||||
|
.form-entry__checkbox {
|
||||||
|
margin: 0.25em 1em;
|
||||||
|
|
||||||
|
input {
|
||||||
|
margin-right: 0.25em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-entry__info {
|
||||||
|
font-size: 0.75em;
|
||||||
|
opacity: 0.5;
|
||||||
|
line-height: 1.4;
|
||||||
|
margin: 0.25em 0;
|
||||||
|
}
|
||||||
|
|
||||||
.textfield {
|
.textfield {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -168,42 +184,30 @@ textarea {
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex {
|
.flex {
|
||||||
display: -webkit-box;
|
|
||||||
display: -webkit-flex;
|
|
||||||
display: -moz-box;
|
|
||||||
display: -ms-flexbox;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--row {
|
.flex--row {
|
||||||
-webkit-box-orient: horizontal;
|
|
||||||
-webkit-flex-direction: row;
|
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--column {
|
.flex--column {
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
-webkit-flex-direction: column;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--center {
|
.flex--center {
|
||||||
-webkit-justify-content: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--end {
|
.flex--end {
|
||||||
-webkit-justify-content: flex-end;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--space-between {
|
.flex--space-between {
|
||||||
-webkit-justify-content: space-between;
|
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex--align-center {
|
.flex--align-center {
|
||||||
-webkit-align-items: center;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,7 +216,6 @@ textarea {
|
||||||
line-height: 36px;
|
line-height: 36px;
|
||||||
padding: 4px 4px 0;
|
padding: 4px 4px 0;
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
-webkit-flex: none;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -223,7 +226,6 @@ textarea {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
-webkit-flex: none;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
|
|
||||||
/* prevent from seeing wrapped buttons */
|
/* prevent from seeing wrapped buttons */
|
||||||
|
@ -261,7 +263,8 @@ textarea {
|
||||||
width: 50%;
|
width: 50%;
|
||||||
float: left;
|
float: left;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
line-height: 2.5em;
|
line-height: 1.4;
|
||||||
|
padding: 0.67em 0.33em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
border-top-left-radius: $border-radius-base;
|
border-top-left-radius: $border-radius-base;
|
||||||
|
|
|
@ -6,6 +6,7 @@ $font-size-monospace: 0.85em;
|
||||||
$code-bg: rgba(0, 0, 0, 0.05);
|
$code-bg: rgba(0, 0, 0, 0.05);
|
||||||
$code-border-radius: 2px;
|
$code-border-radius: 2px;
|
||||||
$link-color: #0c93e4;
|
$link-color: #0c93e4;
|
||||||
|
$error-color: #f20;
|
||||||
$border-radius-base: 2px;
|
$border-radius-base: 2px;
|
||||||
$hr-color: rgba(128, 128, 128, 0.2);
|
$hr-color: rgba(128, 128, 128, 0.2);
|
||||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
$navbar-color: rgba(255, 255, 255, 0.67);
|
||||||
|
|
39
src/components/menus/ExportMenu.vue
Normal file
39
src/components/menus/ExportMenu.vue
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<menu-entry @click.native="exportMarkdown">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
Export as Markdown
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportHtml">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
Export as HTML
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="exportPdf">
|
||||||
|
<icon-download slot="icon"></icon-download>
|
||||||
|
Export as PDF
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MenuEntry from './MenuEntry';
|
||||||
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
exportMarkdown() {
|
||||||
|
const currentFile = this.$store.getters['file/current'];
|
||||||
|
return exportSvc.exportToDisk(currentFile.id, 'md');
|
||||||
|
},
|
||||||
|
exportHtml() {
|
||||||
|
return this.$store.dispatch('modal/open', 'htmlExport');
|
||||||
|
},
|
||||||
|
exportPdf() {
|
||||||
|
return this.$store.dispatch('modal/notImplemented');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
81
src/components/menus/MainMenu.vue
Normal file
81
src/components/menus/MainMenu.vue
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<menu-entry v-if="!loginToken" @click.native="signin">
|
||||||
|
<icon-login slot="icon"></icon-login>
|
||||||
|
<div>Sign in with Google</div>
|
||||||
|
<span>Back up and sync all your files, folders and settings.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="setPanel('sync')">
|
||||||
|
<icon-sync slot="icon"></icon-sync>
|
||||||
|
<div>Synchronize</div>
|
||||||
|
<span>Open, save, collaborate in the Cloud.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="setPanel('publish')">
|
||||||
|
<icon-upload slot="icon"></icon-upload>
|
||||||
|
<div>Publish</div>
|
||||||
|
<span>Export to the web.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="fileProperties">
|
||||||
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
|
<div>File properties</div>
|
||||||
|
<span>Add metadata and configure extensions.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="setPanel('toc')">
|
||||||
|
<icon-toc slot="icon"></icon-toc>
|
||||||
|
Table of contents
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="setPanel('help')">
|
||||||
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
|
Markdown cheat sheet
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="importFile">
|
||||||
|
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||||
|
Import from disk
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="setPanel('export')">
|
||||||
|
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||||
|
Export to disk
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="setPanel('more')">
|
||||||
|
More...
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
|
import MenuEntry from './MenuEntry';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('data', [
|
||||||
|
'loginToken',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions('data', {
|
||||||
|
setPanel: 'setSideBarPanel',
|
||||||
|
}),
|
||||||
|
signin() {
|
||||||
|
return googleHelper.signin()
|
||||||
|
.then(() => syncSvc.requestSync());
|
||||||
|
},
|
||||||
|
importFile() {
|
||||||
|
return this.$store.dispatch('modal/notImplemented');
|
||||||
|
},
|
||||||
|
fileProperties() {
|
||||||
|
return this.$store.dispatch('modal/open', 'fileProperties')
|
||||||
|
.then(properties => this.$store.dispatch('content/patchCurrent', { properties }));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -28,7 +28,6 @@
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
-webkit-flex: none;
|
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
44
src/components/menus/MoreMenu.vue
Normal file
44
src/components/menus/MoreMenu.vue
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<menu-entry @click.native="settings">
|
||||||
|
<icon-settings slot="icon"></icon-settings>
|
||||||
|
<div>Settings</div>
|
||||||
|
<span>Tweak application and keyboard shortcuts.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="templates">
|
||||||
|
<icon-code-braces slot="icon"></icon-code-braces>
|
||||||
|
<div>Templates</div>
|
||||||
|
<span>Configure Handlebars templates for your exports.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="reset">
|
||||||
|
<icon-logout slot="icon"></icon-logout>
|
||||||
|
<div>Reset application</div>
|
||||||
|
<span>Sign out and clean local data.</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import MenuEntry from './MenuEntry';
|
||||||
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
settings() {
|
||||||
|
return this.$store.dispatch('modal/open', 'settings')
|
||||||
|
.then(settings => this.$store.dispatch('data/setSettings', settings));
|
||||||
|
},
|
||||||
|
templates() {
|
||||||
|
return this.$store.dispatch('modal/open', 'templates')
|
||||||
|
.then(({ templates }) => this.$store.dispatch('data/setTemplates', templates));
|
||||||
|
},
|
||||||
|
reset() {
|
||||||
|
return this.$store.dispatch('modal/reset')
|
||||||
|
.then(() => localDbSvc.removeDb());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
165
src/components/menus/PublishMenu.vue
Normal file
165
src/components/menus/PublishMenu.vue
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<div class="side-bar__warning" v-if="publishLocations.length">
|
||||||
|
<p><b>{{currentFileName}}</b> is already published.</p>
|
||||||
|
<menu-entry v-if="!offline" @click.native="requestPublish">
|
||||||
|
<icon-upload slot="icon"></icon-upload>
|
||||||
|
<div>Publish now</div>
|
||||||
|
<span>Upload current file to its publication locations.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="managePublish">
|
||||||
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
|
<div>File publication</div>
|
||||||
|
<span>Manage current file publication locations.</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Publish to Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Publish to Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in githubTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Publish to GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="publishGist(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||||
|
<div>Publish to Gist</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in bloggerTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="publishBlogger(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
|
<div>Publish to Blogger</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="publishBloggerPage(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="bloggerPage"></icon-provider>
|
||||||
|
<div>Publish to Blogger Page</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="addGoogleDriveAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<span>Add Google Drive account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addDropboxAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<span>Add Dropbox account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGithubAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<span>Add GitHub account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addBloggerAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
|
<span>Add Blogger account</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
import MenuEntry from './MenuEntry';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||||
|
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||||
|
import publishSvc from '../../services/publishSvc';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
||||||
|
.map(sub => tokens[sub])
|
||||||
|
.filter(token => filter(token))
|
||||||
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
|
|
||||||
|
const openPublishModal = (token, type) => store.dispatch('modal/open', {
|
||||||
|
type,
|
||||||
|
token,
|
||||||
|
}).then(publishLocation => publishSvc.createPublishLocation(publishLocation));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'offline',
|
||||||
|
]),
|
||||||
|
...mapState('queue', [
|
||||||
|
'isPublishRequested',
|
||||||
|
]),
|
||||||
|
...mapGetters('publishLocation', {
|
||||||
|
publishLocations: 'current',
|
||||||
|
}),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
googleDriveTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
|
||||||
|
},
|
||||||
|
dropboxTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/dropboxTokens']);
|
||||||
|
},
|
||||||
|
githubTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/githubTokens']);
|
||||||
|
},
|
||||||
|
bloggerTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isBlogger);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
requestPublish() {
|
||||||
|
if (!this.isPublishRequested) {
|
||||||
|
publishSvc.requestPublish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
managePublish() {
|
||||||
|
return this.$store.dispatch('modal/open', 'publishManagement');
|
||||||
|
},
|
||||||
|
addGoogleDriveAccount() {
|
||||||
|
return googleHelper.addDriveAccount();
|
||||||
|
},
|
||||||
|
addDropboxAccount() {
|
||||||
|
return dropboxHelper.addAccount();
|
||||||
|
},
|
||||||
|
addGithubAccount() {
|
||||||
|
return githubHelper.addAccount();
|
||||||
|
},
|
||||||
|
addBloggerAccount() {
|
||||||
|
return googleHelper.addBloggerAccount();
|
||||||
|
},
|
||||||
|
publishGoogleDrive(token) {
|
||||||
|
return openPublishModal(token, 'googleDrivePublish');
|
||||||
|
},
|
||||||
|
publishDropbox(token) {
|
||||||
|
return openPublishModal(token, 'dropboxPublish');
|
||||||
|
},
|
||||||
|
publishGithub(token) {
|
||||||
|
return openPublishModal(token, 'githubPublish');
|
||||||
|
},
|
||||||
|
publishGist(token) {
|
||||||
|
return openPublishModal(token, 'gistPublish');
|
||||||
|
},
|
||||||
|
publishBlogger(token) {
|
||||||
|
return openPublishModal(token, 'bloggerPublish');
|
||||||
|
},
|
||||||
|
publishBloggerPage(token) {
|
||||||
|
return openPublishModal(token, 'bloggerPagePublish');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
179
src/components/menus/SyncMenu.vue
Normal file
179
src/components/menus/SyncMenu.vue
Normal file
|
@ -0,0 +1,179 @@
|
||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<div class="side-bar__warning" v-if="syncLocations.length">
|
||||||
|
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
||||||
|
<menu-entry v-if="!offline && isSyncPossible" @click.native="requestSync">
|
||||||
|
<icon-sync slot="icon"></icon-sync>
|
||||||
|
<div>Synchronize now</div>
|
||||||
|
<span>Download, merge and upload file changes.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="manageSync">
|
||||||
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
|
<div>File synchronization</div>
|
||||||
|
<span>Manage current file synchronized locations.</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!offline && isSyncPossible">
|
||||||
|
<menu-entry @click.native="requestSync">
|
||||||
|
<icon-sync slot="icon"></icon-sync>
|
||||||
|
<div>Synchronize now</div>
|
||||||
|
<span>Download, merge and upload file changes.</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<div v-for="token in googleDriveTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Open from Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveGoogleDrive(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div>Save on Google Drive</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in dropboxTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="openDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Open from Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveDropbox(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<div>Save on Dropbox</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<div v-for="token in githubTokens" :key="token.sub">
|
||||||
|
<menu-entry @click.native="saveGithub(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<div>Save on GitHub</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="saveGist(token)">
|
||||||
|
<icon-provider slot="icon" provider-id="gist"></icon-provider>
|
||||||
|
<div>Save on Gist</div>
|
||||||
|
<span>{{token.name}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="addGoogleDriveAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<span>Add Google Drive account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addDropboxAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<span>Add Dropbox account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGithubAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<span>Add GitHub account</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex';
|
||||||
|
import MenuEntry from './MenuEntry';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||||
|
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||||
|
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||||
|
import dropboxProvider from '../../services/providers/dropboxProvider';
|
||||||
|
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
|
||||||
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const tokensToArray = (tokens, filter = () => true) => Object.keys(tokens)
|
||||||
|
.map(sub => tokens[sub])
|
||||||
|
.filter(token => filter(token))
|
||||||
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
|
|
||||||
|
const openSyncModal = (token, type) => store.dispatch('modal/open', {
|
||||||
|
type,
|
||||||
|
token,
|
||||||
|
}).then(syncLocation => syncSvc.createSyncLocation(syncLocation));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState([
|
||||||
|
'offline',
|
||||||
|
]),
|
||||||
|
...mapState('queue', [
|
||||||
|
'isSyncRequested',
|
||||||
|
]),
|
||||||
|
...mapGetters('data', [
|
||||||
|
'loginToken',
|
||||||
|
]),
|
||||||
|
...mapGetters('syncLocation', {
|
||||||
|
syncLocations: 'current',
|
||||||
|
}),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
isSyncPossible() {
|
||||||
|
return this.$store.getters['data/loginToken'] ||
|
||||||
|
this.$store.getters['syncLocation/current'].length;
|
||||||
|
},
|
||||||
|
googleDriveTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/googleTokens'], token => token.isDrive);
|
||||||
|
},
|
||||||
|
dropboxTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/dropboxTokens']);
|
||||||
|
},
|
||||||
|
githubTokens() {
|
||||||
|
return tokensToArray(this.$store.getters['data/githubTokens']);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
requestSync() {
|
||||||
|
if (!this.isSyncRequested) {
|
||||||
|
syncSvc.requestSync();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
manageSync() {
|
||||||
|
return this.$store.dispatch('modal/open', 'syncManagement');
|
||||||
|
},
|
||||||
|
addGoogleDriveAccount() {
|
||||||
|
return googleHelper.addDriveAccount();
|
||||||
|
},
|
||||||
|
addDropboxAccount() {
|
||||||
|
return dropboxHelper.addAccount();
|
||||||
|
},
|
||||||
|
addGithubAccount() {
|
||||||
|
return githubHelper.addAccount();
|
||||||
|
},
|
||||||
|
openGoogleDrive(token) {
|
||||||
|
return googleHelper.openPicker(token, 'doc')
|
||||||
|
.then(files => this.$store.dispatch('queue/enqueue',
|
||||||
|
() => googleDriveProvider.openFiles(token, files)));
|
||||||
|
},
|
||||||
|
openDropbox(token) {
|
||||||
|
return dropboxHelper.openChooser(token)
|
||||||
|
.then(paths => this.$store.dispatch('queue/enqueue',
|
||||||
|
() => {
|
||||||
|
if (token.fullAccess) {
|
||||||
|
return dropboxProvider.openFiles(token, paths);
|
||||||
|
}
|
||||||
|
return dropboxRestrictedProvider.openFiles(token, paths);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
saveGoogleDrive(token) {
|
||||||
|
return openSyncModal(token, 'googleDriveSync');
|
||||||
|
},
|
||||||
|
saveDropbox(token) {
|
||||||
|
return openSyncModal(token, 'dropboxSync');
|
||||||
|
},
|
||||||
|
saveGithub(token) {
|
||||||
|
return openSyncModal(token, 'githubSync');
|
||||||
|
},
|
||||||
|
saveGist(token) {
|
||||||
|
return openSyncModal(token, 'gistSync');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
104
src/components/modals/BloggerPagePublishModal.vue
Normal file
104
src/components/modals/BloggerPagePublishModal.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="bloggerPage"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger Page</b>.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="blogUrl">Blog URL</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="blogUrl" type="text" class="textfield" v-model="blogUrl" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> http://example.blogger.com/
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="fileId">Existing page ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="fileId" type="text" class="textfield" v-model="pageId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__tip">
|
||||||
|
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import bloggerPageProvider from '../../services/providers/bloggerPageProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
pageId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
blogUrl: computedLocalSetting('bloggerBlogUrl'),
|
||||||
|
selectedTemplate: computedLocalSetting('bloggerPublishTemplate'),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
bloggerPublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
if (this.blogUrl) {
|
||||||
|
// Return new location
|
||||||
|
const location = bloggerPageProvider.makeLocation(
|
||||||
|
this.config.token, this.blogUrl, this.pageId);
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
105
src/components/modals/BloggerPublishModal.vue
Normal file
105
src/components/modals/BloggerPublishModal.vue
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="blogger"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Blogger</b> site.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="blogUrl">Blog URL</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="blogUrl" type="text" class="textfield" v-model="blogUrl" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> http://example.blogger.com/
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="fileId">Existing post ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="fileId" type="text" class="textfield" v-model="postId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__tip">
|
||||||
|
<b>Tip:</b> You can provide values for <code>title</code>, <code>tags</code>,
|
||||||
|
<code>status</code> and <code>date</code> in the <b>file properties</b>.
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import bloggerProvider from '../../services/providers/bloggerProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
postId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
blogUrl: computedLocalSetting('bloggerBlogUrl'),
|
||||||
|
selectedTemplate: computedLocalSetting('bloggerPublishTemplate'),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
bloggerPublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
if (this.blogUrl) {
|
||||||
|
// Return new location
|
||||||
|
const location = bloggerProvider.makeLocation(
|
||||||
|
this.config.token, this.blogUrl, this.postId);
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
100
src/components/modals/DropboxPublishModal.vue
Normal file
100
src/components/modals/DropboxPublishModal.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Dropbox</b>.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="path">File path</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> /path/to/My Document.html<br>
|
||||||
|
If the file exists, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import dropboxProvider from '../../services/providers/dropboxProvider';
|
||||||
|
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
selectedTemplate: computedLocalSetting('dropboxPublishTemplate'),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.path = `/${this.currentFileName}.html`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
dropboxPublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
if (dropboxProvider.checkPath(this.path)) {
|
||||||
|
// Return new location
|
||||||
|
const location = this.config.token.fullAccess
|
||||||
|
? dropboxProvider.makeLocation(this.config.token, this.path)
|
||||||
|
: dropboxRestrictedProvider.makeLocation(this.config.token, this.path);
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
58
src/components/modals/DropboxSyncModal.vue
Normal file
58
src/components/modals/DropboxSyncModal.vue
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="dropbox"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will save <b>{{currentFileName}}</b> to your <b>Dropbox</b> and keep it synchronized.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="path">File path</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> /path/to/My Document.md<br>
|
||||||
|
If the file exists, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import dropboxProvider from '../../services/providers/dropboxProvider';
|
||||||
|
import dropboxRestrictedProvider from '../../services/providers/dropboxRestrictedProvider';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.path = `/${this.currentFileName}.md`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (dropboxProvider.checkPath(this.path)) {
|
||||||
|
// Return new location
|
||||||
|
const location = this.config.token.fullAccess
|
||||||
|
? dropboxProvider.makeLocation(this.config.token, this.path)
|
||||||
|
: dropboxRestrictedProvider.makeLocation(this.config.token, this.path);
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--file-properties">
|
<div class="modal__inner-1 modal__inner-1--file-properties">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="tabs">
|
<div class="tabs flex flex--row">
|
||||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||||
Current file properties
|
Current file properties
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||||
Default properties
|
Default properties
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,9 +27,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export default {
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
strippedCustomProperties() {
|
strippedCustomProperties() {
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import 'common/variables.scss';
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
.modal__inner-1--file-properties {
|
.modal__inner-1--file-properties {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
115
src/components/modals/GistPublishModal.vue
Normal file
115
src/components/modals/GistPublishModal.vue
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to a <b>Gist</b>.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="filename">Filename</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="filename" type="text" class="textfield" v-model.trim="filename" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<div class="form-entry__checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="isPublic"> Public
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="gistId">Gist ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="gistId" type="text" class="textfield" v-model.trim="gistId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If the file exists in the provided Gist, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__tip">
|
||||||
|
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import gistProvider from '../../services/providers/gistProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
filename: '',
|
||||||
|
gistId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
isPublic: computedLocalSetting('gistIsPublic'),
|
||||||
|
selectedTemplate: computedLocalSetting('gistPublishTemplate'),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.filename = `${this.currentFileName}.md`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
gistPublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
if (this.filename) {
|
||||||
|
// Return new location
|
||||||
|
const location = gistProvider.makeLocation(
|
||||||
|
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
82
src/components/modals/GistSyncModal.vue
Normal file
82
src/components/modals/GistSyncModal.vue
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="gist"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will save <b>{{currentFileName}}</b> to a <b>Gist</b> repository and keep it synchronized.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="filename">Filename</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="filename" type="text" class="textfield" v-model.trim="filename" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<div class="form-entry__checkbox">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" v-model="isPublic"> Public
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="gistId">Gist ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="gistId" type="text" class="textfield" v-model.trim="gistId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If the file exists in the provided Gist, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import gistProvider from '../../services/providers/gistProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
filename: '',
|
||||||
|
gistId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
isPublic: computedLocalSetting('gistIsPublic'),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.filename = `${this.currentFileName}.md`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (this.filename) {
|
||||||
|
// Return new location
|
||||||
|
const location = gistProvider.makeLocation(
|
||||||
|
this.config.token, this.filename, this.isPublic, this.gistId);
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
121
src/components/modals/GithubPublishModal.vue
Normal file
121
src/components/modals/GithubPublishModal.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>GitHub</b> repository.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="repo">Repository URL</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="repo" type="text" class="textfield" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> https://github.com/benweet/stackedit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="branch">Branch (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="branch" type="text" class="textfield" v-model.trim="branch" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If not provided, the master branch will be used.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="path">File path</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> docs/README.md<br>
|
||||||
|
If the file exists, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import githubProvider from '../../services/providers/githubProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
branch: '',
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
repoUrl: computedLocalSetting('githubRepoUrl'),
|
||||||
|
selectedTemplate: computedLocalSetting('githubPublishTemplate'),
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.path = `${this.currentFileName}.md`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
githubPublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
if (this.repoUrl && this.path) {
|
||||||
|
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
||||||
|
if (parsedRepo) {
|
||||||
|
// Return new location
|
||||||
|
const location = githubProvider.makeLocation(
|
||||||
|
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
88
src/components/modals/GithubSyncModal.vue
Normal file
88
src/components/modals/GithubSyncModal.vue
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="github"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will save <b>{{currentFileName}}</b> to your <b>GitHub</b> repository and keep it synchronized.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="repo">Repository URL</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="repo" type="text" class="textfield" v-model.trim="repoUrl" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> https://github.com/benweet/stackedit
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="branch">Branch (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="branch" type="text" class="textfield" v-model.trim="branch" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If not provided, the master branch will be used.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="path">File path</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="path" type="text" class="textfield" v-model.trim="path" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
<b>Example:</b> docs/README.md<br>
|
||||||
|
If the file exists, it will be replaced.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import githubProvider from '../../services/providers/githubProvider';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
branch: '',
|
||||||
|
path: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
repoUrl: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters['data/localSettings'].githubRepoUrl;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
githubRepoUrl: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.path = `${this.currentFileName}.md`;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resolve() {
|
||||||
|
if (this.repoUrl && this.path) {
|
||||||
|
const parsedRepo = this.repoUrl.match(/[/:]?([^/:]+)\/([^/]+?)(?:\.git)?$/);
|
||||||
|
if (parsedRepo) {
|
||||||
|
// Return new location
|
||||||
|
const location = githubProvider.makeLocation(
|
||||||
|
this.config.token, parsedRepo[1], parsedRepo[2], this.branch || 'master', this.path);
|
||||||
|
this.config.resolve(location);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
121
src/components/modals/GoogleDrivePublishModal.vue
Normal file
121
src/components/modals/GoogleDrivePublishModal.vue
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will publish <b>{{currentFileName}}</b> to your <b>Google Drive</b> account.</p>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="fileId">File ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="fileId" type="text" class="textfield" v-model="fileId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If no file ID is supplied, a new file will be created in your Google Drive root folder.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<div class="form-entry__radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" v-model="format" value="markdown"> Export Markdown
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" v-model="format" value="html"> Export HTML
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<select class="textfield" id="template" v-model="selectedTemplate" :disabled="format === 'markdown'" @keyup.enter="resolve()">
|
||||||
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
|
{{ template.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="configureTemplates">Configure templates</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__tip">
|
||||||
|
<b>Tip:</b> You can provide a value for <code>title</code> in the <b>file properties</b>.
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||||
|
import store from '../../store';
|
||||||
|
|
||||||
|
const computedLocalSetting = id => ({
|
||||||
|
get() {
|
||||||
|
return store.getters['data/localSettings'][id];
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
[id]: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
fileId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
...mapGetters('data', [
|
||||||
|
'allTemplates',
|
||||||
|
]),
|
||||||
|
selectedTemplate: computedLocalSetting('googleDrivePublishTemplate'),
|
||||||
|
format: computedLocalSetting('googleDrivePublishFormat'),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
configureTemplates() {
|
||||||
|
this.$store.dispatch('modal/open', {
|
||||||
|
type: 'templates',
|
||||||
|
selectedId: this.selectedTemplate,
|
||||||
|
})
|
||||||
|
.then(({ templates, selectedId }) => {
|
||||||
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
googleDrivePublishTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
openFolder() {
|
||||||
|
return this.$store.dispatch(
|
||||||
|
'modal/hideUntil',
|
||||||
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
|
.then((folders) => {
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
googleDriveFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
// Return new location
|
||||||
|
const location = googleDriveProvider.makeLocation(
|
||||||
|
this.config.token, this.fileId);
|
||||||
|
if (this.format) {
|
||||||
|
location.templateId = this.selectedTemplate;
|
||||||
|
}
|
||||||
|
this.config.resolve(location);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
90
src/components/modals/GoogleDriveSyncModal.vue
Normal file
90
src/components/modals/GoogleDriveSyncModal.vue
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--google-drive-sync">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will save <b>{{currentFileName}}</b> to your <b>Google Drive</b> account and keep it synchronized.</p>
|
||||||
|
<a href="javascript:void(0)" v-if="!showOptions" @click="showOptions = true">See options ▾</a>
|
||||||
|
<div v-else>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="folderId">Folder ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="folderId" type="text" class="textfield" v-model.trim="folderId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If no folder ID is supplied, the file will be created in your root folder.
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="openFolder">Choose folder</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-entry">
|
||||||
|
<label class="form-entry__label" for="fileId">File ID (optional)</label>
|
||||||
|
<div class="form-entry__field">
|
||||||
|
<input id="fileId" type="text" class="textfield" v-model="fileId" @keyup.enter="resolve()">
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__info">
|
||||||
|
This will overwrite the existing file on the server.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import googleDriveProvider from '../../services/providers/googleDriveProvider';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
showOptions: false,
|
||||||
|
fileId: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
folderId: {
|
||||||
|
get() {
|
||||||
|
return this.$store.getters['data/localSettings'].googleDriveFolderId;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
googleDriveFolderId: value,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.showOptions = this.folderId || this.fileId;
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openFolder() {
|
||||||
|
return this.$store.dispatch(
|
||||||
|
'modal/hideUntil',
|
||||||
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
|
.then((folders) => {
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
googleDriveFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
// Return new location
|
||||||
|
const location = googleDriveProvider.makeLocation(
|
||||||
|
this.config.token, this.fileId, this.folderId);
|
||||||
|
this.config.resolve(location);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -23,7 +23,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ export default {
|
||||||
thumbnailUrl() {
|
thumbnailUrl() {
|
||||||
return `url(${makeThumbnail(this.config.url, 320)})`;
|
return `url(${makeThumbnail(this.config.url, 320)})`;
|
||||||
},
|
},
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<label class="form-entry__label" for="template">Template</label>
|
<label class="form-entry__label" for="template">Template</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
<select id="template" v-model="selectedTemplate" class="textfield">
|
<select class="textfield" id="template" v-model="selectedTemplate" @keyup.enter="resolve()">
|
||||||
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
<option v-for="(template, id) in allTemplates" :key="id" v-bind:value="id">
|
||||||
{{ template.name }}
|
{{ template.name }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -24,16 +24,16 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState, mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Clipboard from 'clipboard';
|
import Clipboard from 'clipboard';
|
||||||
import exportSvc from '../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
result: '',
|
result: '',
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
...mapGetters('data', [
|
...mapGetters('data', [
|
||||||
|
@ -41,11 +41,11 @@ export default {
|
||||||
]),
|
]),
|
||||||
selectedTemplate: {
|
selectedTemplate: {
|
||||||
get() {
|
get() {
|
||||||
return this.$store.getters['data/localSettings'].htmlExportLastTemplate;
|
return this.$store.getters['data/localSettings'].htmlExportTemplate;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$store.dispatch('data/patchLocalSettings', {
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
htmlExportLastTemplate: value,
|
htmlExportTemplate: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -54,8 +54,8 @@ export default {
|
||||||
this.$watch('selectedTemplate', (selectedTemplate) => {
|
this.$watch('selectedTemplate', (selectedTemplate) => {
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||||
.then((res) => {
|
.then((html) => {
|
||||||
this.result = res;
|
this.result = html;
|
||||||
});
|
});
|
||||||
}, {
|
}, {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
|
@ -69,13 +69,16 @@ export default {
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
configureTemplates() {
|
configureTemplates() {
|
||||||
const reopen = () => this.$store.dispatch('modal/open', 'htmlExport');
|
|
||||||
this.$store.dispatch('modal/open', {
|
this.$store.dispatch('modal/open', {
|
||||||
type: 'templates',
|
type: 'templates',
|
||||||
selectedKey: this.selectedTemplate,
|
selectedId: this.selectedTemplate,
|
||||||
})
|
})
|
||||||
.then(templates => this.$store.dispatch('data/setTemplates', templates))
|
.then(({ templates, selectedId }) => {
|
||||||
.then(reopen, reopen);
|
this.$store.dispatch('data/setTemplates', templates);
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
htmlExportTemplate: selectedId,
|
||||||
|
});
|
||||||
|
});
|
||||||
},
|
},
|
||||||
resolve() {
|
resolve() {
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--image">
|
<div class="modal__inner-1 modal__inner-1--image">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
|
<p>Please provide a <b>URL</b> for your image.
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<label class="form-entry__label" for="url">URL</label>
|
<label class="form-entry__label" for="url">URL</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
|
@ -8,12 +9,12 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
<menu-entry @click.native="openGooglePhotos(token)" v-for="token in googlePhotosTokens" :key="token.sub">
|
||||||
<icon-google-photos slot="icon"></icon-google-photos>
|
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||||
<div>Open from Google Photos</div>
|
<div>Open from Google Photos</div>
|
||||||
<span>{{token.name}}</span>
|
<span>{{token.name}}</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGooglePhotosAccount">
|
<menu-entry @click.native="addGooglePhotosAccount">
|
||||||
<icon-google-photos slot="icon"></icon-google-photos>
|
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||||
<span>Add Google Photos account</span>
|
<span>Add Google Photos account</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
|
@ -25,9 +26,9 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from '../menus/MenuEntry';
|
||||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -37,7 +38,7 @@ export default {
|
||||||
url: '',
|
url: '',
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
googlePhotosTokens() {
|
googlePhotosTokens() {
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--link" @keyup.enter="resolve()">
|
<div class="modal__inner-1 modal__inner-1--link" @keyup.enter="resolve()">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
|
<p>Please provide a <b>URL</b> for your link.
|
||||||
<div class="form-entry">
|
<div class="form-entry">
|
||||||
<label class="form-entry__label" for="url">URL</label>
|
<label class="form-entry__label" for="url">URL</label>
|
||||||
<div class="form-entry__field">
|
<div class="form-entry__field">
|
||||||
|
@ -16,13 +17,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
url: '',
|
url: '',
|
||||||
}),
|
}),
|
||||||
computed: mapState('modal', [
|
computed: mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
methods: {
|
methods: {
|
104
src/components/modals/PublishManagementModal.vue
Normal file
104
src/components/modals/PublishManagementModal.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--publish-management">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||||
|
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||||
|
<div>
|
||||||
|
<div v-for="location in publishLocations" :key="location.id" class="publish-entry flex flex--row flex--align-center">
|
||||||
|
<div class="publish-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<div class="publish-entry__description">
|
||||||
|
{{location.description}}
|
||||||
|
</div>
|
||||||
|
<div class="publish-entry__buttons flex flex--row flex--center">
|
||||||
|
<a class="publish-entry__button button" :href="location.url" target="_blank">
|
||||||
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
<button class="publish-entry__button button" @click="remove(location)">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
...mapGetters('publishLocation', {
|
||||||
|
publishLocations: 'current',
|
||||||
|
}),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
remove(location) {
|
||||||
|
this.$store.commit('publishLocation/deleteItem', location.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.modal__inner-1--publish-management {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-entry {
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
border-bottom: 1px solid $hr-color;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-entry__icon {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-entry__description {
|
||||||
|
opacity: 0.5;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.9em;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-entry__buttons {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publish-entry__button {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,11 +1,11 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="modal__inner-1 modal__inner-1--settings">
|
<div class="modal__inner-1 modal__inner-1--settings">
|
||||||
<div class="modal__inner-2">
|
<div class="modal__inner-2">
|
||||||
<div class="tabs">
|
<div class="tabs flex flex--row">
|
||||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||||
Custom settings
|
Custom settings
|
||||||
</div>
|
</div>
|
||||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'default'}" @click="tab = 'default'">
|
||||||
Default settings
|
Default settings
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -27,9 +27,9 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultSettings from '../data/defaultSettings.yml';
|
import defaultSettings from '../../data/defaultSettings.yml';
|
||||||
|
|
||||||
const emptySettings = '# Add your custom settings here to override the default settings.\n';
|
const emptySettings = '# Add your custom settings here to override the default settings.\n';
|
||||||
|
|
||||||
|
@ -44,7 +44,7 @@ export default {
|
||||||
error: null,
|
error: null,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
strippedCustomSettings() {
|
strippedCustomSettings() {
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import 'common/variables.scss';
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
.modal__inner-1--settings {
|
.modal__inner-1--settings {
|
||||||
max-width: 600px;
|
max-width: 600px;
|
104
src/components/modals/SyncManagementModal.vue
Normal file
104
src/components/modals/SyncManagementModal.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--sync-management">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||||
|
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||||
|
<div>
|
||||||
|
<div v-for="location in syncLocations" :key="location.id" class="sync-entry flex flex--row flex--align-center">
|
||||||
|
<div class="sync-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<div class="sync-entry__description">
|
||||||
|
{{location.description}}
|
||||||
|
</div>
|
||||||
|
<div class="sync-entry__buttons flex flex--row flex--center">
|
||||||
|
<a class="sync-entry__button button" :href="location.url" target="_blank">
|
||||||
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
<button class="sync-entry__button button" @click="remove(location)">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="config.resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
...mapGetters('syncLocation', {
|
||||||
|
syncLocations: 'current',
|
||||||
|
}),
|
||||||
|
currentFileName() {
|
||||||
|
return this.$store.getters['file/current'].name;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
remove(location) {
|
||||||
|
this.$store.commit('syncLocation/deleteItem', location.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.modal__inner-1--sync-management {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry {
|
||||||
|
padding: 0.5rem 0.25rem;
|
||||||
|
border-bottom: 1px solid $hr-color;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__icon {
|
||||||
|
height: 30px;
|
||||||
|
width: 30px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__description {
|
||||||
|
opacity: 0.5;
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.9em;
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__buttons {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sync-entry__button {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -51,11 +51,13 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapState } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import utils from '../services/utils';
|
import utils from '../../services/utils';
|
||||||
import CodeEditor from './CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import emptyTemplateValue from '../data/emptyTemplateValue.html';
|
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
||||||
import emptyTemplateHelpers from '!raw-loader!../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||||
|
|
||||||
|
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||||
|
|
||||||
function fillEmptyFields(template) {
|
function fillEmptyFields(template) {
|
||||||
if (template.value === '\n') {
|
if (template.value === '\n') {
|
||||||
|
@ -78,7 +80,7 @@ export default {
|
||||||
editingName: '',
|
editingName: '',
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState('modal', [
|
...mapGetters('modal', [
|
||||||
'config',
|
'config',
|
||||||
]),
|
]),
|
||||||
isReadOnly() {
|
isReadOnly() {
|
||||||
|
@ -89,13 +91,17 @@ export default {
|
||||||
this.$watch(
|
this.$watch(
|
||||||
() => this.$store.getters['data/allTemplates'],
|
() => this.$store.getters['data/allTemplates'],
|
||||||
(allTemplates) => {
|
(allTemplates) => {
|
||||||
const templates = utils.sortObject(
|
const templates = {};
|
||||||
utils.deepCopy(allTemplates),
|
// Sort templates by name
|
||||||
(key, template) => template.name,
|
Object.keys(allTemplates)
|
||||||
);
|
.sort((id1, id2) => collator.compare(allTemplates[id1].name, allTemplates[id2].name))
|
||||||
Object.keys(templates).forEach(id => fillEmptyFields(templates[id]));
|
.forEach((id) => {
|
||||||
|
const template = utils.deepCopy(allTemplates[id]);
|
||||||
|
fillEmptyFields(template);
|
||||||
|
templates[id] = template;
|
||||||
|
});
|
||||||
this.templates = templates;
|
this.templates = templates;
|
||||||
this.selectedId = this.$store.state.modal.config.selectedId;
|
this.selectedId = this.config.selectedId;
|
||||||
if (!templates[this.selectedId]) {
|
if (!templates[this.selectedId]) {
|
||||||
this.selectedId = Object.keys(templates)[0];
|
this.selectedId = Object.keys(templates)[0];
|
||||||
}
|
}
|
||||||
|
@ -143,7 +149,10 @@ export default {
|
||||||
}, 1);
|
}, 1);
|
||||||
},
|
},
|
||||||
resolve() {
|
resolve() {
|
||||||
this.config.resolve(this.templates);
|
this.config.resolve({
|
||||||
|
templates: this.templates,
|
||||||
|
selectedId: this.selectedId,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -151,6 +160,6 @@ export default {
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.modal__inner-1--templates {
|
.modal__inner-1--templates {
|
||||||
max-width: 720px;
|
max-width: 680px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,7 +1,12 @@
|
||||||
### File properties can contain metadata used for your publications (Wordpress, Blogger...).
|
### File properties can contain metadata used for your publications (Wordpress, Blogger...).
|
||||||
|
|
||||||
### For example, you can specify a blog post title:
|
|
||||||
#title: My article
|
#title: My article
|
||||||
|
#author:
|
||||||
|
#tags: Tag 1, Tag 2
|
||||||
|
#categories: Categorie 1, Categorie 2
|
||||||
|
#excerpt:
|
||||||
|
#featuredImage:
|
||||||
|
#status: draft
|
||||||
|
#date: YYYY-MM-DD HH:MM:SS
|
||||||
|
|
||||||
### Extension configuration
|
### Extension configuration
|
||||||
extensions:
|
extensions:
|
||||||
|
|
|
@ -7,5 +7,15 @@ export default () => ({
|
||||||
showExplorer: false,
|
showExplorer: false,
|
||||||
focusMode: false,
|
focusMode: false,
|
||||||
sideBarPanel: 'menu',
|
sideBarPanel: 'menu',
|
||||||
htmlExportLastTemplate: 'styledHtml',
|
htmlExportTemplate: 'styledHtml',
|
||||||
|
googleDriveFolderId: '',
|
||||||
|
googleDrivePublishFormat: 'markdown',
|
||||||
|
googleDrivePublishTemplate: 'styledHtml',
|
||||||
|
bloggerBlogUrl: '',
|
||||||
|
bloggerPublishTemplate: 'styledHtml',
|
||||||
|
dropboxPublishTemplate: 'styledHtml',
|
||||||
|
githubRepoUrl: '',
|
||||||
|
githubPublishTemplate: 'jekyllSite',
|
||||||
|
gistIsPublic: false,
|
||||||
|
gistPublishTemplate: 'plainText',
|
||||||
});
|
});
|
||||||
|
|
|
@ -75,7 +75,7 @@ newFileContent: |
|
||||||
|
|
||||||
# Default properties for newly created files
|
# Default properties for newly created files
|
||||||
newFileProperties: |
|
newFileProperties: |
|
||||||
# extensions:
|
#extensions:
|
||||||
# markdown:
|
# markdown:
|
||||||
# breaks: true
|
# breaks: true
|
||||||
|
|
||||||
|
|
8
src/data/emptyPublishLocation.js
Normal file
8
src/data/emptyPublishLocation.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export default () => ({
|
||||||
|
id: null,
|
||||||
|
type: 'publishLocation',
|
||||||
|
providerId: null,
|
||||||
|
fileId: null,
|
||||||
|
templateId: null,
|
||||||
|
hash: 0,
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
export default () => ({
|
export default () => ({
|
||||||
id: null,
|
id: null,
|
||||||
type: 'syncLocation',
|
type: 'syncLocation',
|
||||||
provider: null,
|
providerId: null,
|
||||||
fileId: null,
|
fileId: null,
|
||||||
hash: 0,
|
hash: 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -8,3 +8,4 @@ Handlebars.registerHelper('transform', function (options) {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,7 @@ The following JavaScript context will be passed to the template:
|
||||||
}
|
}
|
||||||
|
|
||||||
You can use Handlebars built-in helpers and some custom StackEdit helpers:
|
You can use Handlebars built-in helpers and some custom StackEdit helpers:
|
||||||
- {{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a nice TOC.
|
{{#tocToHtml files.0.content.toc}}{{/tocToHtml}} will produce a nice TOC.
|
||||||
- {{#tocToHtml files.0.content.toc 3}}{{/tocToHtml}} will limit the TOC depth to 3.
|
{{#tocToHtml files.0.content.toc 3}}{{/tocToHtml}} will limit the TOC depth to 3.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
|
5
src/data/jekyllSiteTemplate.html
Normal file
5
src/data/jekyllSiteTemplate.html
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
{{{files.0.content.yamlProperties}}}
|
||||||
|
---
|
||||||
|
|
||||||
|
{{{files.0.content.html}}}
|
5
src/icons/Alert.vue
Normal file
5
src/icons/Alert.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
|
<path d="M 13,14L 11,14L 11,9.99998L 13,9.99998M 13,18L 11,18L 11,16L 13,16M 1,21L 23,21L 12,1.99998L 1,21 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/icons/FolderOpen.vue
Normal file
5
src/icons/FolderOpen.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
|
<path d="M 18.9994,19.9981L 3.99939,19.9981C 2.89437,19.9981 1.99939,19.1021 1.99939,17.9981L 2.0094,5.99807C 2.0094,4.89407 2.89437,3.99807 3.99939,3.99807L 9.99939,3.99807L 11.9994,5.99807L 18.9994,5.99808C 20.1034,5.99808 20.9994,6.89407 20.9994,7.99808L 20.9994,8.00001L 3.99939,7.99807L 3.99939,17.9981L 6.14359,10L 23.2141,10L 20.9318,18.5176C 20.7023,19.37 19.9237,19.9981 18.9994,19.9981 Z " />
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -1,9 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 133156 115341">
|
|
||||||
<g>
|
|
||||||
<polygon style="fill:#3777E3" points="22194,115341 44385,76894 133156,76894 110963,115341 "/>
|
|
||||||
<polygon style="fill:#FFCF63" points="88772,76894 133156,76894 88772,0 44385,0 "/>
|
|
||||||
<polygon style="fill:#11A861" points="0,76894 22194,115341 66578,38447 44385,0 "/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
|
@ -1,12 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 512 511">
|
|
||||||
<path d="M255.912,0.08c1.4,0.8 2.6,2 3.7,3.2c41.3,41.5 82.7,83 123.899,124.6c-26,25.6 -51.6,51.6 -77.399,77.3c-9.7,9.8 -19.601,19.4 -29.2,29.4c-7.2,-17.4 -14.1,-34.9 -21,-52.4c0,-18.2 0.1,-36.4 0,-54.7c-0.1,-42.4 -0.2,-84.9 0,-127.4l0,0Z" style="fill:#dc4b3e;fill-rule:nonzero;stroke:#dd4b39;stroke-width:0.09px;"/>
|
|
||||||
<path d="M127.812,127.48l128.1,0c0.1,18.3 0,36.5 0,54.7c-7.1,17.2 -14,34.5 -20.8,51.9c-2.2,-1.2 -3.8,-3 -5.5,-4.8l-101.4,-101.4l-0.4,-0.4Z" style="fill:#ff9e0e;fill-rule:nonzero;stroke:#ef851c;stroke-width:0.09px;"/>
|
|
||||||
<path d="M383.511,127.88l0.4,-0.3c-0.1,42.6 -0.1,85.3 0,127.9l-55.1,0c-17.2,-7.2 -34.601,-13.8 -51.9,-20.9c9.6,-10 19.5,-19.6 29.2,-29.4c25.801,-25.7 51.4,-51.7 77.4,-77.3l0,0Z" style="fill:#af195a;fill-rule:nonzero;stroke:#7e3794;stroke-width:0.09px;"/>
|
|
||||||
<path d="M106.912,148.98c7.2,-6.9 13.9,-14.3 21.3,-21.1l101.4,101.4c1.7,1.8 3.3,3.6 5.5,4.8c-2.3,1.7 -5.2,2.3 -7.8,3.5c-14.801,6 -29.801,11.6 -44.5,18c-18.301,-0.2 -36.601,-0.1 -54.9,-0.1c-42.6,-0.1 -85.2,0.2 -127.8,-0.1c35.5,-35.6 71.2,-71 106.8,-106.4l0,0Z" style="fill:#ffc112;fill-rule:nonzero;stroke:#ffbb1b;stroke-width:0.09px;"/>
|
|
||||||
<path d="M127.912,255.48c18.3,0 36.6,-0.1 54.9,0.1c17.3,7.1 34.6,13.8 51.899,20.8c-28.399,28.8 -57.099,57.2 -85.599,85.9c-7.2,6.8 -13.7,14.3 -21.3,20.7c0,-42.5 -0.1,-85 0.1,-127.5Z" style="fill:#17a05e;fill-rule:nonzero;stroke:#1a8763;stroke-width:0.09px;"/>
|
|
||||||
<path d="M328.812,255.48l55.1,0c42.5,0.1 85.1,-0.1 127.6,0.1c-27.3,27.7 -55,55.1 -82.399,82.6c-15.2,15.1 -30.2,30.399 -45.4,45.3c-34,-34.4 -68.5,-68.4 -102.6,-102.8c-1.4,-1.5 -2.9,-2.8 -4.601,-3.8c2.9,-1.801 6.101,-2.7 9.2,-4c14.4,-5.8 28.799,-11.4 43.1,-17.4l0,0Z" style="fill:#4587f4;fill-rule:nonzero;stroke:#427fed;stroke-width:0.09px;"/>
|
|
||||||
<path d="M234.712,276.38c7.3,17.399 13.9,35 21.2,52.399c-0.1,18.2 0,36.5 -0.1,54.7l0,88c-0.2,13.1 0.3,26.2 -0.2,39.2c-2.101,-1 -3.4,-2.9 -5.101,-4.5c-40.899,-41.099 -81.699,-82.199 -122.699,-123.199c7.6,-6.4 14.1,-13.9 21.3,-20.7c28.5,-28.7 57.2,-57.1 85.6,-85.9Z" style="fill:#8dc44d;fill-rule:nonzero;stroke:#65b045;stroke-width:0.09px;"/>
|
|
||||||
<path d="M276.511,276.88c1.7,1 3.2,2.3 4.601,3.8c34.1,34.4 68.6,68.4 102.6,102.8c-42.7,-0.1 -85.3,0.1 -127.899,0c0.1,-18.2 0,-36.5 0.1,-54.7c6.699,-17.3 13.899,-34.5 20.598,-51.9l0,0Z" style="fill:#3569d6;fill-rule:nonzero;stroke:#43459d;stroke-width:0.09px;"/>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 15.0661,11.2518L 14.1711,12.1697C 13.4471,12.8937 12.9991,13.4977 12.9991,14.9977L 10.9991,14.9977L 10.9991,14.4977C 10.9991,13.3937 11.4471,12.3937 12.1711,11.6697L 13.4141,10.4117C 13.7751,10.0497 13.9991,9.54974 13.9991,8.99774C 13.9991,7.89374 13.1041,6.99774 11.9991,6.99774C 10.8951,6.99774 9.99908,7.89374 9.99908,8.99774L 7.99908,8.99774C 7.99908,6.78876 9.7901,4.99774 11.9991,4.99774C 14.2091,4.99774 15.9991,6.78876 15.9991,8.99774C 15.9991,9.87775 15.6431,10.6747 15.0661,11.2518 Z M 12.9991,18.9977L 10.9991,18.9977L 10.9991,16.9977L 12.9991,16.9977M 11.9991,1.99774C 6.4761,1.99774 1.99908,6.47473 1.99908,11.9977C 1.99908,17.5217 6.4761,21.9977 11.9991,21.9977C 17.5231,21.9977 21.9991,17.5217 21.9991,11.9977C 21.9991,6.47473 17.5231,1.99774 11.9991,1.99774 Z "/>
|
<path d="M 15.0661,11.2518L 14.1711,12.1697C 13.4471,12.8937 12.9991,13.4977 12.9991,14.9977L 10.9991,14.9977L 10.9991,14.4977C 10.9991,13.3937 11.4471,12.3937 12.1711,11.6697L 13.4141,10.4117C 13.7751,10.0497 13.9991,9.54974 13.9991,8.99774C 13.9991,7.89374 13.1041,6.99774 11.9991,6.99774C 10.8951,6.99774 9.99908,7.89374 9.99908,8.99774L 7.99908,8.99774C 7.99908,6.78876 9.7901,4.99774 11.9991,4.99774C 14.2091,4.99774 15.9991,6.78876 15.9991,8.99774C 15.9991,9.87775 15.6431,10.6747 15.0661,11.2518 Z M 12.9991,18.9977L 10.9991,18.9977L 10.9991,16.9977L 12.9991,16.9977M 11.9991,1.99774C 6.4761,1.99774 1.99908,6.47473 1.99908,11.9977C 1.99908,17.5217 6.4761,21.9977 11.9991,21.9977C 17.5231,21.9977 21.9991,17.5217 21.9991,11.9977C 21.9991,6.47473 17.5231,1.99774 11.9991,1.99774 Z "/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
|
5
src/icons/Information.vue
Normal file
5
src/icons/Information.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
|
<path d="M 12.9994,8.99805L 10.9994,8.99805L 10.9994,6.99805L 12.9994,6.99805M 12.9994,16.998L 10.9994,16.998L 10.9994,10.998L 12.9994,10.998M 11.9994,1.99805C 6.47642,1.99805 1.99943,6.47504 1.99943,11.998C 1.99943,17.5211 6.47642,21.998 11.9994,21.998C 17.5224,21.998 21.9994,17.5211 21.9994,11.998C 21.9994,6.47504 17.5224,1.99805 11.9994,1.99805 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/icons/Menu.vue
Normal file
5
src/icons/Menu.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
|
<path fill="#000000" fill-opacity="1" stroke-width="0.2" stroke-linejoin="round" d="M 3,6L 21,6L 21,8L 3,8L 3,6 Z M 3,11L 21,11L 21,13L 3,13L 3,11 Z M 3,16L 21,16L 21,18L 3,18L 3,16 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
5
src/icons/OpenInNew.vue
Normal file
5
src/icons/OpenInNew.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 24.00 24.00">
|
||||||
|
<path d="M 14,3L 14,5L 17.59,5L 7.76,14.83L 9.17,16.24L 19,6.41L 19,10L 21,10L 21,3M 19,19L 5,19L 5,5L 12,5L 12,3L 5,3C 3.89,3 3,3.9 3,5L 3,19C 3,20.1 3.89,21 5,21L 19,21C 20.1,21 21,20.1 21,19L 21,12L 19,12L 19,19 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
47
src/icons/Provider.vue
Normal file
47
src/icons/Provider.vue
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
<template>
|
||||||
|
<div class="icon-provider" :class="['icon-provider--' + providerId]">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ['providerId'],
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.icon-provider {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--stackedit {
|
||||||
|
background-image: url(../assets/iconStackedit.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--googleDrive {
|
||||||
|
background-image: url(../assets/iconGoogleDrive.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--googlePhotos {
|
||||||
|
background-image: url(../assets/iconGooglePhotos.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--github,
|
||||||
|
.icon-provider--gist {
|
||||||
|
background-image: url(../assets/iconGithub.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--dropbox,
|
||||||
|
.icon-provider--dropboxRestricted {
|
||||||
|
background-image: url(../assets/iconDropbox.svg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-provider--blogger,
|
||||||
|
.icon-provider--bloggerPage {
|
||||||
|
background-image: url(../assets/iconBlogger.svg);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,19 +0,0 @@
|
||||||
<template>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="100%" height="100%" viewBox="0 0 126 126">
|
|
||||||
<path d="M103.5,0c3.762,0.003 7.395,0.971 10.725,2.716c0.966,0.506 1.808,1.221 2.712,1.831l-53.937,40.453l-53.937,-40.453c4.402,-3.289 8.02,-4.273 13.437,-4.547l81,0Z" style="fill:#ffe600;fill-rule:nonzero;"/>
|
|
||||||
<path d="M9.063,4.547l53.937,40.453l-58.04,72.55c-3.44,-4.417 -4.681,-8.528 -4.96,-14.05l0,-81c0.064,-5.221 1.801,-10.265 5.138,-14.312c0.914,-1.109 2.033,-2.033 3.05,-3.05l0.875,-0.591Z" style="fill:#bbd500;fill-rule:nonzero;"/>
|
|
||||||
<path d="M63,45l58.04,72.549l-0.178,0.263c-4.901,5.465 -10.068,7.82 -17.362,8.188l-81,0c-5.221,-0.065 -10.265,-1.801 -14.312,-5.138c-1.109,-0.915 -2.033,-2.034 -3.05,-3.05l-0.177,-0.262l58.039,-72.55Z" style="fill:#ff8a00;fill-rule:nonzero;"/>
|
|
||||||
<path d="M116.937,4.547c3.844,2.631 6.684,6.83 8.051,11.262c0.441,1.427 0.673,2.914 0.896,4.391c0.114,0.759 0.077,1.533 0.116,2.3l0,81c-0.023,3.748 -0.939,7.415 -2.716,10.725c-0.632,1.178 -1.496,2.216 -2.245,3.325l-58.039,-72.55l53.937,-40.453Z" style="fill:#75b7fd;fill-rule:nonzero;"/>
|
|
||||||
<path d="M32.063,12l61.874,0c7.767,0 14.063,6.296 14.063,14.063l0,61.874c0,7.767 -6.296,14.063 -14.063,14.063l-61.875,0c-7.766,0 -14.062,-6.296 -14.062,-14.063l0,-61.875c0,-7.766 6.296,-14.062 14.062,-14.062Z" style="fill:#fff;fill-rule:nonzero;"/>
|
|
||||||
<g>
|
|
||||||
<g>
|
|
||||||
<rect x="49.927" y="23.501" width="8.137" height="58.668" style="fill:#737373;"/>
|
|
||||||
<rect x="67.937" y="23.501" width="8.137" height="58.668" style="fill:#737373;"/>
|
|
||||||
</g>
|
|
||||||
<g>
|
|
||||||
<path d="M84.047,46.623l0,-10.239l-42.094,0l0,10.239l42.094,0Z" style="fill:#737373;"/>
|
|
||||||
<path d="M84.047,69.287l0,-10.24l-42.094,0l0,10.24l42.094,0Z" style="fill:#737373;"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</template>
|
|
|
@ -20,7 +20,7 @@ import FileMultiple from './FileMultiple';
|
||||||
import FolderPlus from './FolderPlus';
|
import FolderPlus from './FolderPlus';
|
||||||
import Delete from './Delete';
|
import Delete from './Delete';
|
||||||
import Close from './Close';
|
import Close from './Close';
|
||||||
import FolderMultiple from './FolderMultiple';
|
import FolderOpen from './FolderOpen';
|
||||||
import Pen from './Pen';
|
import Pen from './Pen';
|
||||||
import Target from './Target';
|
import Target from './Target';
|
||||||
import ArrowLeft from './ArrowLeft';
|
import ArrowLeft from './ArrowLeft';
|
||||||
|
@ -36,10 +36,11 @@ import HardDisk from './HardDisk';
|
||||||
import Download from './Download';
|
import Download from './Download';
|
||||||
import CodeTags from './CodeTags';
|
import CodeTags from './CodeTags';
|
||||||
import CodeBraces from './CodeBraces';
|
import CodeBraces from './CodeBraces';
|
||||||
|
import OpenInNew from './OpenInNew';
|
||||||
|
import Information from './Information';
|
||||||
|
import Alert from './Alert';
|
||||||
// Providers
|
// Providers
|
||||||
import Stackedit from './Stackedit';
|
import Provider from './Provider';
|
||||||
import GoogleDrive from './GoogleDrive';
|
|
||||||
import GooglePhotos from './GooglePhotos';
|
|
||||||
|
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
Vue.component('iconFormatItalic', FormatItalic);
|
Vue.component('iconFormatItalic', FormatItalic);
|
||||||
|
@ -62,7 +63,7 @@ Vue.component('iconFileMultiple', FileMultiple);
|
||||||
Vue.component('iconFolderPlus', FolderPlus);
|
Vue.component('iconFolderPlus', FolderPlus);
|
||||||
Vue.component('iconDelete', Delete);
|
Vue.component('iconDelete', Delete);
|
||||||
Vue.component('iconClose', Close);
|
Vue.component('iconClose', Close);
|
||||||
Vue.component('iconFolderMultiple', FolderMultiple);
|
Vue.component('iconFolderOpen', FolderOpen);
|
||||||
Vue.component('iconPen', Pen);
|
Vue.component('iconPen', Pen);
|
||||||
Vue.component('iconTarget', Target);
|
Vue.component('iconTarget', Target);
|
||||||
Vue.component('iconArrowLeft', ArrowLeft);
|
Vue.component('iconArrowLeft', ArrowLeft);
|
||||||
|
@ -78,7 +79,8 @@ Vue.component('iconHardDisk', HardDisk);
|
||||||
Vue.component('iconDownload', Download);
|
Vue.component('iconDownload', Download);
|
||||||
Vue.component('iconCodeTags', CodeTags);
|
Vue.component('iconCodeTags', CodeTags);
|
||||||
Vue.component('iconCodeBraces', CodeBraces);
|
Vue.component('iconCodeBraces', CodeBraces);
|
||||||
|
Vue.component('iconOpenInNew', OpenInNew);
|
||||||
|
Vue.component('iconInformation', Information);
|
||||||
|
Vue.component('iconAlert', Alert);
|
||||||
// Providers
|
// Providers
|
||||||
Vue.component('iconStackedit', Stackedit);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconGoogleDrive', GoogleDrive);
|
|
||||||
Vue.component('iconGooglePhotos', GooglePhotos);
|
|
||||||
|
|
|
@ -356,7 +356,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||||
if (sectionDesc) {
|
if (sectionDesc) {
|
||||||
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
||||||
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
(sectionDesc[objectToScroll.dimensionKey].height * scrollPosition.posInSection);
|
||||||
objectToScroll.elt.scrollTop = scrollTop;
|
objectToScroll.elt.scrollTop = Math.floor(scrollTop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -468,6 +468,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||||
});
|
});
|
||||||
|
|
||||||
this.editorElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
|
this.editorElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
|
||||||
|
this.previewElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
|
||||||
|
|
||||||
const refreshPreview = () => {
|
const refreshPreview = () => {
|
||||||
this.convert();
|
this.convert();
|
||||||
|
@ -594,12 +595,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
||||||
|
|
||||||
this.$emit('inited');
|
this.$emit('inited');
|
||||||
|
|
||||||
// scope.$watch('editorLayoutSvc.currentControl', function (currentControl) {
|
|
||||||
// !currentControl && setTimeout(function () {
|
|
||||||
// !scope.isDialogOpen && clEditorSvc.cledit && clEditorSvc.cledit.focus()
|
|
||||||
// }, 1)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// clEditorSvc.setPreviewElt(element[0].querySelector('.preview__inner-2'))
|
// clEditorSvc.setPreviewElt(element[0].querySelector('.preview__inner-2'))
|
||||||
// var previewElt = element[0].querySelector('.preview')
|
// var previewElt = element[0].querySelector('.preview')
|
||||||
// clEditorSvc.isPreviewTop = previewElt.scrollTop < 10
|
// clEditorSvc.isPreviewTop = previewElt.scrollTop < 10
|
||||||
|
|
|
@ -4,8 +4,8 @@ import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
const indexedDB = window.indexedDB;
|
const indexedDB = window.indexedDB;
|
||||||
const localStorage = window.localStorage;
|
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
|
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
|
|
||||||
if (!indexedDB) {
|
if (!indexedDB) {
|
||||||
|
@ -27,7 +27,7 @@ class Connection {
|
||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
this.db = event.target.result;
|
this.db = event.target.result;
|
||||||
localStorage.localDbVersion = this.db.version; // Safari does not support onversionchange
|
localStorage[dbVersionKey] = this.db.version; // Safari does not support onversionchange
|
||||||
this.db.onversionchange = () => window.location.reload();
|
this.db.onversionchange = () => window.location.reload();
|
||||||
|
|
||||||
this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
||||||
|
@ -68,7 +68,7 @@ class Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If DB version has changed (Safari support)
|
// If DB version has changed (Safari support)
|
||||||
if (parseInt(localStorage.localDbVersion, 10) !== this.db.version) {
|
if (parseInt(localStorage[dbVersionKey], 10) !== this.db.version) {
|
||||||
return window.location.reload();
|
return window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,8 +292,8 @@ export default {
|
||||||
request.onsuccess = resolve;
|
request.onsuccess = resolve;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
localStorage.removeItem('localDbVersion');
|
localStorage.removeItem(dbVersionKey);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}, () => console.error('Could not delete local database.'));
|
}, () => store.dispatch('notification/error', 'Could not delete local database.'));
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -167,9 +167,9 @@ editorSvc.$on('previewText', () => {
|
||||||
|
|
||||||
store.watch(
|
store.watch(
|
||||||
() => store.getters['layout/styles'],
|
() => store.getters['layout/styles'],
|
||||||
() => {
|
(styles) => {
|
||||||
isScrollEditor = true;
|
isScrollEditor = styles.showEditor;
|
||||||
isScrollPreview = false;
|
isScrollPreview = !styles.showEditor;
|
||||||
skipAnimation = true;
|
skipAnimation = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import editorSvc from '../../services/editorSvc';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
|
||||||
// Skip shortcuts if modal is open or editor is hidden
|
// Skip shortcuts if modal is open or editor is hidden
|
||||||
Mousetrap.prototype.stopCallback = () => store.state.modal.config || !store.getters['layout/styles'].showEditor;
|
Mousetrap.prototype.stopCallback = () => store.getters['modal/config'] || !store.getters['layout/styles'].showEditor;
|
||||||
|
|
||||||
const pagedownHandler = name => () => editorSvc.pagedownEditor.uiManager.doClick(name);
|
const pagedownHandler = name => () => editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||||
|
|
||||||
|
|
48
src/services/providers/bloggerPageProvider.js
Normal file
48
src/services/providers/bloggerPageProvider.js
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
import store from '../../store';
|
||||||
|
import googleHelper from './helpers/googleHelper';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'bloggerPage',
|
||||||
|
getToken(location) {
|
||||||
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
|
return token && token.isBlogger ? token : null;
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=page;pageID=${location.pageId}`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
return `${location.pageId} — ${location.blogUrl} — ${token.name}`;
|
||||||
|
},
|
||||||
|
publish(token, html, metadata, publishLocation) {
|
||||||
|
return googleHelper.uploadBlogger(
|
||||||
|
token,
|
||||||
|
publishLocation.blogUrl,
|
||||||
|
publishLocation.blogId,
|
||||||
|
publishLocation.pageId,
|
||||||
|
metadata.title,
|
||||||
|
html,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.then(page => ({
|
||||||
|
...publishLocation,
|
||||||
|
blogId: page.blog.id,
|
||||||
|
pageId: page.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
makeLocation(token, blogUrl, pageId) {
|
||||||
|
const location = {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
blogUrl,
|
||||||
|
};
|
||||||
|
if (pageId) {
|
||||||
|
location.pageId = pageId;
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
});
|
47
src/services/providers/bloggerProvider.js
Normal file
47
src/services/providers/bloggerProvider.js
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import store from '../../store';
|
||||||
|
import googleHelper from './helpers/googleHelper';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'blogger',
|
||||||
|
getToken(location) {
|
||||||
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
|
return token && token.isBlogger ? token : null;
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
return `https://www.blogger.com/blogger.g?blogID=${location.blogId}#editor/target=post;postID=${location.postId}`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
return `${location.postId} — ${location.blogUrl} — ${token.name}`;
|
||||||
|
},
|
||||||
|
publish(token, html, metadata, publishLocation) {
|
||||||
|
return googleHelper.uploadBlogger(
|
||||||
|
token,
|
||||||
|
publishLocation.blogUrl,
|
||||||
|
publishLocation.blogId,
|
||||||
|
publishLocation.postId,
|
||||||
|
metadata.title,
|
||||||
|
html,
|
||||||
|
metadata.tags,
|
||||||
|
metadata.status === 'draft',
|
||||||
|
metadata.date,
|
||||||
|
)
|
||||||
|
.then(post => ({
|
||||||
|
...publishLocation,
|
||||||
|
blogId: post.blog.id,
|
||||||
|
postId: post.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
makeLocation(token, blogUrl, postId) {
|
||||||
|
const location = {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
blogUrl,
|
||||||
|
};
|
||||||
|
if (postId) {
|
||||||
|
location.postId = postId;
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
});
|
143
src/services/providers/dropboxProvider.js
Normal file
143
src/services/providers/dropboxProvider.js
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
import store from '../../store';
|
||||||
|
import dropboxHelper from './helpers/dropboxHelper';
|
||||||
|
import providerUtils from './providerUtils';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
|
const restrictedFolder = '/Applications/StackEdit (restricted)';
|
||||||
|
const restrictedFolderRegexp = /^\/Applications\/StackEdit \(restricted\)/;
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'dropbox',
|
||||||
|
fullAccess: true,
|
||||||
|
getToken(location) {
|
||||||
|
const token = store.getters['data/dropboxTokens'][location.sub];
|
||||||
|
if (token && !!token.fullAccess === this.fullAccess) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
const pathComponents = location.path.split('/').map(encodeURIComponent);
|
||||||
|
const filename = pathComponents.pop();
|
||||||
|
let baseUrl = 'https://www.dropbox.com/home';
|
||||||
|
if (!this.fullAccess) {
|
||||||
|
baseUrl += encodeURIComponent(restrictedFolder);
|
||||||
|
}
|
||||||
|
return `${baseUrl}${pathComponents.join('/')}?preview=${filename}`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
if (this.fullAccess) {
|
||||||
|
return `${location.path} — ${token.name}`;
|
||||||
|
}
|
||||||
|
return `${location.path} — ${token.name} (restricted)`;
|
||||||
|
},
|
||||||
|
checkPath(path) {
|
||||||
|
return path && path.match(/^\/[^\\<>:"|?*]+$/);
|
||||||
|
},
|
||||||
|
downloadContent(token, location) {
|
||||||
|
return dropboxHelper.downloadFile(token, location.path, location.dropboxFileId)
|
||||||
|
.then(({ content }) => providerUtils.parseContent(content));
|
||||||
|
},
|
||||||
|
uploadContent(token, content, location) {
|
||||||
|
return dropboxHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
location.path,
|
||||||
|
providerUtils.serializeContent(content),
|
||||||
|
location.dropboxFileId,
|
||||||
|
)
|
||||||
|
.then(dropboxFile => ({
|
||||||
|
...location,
|
||||||
|
path: dropboxFile.path_display,
|
||||||
|
dropboxFileId: dropboxFile.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
publish(token, html, metadata, location) {
|
||||||
|
return dropboxHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
location.path,
|
||||||
|
html,
|
||||||
|
location.dropboxFileId,
|
||||||
|
)
|
||||||
|
.then(dropboxFile => ({
|
||||||
|
...location,
|
||||||
|
path: dropboxFile.path_display,
|
||||||
|
dropboxFileId: dropboxFile.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
openFiles(token, paths) {
|
||||||
|
const openOneFile = () => {
|
||||||
|
let path = paths.pop();
|
||||||
|
if (!path) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!token.fullAccess) {
|
||||||
|
path = path.replace(restrictedFolderRegexp, '');
|
||||||
|
}
|
||||||
|
let syncLocation;
|
||||||
|
// Try to find an existing sync location
|
||||||
|
store.getters['syncLocation/items'].some((existingSyncLocation) => {
|
||||||
|
if (existingSyncLocation.providerId === this.id &&
|
||||||
|
existingSyncLocation.path === path
|
||||||
|
) {
|
||||||
|
syncLocation = existingSyncLocation;
|
||||||
|
}
|
||||||
|
return syncLocation;
|
||||||
|
});
|
||||||
|
if (syncLocation) {
|
||||||
|
// Sync location already exists, just open the file
|
||||||
|
store.commit('file/setCurrentId', syncLocation.fileId);
|
||||||
|
return openOneFile();
|
||||||
|
}
|
||||||
|
// Sync location does not exist, download content from Dropbox and create the file
|
||||||
|
syncLocation = {
|
||||||
|
path,
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
};
|
||||||
|
return this.downloadContent(token, syncLocation)
|
||||||
|
.then((content) => {
|
||||||
|
const id = utils.uid();
|
||||||
|
delete content.history;
|
||||||
|
store.commit('content/setItem', {
|
||||||
|
...content,
|
||||||
|
id: `${id}/content`,
|
||||||
|
});
|
||||||
|
let name = path;
|
||||||
|
const slashPos = name.lastIndexOf('/');
|
||||||
|
if (slashPos > -1 && slashPos < name.length - 1) {
|
||||||
|
name = name.slice(slashPos + 1);
|
||||||
|
}
|
||||||
|
const dotPos = name.lastIndexOf('.');
|
||||||
|
if (dotPos > 0 && slashPos < name.length) {
|
||||||
|
name = name.slice(0, dotPos);
|
||||||
|
}
|
||||||
|
store.commit('file/setItem', {
|
||||||
|
id,
|
||||||
|
name: name.slice(0, 250),
|
||||||
|
parentId: store.getters['file/current'].parentId,
|
||||||
|
});
|
||||||
|
store.commit('syncLocation/setItem', {
|
||||||
|
...syncLocation,
|
||||||
|
id: utils.uid(),
|
||||||
|
fileId: id,
|
||||||
|
});
|
||||||
|
store.commit('file/setCurrentId', id);
|
||||||
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Dropbox.`);
|
||||||
|
}, () => {
|
||||||
|
store.dispatch('notification/error', `Could not open file ${path}.`);
|
||||||
|
})
|
||||||
|
.then(() => openOneFile());
|
||||||
|
};
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => openOneFile());
|
||||||
|
},
|
||||||
|
makeLocation(token, path) {
|
||||||
|
return {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
8
src/services/providers/dropboxRestrictedProvider.js
Normal file
8
src/services/providers/dropboxRestrictedProvider.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import dropboxProvider from './dropboxProvider';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
...dropboxProvider,
|
||||||
|
id: 'dropboxRestricted',
|
||||||
|
fullAccess: false,
|
||||||
|
});
|
63
src/services/providers/gistProvider.js
Normal file
63
src/services/providers/gistProvider.js
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import store from '../../store';
|
||||||
|
import githubHelper from './helpers/githubHelper';
|
||||||
|
import providerUtils from './providerUtils';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
|
const defaultDescription = 'Untitled';
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'gist',
|
||||||
|
getToken(location) {
|
||||||
|
return store.getters['data/githubTokens'][location.sub];
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
return `https://gist.github.com/${location.gistId}`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
return `${location.filename} — ${location.gistId} — ${token.name}`;
|
||||||
|
},
|
||||||
|
downloadContent(token, location) {
|
||||||
|
return githubHelper.downloadGist(token, location.gistId, location.filename)
|
||||||
|
.then(content => providerUtils.parseContent(content));
|
||||||
|
},
|
||||||
|
uploadContent(token, content, location) {
|
||||||
|
const file = store.state.file.itemMap[location.fileId];
|
||||||
|
const description = (file && file.name) || defaultDescription;
|
||||||
|
return githubHelper.uploadGist(
|
||||||
|
token,
|
||||||
|
description,
|
||||||
|
location.filename,
|
||||||
|
providerUtils.serializeContent(content),
|
||||||
|
location.isPublic,
|
||||||
|
location.gistId,
|
||||||
|
)
|
||||||
|
.then(gist => ({
|
||||||
|
...location,
|
||||||
|
gistId: gist.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
publish(token, html, metadata, location) {
|
||||||
|
return githubHelper.uploadGist(
|
||||||
|
token,
|
||||||
|
metadata.title,
|
||||||
|
location.filename,
|
||||||
|
html,
|
||||||
|
location.isPublic,
|
||||||
|
location.gistId,
|
||||||
|
)
|
||||||
|
.then(gist => ({
|
||||||
|
...location,
|
||||||
|
gistId: gist.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
makeLocation(token, filename, isPublic, gistId) {
|
||||||
|
return {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
filename,
|
||||||
|
isPublic,
|
||||||
|
gistId,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
71
src/services/providers/githubProvider.js
Normal file
71
src/services/providers/githubProvider.js
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
import store from '../../store';
|
||||||
|
import githubHelper from './helpers/githubHelper';
|
||||||
|
import providerUtils from './providerUtils';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
|
const savedSha = {};
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'github',
|
||||||
|
getToken(location) {
|
||||||
|
return store.getters['data/githubTokens'][location.sub];
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
return `https://github.com/${encodeURIComponent(location.owner)}/${encodeURIComponent(location.repo)}/blob/${encodeURIComponent(location.branch)}/${encodeURIComponent(location.path)}`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
return `${location.path} — ${location.owner}/${location.repo} — ${token.name}`;
|
||||||
|
},
|
||||||
|
downloadContent(token, location) {
|
||||||
|
return githubHelper.downloadFile(
|
||||||
|
token, location.owner, location.repo, location.branch, location.path,
|
||||||
|
)
|
||||||
|
.then(({ sha, content }) => {
|
||||||
|
savedSha[location.id] = sha;
|
||||||
|
return providerUtils.parseContent(content);
|
||||||
|
})
|
||||||
|
.catch(() => null); // Ignore error, without the sha upload is going to fail anyway
|
||||||
|
},
|
||||||
|
uploadContent(token, content, location) {
|
||||||
|
const sha = savedSha[location.id];
|
||||||
|
delete savedSha[location.id];
|
||||||
|
return githubHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
location.owner,
|
||||||
|
location.repo,
|
||||||
|
location.branch,
|
||||||
|
location.path,
|
||||||
|
providerUtils.serializeContent(content),
|
||||||
|
sha,
|
||||||
|
)
|
||||||
|
.then(() => location);
|
||||||
|
},
|
||||||
|
publish(token, html, metadata, location) {
|
||||||
|
return this.downloadContent(token, location) // Get the last sha
|
||||||
|
.then(() => {
|
||||||
|
const sha = savedSha[location.id];
|
||||||
|
delete savedSha[location.id];
|
||||||
|
return githubHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
location.owner,
|
||||||
|
location.repo,
|
||||||
|
location.branch,
|
||||||
|
location.path,
|
||||||
|
html,
|
||||||
|
sha,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.then(() => location);
|
||||||
|
},
|
||||||
|
makeLocation(token, owner, repo, branch, path) {
|
||||||
|
return {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
owner,
|
||||||
|
repo,
|
||||||
|
branch,
|
||||||
|
path,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,7 +1,12 @@
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
|
||||||
export default {
|
export default providerRegistry.register({
|
||||||
|
id: 'googleDriveAppData',
|
||||||
|
getToken() {
|
||||||
|
return store.getters['data/loginToken'];
|
||||||
|
},
|
||||||
getChanges(token) {
|
getChanges(token) {
|
||||||
return googleHelper.getChanges(token)
|
return googleHelper.getChanges(token)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
|
@ -37,7 +42,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
saveItem(token, item, syncData, ifNotTooLate) {
|
saveItem(token, item, syncData, ifNotTooLate) {
|
||||||
return googleHelper.saveAppDataFile(
|
return googleHelper.uploadAppDataFile(
|
||||||
token,
|
token,
|
||||||
JSON.stringify(item), ['appDataFolder'],
|
JSON.stringify(item), ['appDataFolder'],
|
||||||
null,
|
null,
|
||||||
|
@ -76,19 +81,19 @@ export default {
|
||||||
return item;
|
return item;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
|
const syncData = store.getters['data/syncDataByItemId'][`${syncLocation.fileId}/content`];
|
||||||
if (syncData && syncData.hash === item.hash) {
|
if (syncData && syncData.hash === content.hash) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return googleHelper.saveAppDataFile(
|
return googleHelper.uploadAppDataFile(
|
||||||
token,
|
token,
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
id: item.id,
|
id: content.id,
|
||||||
type: item.type,
|
type: content.type,
|
||||||
hash: item.hash,
|
hash: content.hash,
|
||||||
}), ['appDataFolder'],
|
}), ['appDataFolder'],
|
||||||
JSON.stringify(item),
|
JSON.stringify(content),
|
||||||
syncData && syncData.id,
|
syncData && syncData.id,
|
||||||
ifNotTooLate,
|
ifNotTooLate,
|
||||||
)
|
)
|
||||||
|
@ -97,10 +102,10 @@ export default {
|
||||||
[file.id]: {
|
[file.id]: {
|
||||||
// Build sync data
|
// Build sync data
|
||||||
id: file.id,
|
id: file.id,
|
||||||
itemId: item.id,
|
itemId: content.id,
|
||||||
type: item.type,
|
type: content.type,
|
||||||
hash: item.hash,
|
hash: content.hash,
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
|
@ -1,28 +1,42 @@
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
import googleHelper from './helpers/googleHelper';
|
import googleHelper from './helpers/googleHelper';
|
||||||
import providerUtils from './providerUtils';
|
import providerUtils from './providerUtils';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
const defaultFilename = 'Untitled';
|
const defaultFilename = 'Untitled';
|
||||||
|
|
||||||
export default {
|
export default providerRegistry.register({
|
||||||
|
id: 'googleDrive',
|
||||||
|
getToken(location) {
|
||||||
|
const token = store.getters['data/googleTokens'][location.sub];
|
||||||
|
return token && token.isDrive ? token : null;
|
||||||
|
},
|
||||||
|
getUrl(location) {
|
||||||
|
return `https://docs.google.com/file/d/${location.driveFileId}/edit`;
|
||||||
|
},
|
||||||
|
getDescription(location) {
|
||||||
|
const token = this.getToken(location);
|
||||||
|
return `${location.driveFileId} — ${token.name}`;
|
||||||
|
},
|
||||||
downloadContent(token, syncLocation) {
|
downloadContent(token, syncLocation) {
|
||||||
return googleHelper.downloadFile(token, syncLocation.driveFileId)
|
return googleHelper.downloadFile(token, syncLocation.driveFileId)
|
||||||
.then(content => providerUtils.parseContent(content));
|
.then(content => providerUtils.parseContent(content));
|
||||||
},
|
},
|
||||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||||
const name = (file && file.name) || defaultFilename;
|
const name = (file && file.name) || defaultFilename;
|
||||||
const parents = [];
|
const parents = [];
|
||||||
if (syncLocation.driveParentId) {
|
if (syncLocation.driveParentId) {
|
||||||
parents.push(syncLocation.driveParentId);
|
parents.push(syncLocation.driveParentId);
|
||||||
}
|
}
|
||||||
return googleHelper.saveFile(
|
return googleHelper.uploadFile(
|
||||||
token,
|
token,
|
||||||
name,
|
name,
|
||||||
parents,
|
parents,
|
||||||
providerUtils.serializeContent(item),
|
providerUtils.serializeContent(content),
|
||||||
syncLocation && syncLocation.driveFileId,
|
undefined,
|
||||||
|
syncLocation.driveFileId,
|
||||||
ifNotTooLate,
|
ifNotTooLate,
|
||||||
)
|
)
|
||||||
.then(driveFile => ({
|
.then(driveFile => ({
|
||||||
|
@ -30,6 +44,20 @@ export default {
|
||||||
driveFileId: driveFile.id,
|
driveFileId: driveFile.id,
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
publish(token, html, metadata, publishLocation) {
|
||||||
|
return googleHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
metadata.title,
|
||||||
|
[],
|
||||||
|
html,
|
||||||
|
publishLocation.templateId ? 'text/html' : undefined,
|
||||||
|
publishLocation.driveFileId,
|
||||||
|
)
|
||||||
|
.then(driveFile => ({
|
||||||
|
...publishLocation,
|
||||||
|
driveFileId: driveFile.id,
|
||||||
|
}));
|
||||||
|
},
|
||||||
openFiles(token, files) {
|
openFiles(token, files) {
|
||||||
const openOneFile = () => {
|
const openOneFile = () => {
|
||||||
const file = files.pop();
|
const file = files.pop();
|
||||||
|
@ -39,20 +67,22 @@ export default {
|
||||||
let syncLocation;
|
let syncLocation;
|
||||||
// Try to find an existing sync location
|
// Try to find an existing sync location
|
||||||
store.getters['syncLocation/items'].some((existingSyncLocation) => {
|
store.getters['syncLocation/items'].some((existingSyncLocation) => {
|
||||||
if (existingSyncLocation.driveFileId === file.id) {
|
if (existingSyncLocation.providerId === this.id &&
|
||||||
|
existingSyncLocation.driveFileId === file.id
|
||||||
|
) {
|
||||||
syncLocation = existingSyncLocation;
|
syncLocation = existingSyncLocation;
|
||||||
}
|
}
|
||||||
return syncLocation;
|
return syncLocation;
|
||||||
});
|
});
|
||||||
if (syncLocation) {
|
if (syncLocation) {
|
||||||
// Sync location already exists, just open the file
|
// Sync location already exists, just open the file
|
||||||
this.$store.commit('file/setCurrentId', syncLocation.fileId);
|
store.commit('file/setCurrentId', syncLocation.fileId);
|
||||||
return openOneFile();
|
return openOneFile();
|
||||||
}
|
}
|
||||||
// Sync location does not exist, download content from Google Drive and create the file
|
// Sync location does not exist, download content from Google Drive and create the file
|
||||||
syncLocation = {
|
syncLocation = {
|
||||||
driveFileId: file.id,
|
driveFileId: file.id,
|
||||||
provider: 'googleDrive',
|
providerId: this.id,
|
||||||
sub: token.sub,
|
sub: token.sub,
|
||||||
};
|
};
|
||||||
return this.downloadContent(token, syncLocation)
|
return this.downloadContent(token, syncLocation)
|
||||||
|
@ -74,12 +104,26 @@ export default {
|
||||||
fileId: id,
|
fileId: id,
|
||||||
});
|
});
|
||||||
store.commit('file/setCurrentId', id);
|
store.commit('file/setCurrentId', id);
|
||||||
|
store.dispatch('notification/info', `${store.getters['file/current'].name} was imported from Google Drive.`);
|
||||||
}, () => {
|
}, () => {
|
||||||
console.error(`Could not open file ${file.id}.`);
|
store.dispatch('notification/error', `Could not open file ${file.id}.`);
|
||||||
})
|
})
|
||||||
.then(() => openOneFile());
|
.then(() => openOneFile());
|
||||||
};
|
};
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => openOneFile());
|
.then(() => openOneFile());
|
||||||
},
|
},
|
||||||
};
|
makeLocation(token, fileId, folderId) {
|
||||||
|
const location = {
|
||||||
|
providerId: this.id,
|
||||||
|
sub: token.sub,
|
||||||
|
};
|
||||||
|
if (fileId) {
|
||||||
|
location.driveFileId = fileId;
|
||||||
|
}
|
||||||
|
if (folderId) {
|
||||||
|
location.driveParentId = folderId;
|
||||||
|
}
|
||||||
|
return location;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
106
src/services/providers/helpers/dropboxHelper.js
Normal file
106
src/services/providers/helpers/dropboxHelper.js
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
import utils from '../../utils';
|
||||||
|
import store from '../../../store';
|
||||||
|
|
||||||
|
let Dropbox;
|
||||||
|
|
||||||
|
const getAppKey = (fullAccess) => {
|
||||||
|
if (fullAccess) {
|
||||||
|
return 'lq6mwopab8wskas';
|
||||||
|
}
|
||||||
|
return 'sw0hlixhr8q1xk0';
|
||||||
|
};
|
||||||
|
|
||||||
|
const request = (token, options, args) => utils.request({
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
'Content-Type': 'application/octet-stream',
|
||||||
|
'Dropbox-API-Arg': args && JSON.stringify(args),
|
||||||
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startOauth2(fullAccess, sub = null, silent = false) {
|
||||||
|
return utils.startOauth2(
|
||||||
|
'https://www.dropbox.com/oauth2/authorize', {
|
||||||
|
client_id: getAppKey(fullAccess),
|
||||||
|
response_type: 'token',
|
||||||
|
}, silent)
|
||||||
|
// Call the user info endpoint
|
||||||
|
.then(({ accessToken }) => request({ accessToken }, {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
// Check the returned sub consistency
|
||||||
|
if (sub && res.body.account_id !== sub) {
|
||||||
|
throw new Error('Dropbox account ID not expected.');
|
||||||
|
}
|
||||||
|
// Build token object including scopes and sub
|
||||||
|
const token = {
|
||||||
|
accessToken,
|
||||||
|
name: res.body.name.display_name,
|
||||||
|
sub: res.body.account_id,
|
||||||
|
fullAccess,
|
||||||
|
};
|
||||||
|
// Add token to githubTokens
|
||||||
|
store.dispatch('data/setDropboxToken', token);
|
||||||
|
return token;
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
loadClientScript() {
|
||||||
|
if (Dropbox) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
return utils.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
|
||||||
|
.then(() => {
|
||||||
|
Dropbox = window.Dropbox;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addAccount(fullAccess = false) {
|
||||||
|
return this.startOauth2(fullAccess);
|
||||||
|
},
|
||||||
|
uploadFile(token, path, content, fileId) {
|
||||||
|
return request(token, {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://content.dropboxapi.com/2/files/upload',
|
||||||
|
body: content,
|
||||||
|
}, {
|
||||||
|
path: fileId || path,
|
||||||
|
mode: 'overwrite',
|
||||||
|
})
|
||||||
|
.then(res => res.body);
|
||||||
|
},
|
||||||
|
downloadFile(token, path, fileId) {
|
||||||
|
return request(token, {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://content.dropboxapi.com/2/files/download',
|
||||||
|
raw: true,
|
||||||
|
}, {
|
||||||
|
path: fileId || path,
|
||||||
|
})
|
||||||
|
.then(res => ({
|
||||||
|
id: JSON.parse(res.headers['dropbox-api-result']).id,
|
||||||
|
content: res.body,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
openChooser(token) {
|
||||||
|
return this.loadClientScript()
|
||||||
|
.then(() => new Promise((resolve) => {
|
||||||
|
Dropbox.appKey = getAppKey(token.fullAccess);
|
||||||
|
Dropbox.choose({
|
||||||
|
multiselect: true,
|
||||||
|
linkType: 'direct',
|
||||||
|
success: (files) => {
|
||||||
|
const paths = files.map((file) => {
|
||||||
|
const path = file.link.replace(/.*\/view\/[^/]*/, '');
|
||||||
|
return decodeURI(path);
|
||||||
|
});
|
||||||
|
resolve(paths);
|
||||||
|
},
|
||||||
|
cancel: () => resolve([]),
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
132
src/services/providers/helpers/githubHelper.js
Normal file
132
src/services/providers/helpers/githubHelper.js
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import utils from '../../utils';
|
||||||
|
import store from '../../../store';
|
||||||
|
|
||||||
|
let clientId = 'cbf0cf25cfd026be23e1';
|
||||||
|
if (utils.origin === 'https://stackedit.io') {
|
||||||
|
clientId = '30c1491057c9ad4dbd56';
|
||||||
|
}
|
||||||
|
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||||
|
|
||||||
|
const request = (token, options) => utils.request({
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers,
|
||||||
|
Authorization: `token ${token.accessToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const base64Encode = str => btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
|
||||||
|
(match, p1) => String.fromCharCode(`0x${p1}`),
|
||||||
|
));
|
||||||
|
const base64Decode = str => decodeURIComponent(atob(str).split('').map(
|
||||||
|
c => `%${`00${c.charCodeAt(0).toString(16)}`.slice(-2)}`,
|
||||||
|
).join(''));
|
||||||
|
|
||||||
|
export default {
|
||||||
|
startOauth2(scopes, sub = null, silent = false) {
|
||||||
|
return utils.startOauth2(
|
||||||
|
'https://github.com/login/oauth/authorize', {
|
||||||
|
client_id: clientId,
|
||||||
|
scope: scopes.join(' '),
|
||||||
|
}, silent)
|
||||||
|
// Exchange code with token
|
||||||
|
.then(data => utils.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'oauth2/githubToken',
|
||||||
|
params: {
|
||||||
|
clientId,
|
||||||
|
code: data.code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(res => res.body))
|
||||||
|
// Call the user info endpoint
|
||||||
|
.then(accessToken => utils.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: 'https://api.github.com/user',
|
||||||
|
params: {
|
||||||
|
access_token: accessToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
// Check the returned sub consistency
|
||||||
|
if (sub && res.body.id !== sub) {
|
||||||
|
throw new Error('GitHub account ID not expected.');
|
||||||
|
}
|
||||||
|
// Build token object including scopes and sub
|
||||||
|
const token = {
|
||||||
|
scopes,
|
||||||
|
accessToken,
|
||||||
|
name: res.body.name,
|
||||||
|
sub: res.body.id,
|
||||||
|
repoFullAccess: scopes.indexOf('repo') !== -1,
|
||||||
|
};
|
||||||
|
// Add token to githubTokens
|
||||||
|
store.dispatch('data/setGithubToken', token);
|
||||||
|
return token;
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
addAccount(repoFullAccess = false) {
|
||||||
|
return this.startOauth2(getScopes({ repoFullAccess }));
|
||||||
|
},
|
||||||
|
uploadFile(token, owner, repo, branch, path, content, sha) {
|
||||||
|
return request(token, {
|
||||||
|
method: 'PUT',
|
||||||
|
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
|
||||||
|
body: {
|
||||||
|
message: 'Uploaded by https://stackedit.io/',
|
||||||
|
content: base64Encode(content),
|
||||||
|
sha,
|
||||||
|
branch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
downloadFile(token, owner, repo, branch, path) {
|
||||||
|
return request(token, {
|
||||||
|
url: `https://api.github.com/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/contents/${encodeURIComponent(path)}`,
|
||||||
|
params: { ref: branch },
|
||||||
|
})
|
||||||
|
.then(res => ({
|
||||||
|
sha: res.body.sha,
|
||||||
|
content: base64Decode(res.body.content),
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
uploadGist(token, description, filename, content, isPublic, gistId) {
|
||||||
|
return request(token, gistId ? {
|
||||||
|
method: 'PATCH',
|
||||||
|
url: `https://api.github.com/gists/${gistId}`,
|
||||||
|
body: {
|
||||||
|
description,
|
||||||
|
files: {
|
||||||
|
[filename]: {
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} : {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://api.github.com/gists',
|
||||||
|
body: {
|
||||||
|
description,
|
||||||
|
files: {
|
||||||
|
[filename]: {
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
public: isPublic,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(res => res.body);
|
||||||
|
},
|
||||||
|
downloadGist(token, gistId, filename) {
|
||||||
|
return request(token, {
|
||||||
|
url: `https://api.github.com/gists/${gistId}`,
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
const result = res.body.files[filename];
|
||||||
|
if (!result) {
|
||||||
|
throw new Error('Gist file not found.');
|
||||||
|
}
|
||||||
|
return result.content;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
|
@ -12,7 +12,7 @@ const getDriveScopes = token => [token.driveFullAccess
|
||||||
? 'https://www.googleapis.com/auth/drive'
|
? 'https://www.googleapis.com/auth/drive'
|
||||||
: 'https://www.googleapis.com/auth/drive.file',
|
: 'https://www.googleapis.com/auth/drive.file',
|
||||||
'https://www.googleapis.com/auth/drive.install'];
|
'https://www.googleapis.com/auth/drive.install'];
|
||||||
// const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
|
const bloggerScopes = ['https://www.googleapis.com/auth/blogger'];
|
||||||
const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
||||||
|
|
||||||
const libraries = ['picker'];
|
const libraries = ['picker'];
|
||||||
|
@ -25,9 +25,7 @@ const request = (token, options) => utils.request({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function saveFile(refreshedToken, name, parents, media = null, fileId = null,
|
function uploadFile(refreshedToken, name, parents, media = null, mediaType = 'text/plain', fileId = null, ifNotTooLate = cb => res => cb(res)) {
|
||||||
ifNotTooLate = cb => res => cb(res),
|
|
||||||
) {
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||||
.then(ifNotTooLate(() => {
|
.then(ifNotTooLate(() => {
|
||||||
|
@ -52,7 +50,7 @@ function saveFile(refreshedToken, name, parents, media = null, fileId = null,
|
||||||
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
||||||
multipartRequestBody += JSON.stringify(metadata);
|
multipartRequestBody += JSON.stringify(metadata);
|
||||||
multipartRequestBody += delimiter;
|
multipartRequestBody += delimiter;
|
||||||
multipartRequestBody += 'Content-Type: text/plain; charset=UTF-8\r\n\r\n';
|
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
||||||
multipartRequestBody += media;
|
multipartRequestBody += media;
|
||||||
multipartRequestBody += closeDelimiter;
|
multipartRequestBody += closeDelimiter;
|
||||||
options.url = options.url.replace(
|
options.url = options.url.replace(
|
||||||
|
@ -95,7 +93,7 @@ export default {
|
||||||
login_hint: sub,
|
login_hint: sub,
|
||||||
prompt: silent ? 'none' : null,
|
prompt: silent ? 'none' : null,
|
||||||
}, silent)
|
}, silent)
|
||||||
// Call the tokeninfo endpoint
|
// Call the token info endpoint
|
||||||
.then(data => utils.request({
|
.then(data => utils.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
||||||
|
@ -121,11 +119,12 @@ export default {
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||||
|
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||||
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
// Call the tokeninfo endpoint
|
// Call the user info endpoint
|
||||||
.then(token => request(token, {
|
.then(token => request(token, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
url: 'https://www.googleapis.com/plus/v1/people/me',
|
||||||
|
@ -139,6 +138,7 @@ export default {
|
||||||
// Save flags
|
// Save flags
|
||||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||||
|
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||||
// Save nextPageToken
|
// Save nextPageToken
|
||||||
|
@ -168,7 +168,8 @@ export default {
|
||||||
// Try to get a new token in background
|
// Try to get a new token in background
|
||||||
return this.startOauth2(mergedScopes, sub, true)
|
return this.startOauth2(mergedScopes, sub, true)
|
||||||
// If it fails try to popup a window
|
// If it fails try to popup a window
|
||||||
.catch(() => this.startOauth2(mergedScopes, sub));
|
.catch(() => utils.checkOnline()
|
||||||
|
.then(() => this.startOauth2(mergedScopes, sub)));
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadClientScript() {
|
loadClientScript() {
|
||||||
|
@ -176,17 +177,17 @@ export default {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return utils.loadScript('https://apis.google.com/js/api.js')
|
return utils.loadScript('https://apis.google.com/js/api.js')
|
||||||
.then(() => Promise.all(libraries.map(
|
.then(() => Promise.all(libraries.map(
|
||||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||||
callback: resolve,
|
callback: resolve,
|
||||||
onerror: reject,
|
onerror: reject,
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
ontimeout: reject,
|
ontimeout: reject,
|
||||||
})))))
|
})))))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
gapi = window.gapi;
|
gapi = window.gapi;
|
||||||
google = window.google;
|
google = window.google;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
signin() {
|
signin() {
|
||||||
return this.startOauth2(driveAppDataScopes);
|
return this.startOauth2(driveAppDataScopes);
|
||||||
|
@ -194,6 +195,9 @@ export default {
|
||||||
addDriveAccount(fullAccess = false) {
|
addDriveAccount(fullAccess = false) {
|
||||||
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }));
|
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }));
|
||||||
},
|
},
|
||||||
|
addBloggerAccount() {
|
||||||
|
return this.startOauth2(bloggerScopes);
|
||||||
|
},
|
||||||
addPhotosAccount() {
|
addPhotosAccount() {
|
||||||
return this.startOauth2(photosScopes);
|
return this.startOauth2(photosScopes);
|
||||||
},
|
},
|
||||||
|
@ -224,13 +228,15 @@ export default {
|
||||||
return getPage(refreshedToken.nextPageToken);
|
return getPage(refreshedToken.nextPageToken);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveFile(token, name, parents, media, fileId, ifNotTooLate) {
|
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(getDriveScopes(token), token)
|
||||||
.then(refreshedToken => saveFile(refreshedToken, name, parents, media, fileId, ifNotTooLate));
|
.then(refreshedToken => uploadFile(
|
||||||
|
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
saveAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(driveAppDataScopes, token)
|
||||||
.then(refreshedToken => saveFile(refreshedToken, name, parents, media, fileId, ifNotTooLate));
|
.then(refreshedToken => uploadFile(
|
||||||
|
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
downloadFile(token, id) {
|
downloadFile(token, id) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(getDriveScopes(token), token)
|
||||||
|
@ -248,6 +254,72 @@ export default {
|
||||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||||
})));
|
})));
|
||||||
},
|
},
|
||||||
|
uploadBlogger(
|
||||||
|
token, blogUrl, blogId, postId, title, content, labels, isDraft, published, isPage,
|
||||||
|
) {
|
||||||
|
return this.refreshToken(bloggerScopes, token)
|
||||||
|
.then(refreshedToken => Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
if (blogId) {
|
||||||
|
return blogId;
|
||||||
|
}
|
||||||
|
return request(refreshedToken, {
|
||||||
|
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
|
||||||
|
params: {
|
||||||
|
url: blogUrl,
|
||||||
|
},
|
||||||
|
}).then(res => res.body.id);
|
||||||
|
})
|
||||||
|
.then((resolvedBlogId) => {
|
||||||
|
const path = isPage ? 'pages' : 'posts';
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `https://www.googleapis.com/blogger/v3/blogs/${resolvedBlogId}/${path}/`,
|
||||||
|
body: {
|
||||||
|
kind: isPage ? 'blogger#page' : 'blogger#post',
|
||||||
|
blog: {
|
||||||
|
id: resolvedBlogId,
|
||||||
|
},
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (labels) {
|
||||||
|
options.body.labels = labels;
|
||||||
|
}
|
||||||
|
if (published) {
|
||||||
|
options.body.published = published.toISOString();
|
||||||
|
}
|
||||||
|
// If it's an update
|
||||||
|
if (postId) {
|
||||||
|
options.method = 'PUT';
|
||||||
|
options.url += postId;
|
||||||
|
options.body.id = postId;
|
||||||
|
}
|
||||||
|
return request(refreshedToken, options)
|
||||||
|
.then(res => res.body);
|
||||||
|
})
|
||||||
|
.then((post) => {
|
||||||
|
if (isPage) {
|
||||||
|
return post;
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: `https://www.googleapis.com/blogger/v3/blogs/${post.blog.id}/posts/${post.id}/`,
|
||||||
|
params: {},
|
||||||
|
};
|
||||||
|
if (isDraft) {
|
||||||
|
options.url += 'revert';
|
||||||
|
} else {
|
||||||
|
options.url += 'publish';
|
||||||
|
if (published) {
|
||||||
|
options.params.publishDate = published.toISOString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return request(refreshedToken, options)
|
||||||
|
.then(res => res.body);
|
||||||
|
}));
|
||||||
|
},
|
||||||
openPicker(token, type = 'doc') {
|
openPicker(token, type = 'doc') {
|
||||||
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
||||||
return this.loadClientScript()
|
return this.loadClientScript()
|
||||||
|
|
7
src/services/providers/providerRegistry.js
Normal file
7
src/services/providers/providerRegistry.js
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export default {
|
||||||
|
providers: {},
|
||||||
|
register(provider) {
|
||||||
|
this.providers[provider.id] = provider;
|
||||||
|
return provider;
|
||||||
|
},
|
||||||
|
};
|
148
src/services/publishSvc.js
Normal file
148
src/services/publishSvc.js
Normal file
|
@ -0,0 +1,148 @@
|
||||||
|
import localDbSvc from './localDbSvc';
|
||||||
|
import store from '../store';
|
||||||
|
import utils from './utils';
|
||||||
|
import exportSvc from './exportSvc';
|
||||||
|
import providerRegistry from './providers/providerRegistry';
|
||||||
|
|
||||||
|
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||||
|
|
||||||
|
const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
||||||
|
// Item does not exist, create it
|
||||||
|
.catch(() => store.commit(`${type}/setItem`, {
|
||||||
|
id: `${fileId}/${type}`,
|
||||||
|
}));
|
||||||
|
const loadContent = loader('content');
|
||||||
|
|
||||||
|
const ensureArray = (value) => {
|
||||||
|
if (!value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (!Array.isArray(value)) {
|
||||||
|
return value.toString().trim().split(/\s*,\s*/);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureString = (value, defaultValue) => {
|
||||||
|
if (!value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const ensureDate = (value, defaultValue) => {
|
||||||
|
if (!value) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return new Date(value.toString());
|
||||||
|
};
|
||||||
|
|
||||||
|
function publish(publishLocation) {
|
||||||
|
const fileId = publishLocation.fileId;
|
||||||
|
const template = store.getters['data/allTemplates'][publishLocation.templateId];
|
||||||
|
return exportSvc.applyTemplate(fileId, template)
|
||||||
|
.then(html => localDbSvc.loadItem(`${fileId}/content`)
|
||||||
|
.then((content) => {
|
||||||
|
const file = store.state.file.itemMap[fileId];
|
||||||
|
const properties = utils.computeProperties(content.properties);
|
||||||
|
const provider = providerRegistry.providers[publishLocation.providerId];
|
||||||
|
const token = provider.getToken(publishLocation);
|
||||||
|
const metadata = {
|
||||||
|
title: ensureString(properties.title, file.name),
|
||||||
|
author: ensureString(properties.author),
|
||||||
|
tags: ensureArray(properties.tags),
|
||||||
|
categories: ensureArray(properties.categories),
|
||||||
|
excerpt: ensureString(properties.excerpt),
|
||||||
|
featuredImage: ensureString(properties.featuredImage),
|
||||||
|
status: ensureString(properties.status),
|
||||||
|
date: ensureDate(properties.date),
|
||||||
|
};
|
||||||
|
return provider.publish(token, html, metadata, publishLocation);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function publishFile(fileId) {
|
||||||
|
let counter = 0;
|
||||||
|
return loadContent(fileId)
|
||||||
|
.then(() => {
|
||||||
|
const publishLocations = [
|
||||||
|
...store.getters['publishLocation/groupedByFileId'][fileId] || [],
|
||||||
|
];
|
||||||
|
const publishOneContentLocation = () => {
|
||||||
|
const publishLocation = publishLocations.shift();
|
||||||
|
if (!publishLocation) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return store.dispatch('queue/doWithLocation', {
|
||||||
|
location: publishLocation,
|
||||||
|
promise: publish(publishLocation)
|
||||||
|
.then((publishLocationToStore) => {
|
||||||
|
// Replace publish location if modified
|
||||||
|
if (utils.serializeObject(publishLocation) !==
|
||||||
|
utils.serializeObject(publishLocationToStore)
|
||||||
|
) {
|
||||||
|
store.commit('publishLocation/patchItem', publishLocationToStore);
|
||||||
|
}
|
||||||
|
counter += 1;
|
||||||
|
return publishOneContentLocation();
|
||||||
|
}, (err) => {
|
||||||
|
if (store.state.offline) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
store.dispatch('notification/error', err);
|
||||||
|
return publishOneContentLocation();
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return publishOneContentLocation();
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
const file = store.state.file.itemMap[fileId];
|
||||||
|
store.dispatch('notification/info', `"${file.name}" was published to ${counter} location(s).`);
|
||||||
|
})
|
||||||
|
.then(
|
||||||
|
() => localDbSvc.unloadContents(),
|
||||||
|
err => localDbSvc.unloadContents()
|
||||||
|
.then(() => {
|
||||||
|
throw err;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function requestPublish() {
|
||||||
|
store.dispatch('queue/enqueuePublishRequest', () => new Promise((resolve, reject) => {
|
||||||
|
let intervalId;
|
||||||
|
const attempt = () => {
|
||||||
|
// Only start publishing when these conditions are met
|
||||||
|
if (utils.isUserActive()) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
if (!hasCurrentFilePublishLocations()) {
|
||||||
|
// Cancel sync
|
||||||
|
reject('Publish not possible.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
publishFile(store.getters['file/current'].id)
|
||||||
|
.then(resolve, reject);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
intervalId = utils.setInterval(() => attempt(), 1000);
|
||||||
|
attempt();
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPublishLocation(publishLocation) {
|
||||||
|
publishLocation.id = utils.uid();
|
||||||
|
const currentFile = store.getters['file/current'];
|
||||||
|
publishLocation.fileId = currentFile.id;
|
||||||
|
store.dispatch('queue/enqueue',
|
||||||
|
() => publish(publishLocation)
|
||||||
|
.then((publishLocationToStore) => {
|
||||||
|
store.commit('publishLocation/setItem', publishLocationToStore);
|
||||||
|
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
requestPublish,
|
||||||
|
createPublishLocation,
|
||||||
|
};
|
|
@ -3,11 +3,10 @@ import store from '../store';
|
||||||
import welcomeFile from '../data/welcomeFile.md';
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import diffUtils from './diffUtils';
|
import diffUtils from './diffUtils';
|
||||||
import userActivitySvc from './userActivitySvc';
|
import providerRegistry from './providers/providerRegistry';
|
||||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
import mainProvider from './providers/googleDriveAppDataProvider';
|
||||||
import googleDriveProvider from './providers/googleDriveProvider';
|
|
||||||
|
|
||||||
const lastSyncActivityKey = 'lastSyncActivity';
|
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
||||||
let lastSyncActivity;
|
let lastSyncActivity;
|
||||||
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
|
@ -37,26 +36,6 @@ function setLastSyncActivity() {
|
||||||
localStorage[lastSyncActivityKey] = currentDate;
|
localStorage[lastSyncActivityKey] = currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSyncProvider(syncLocation) {
|
|
||||||
switch (syncLocation.provider) {
|
|
||||||
case 'googleDriveAppData':
|
|
||||||
default:
|
|
||||||
return googleDriveAppDataProvider;
|
|
||||||
case 'googleDrive':
|
|
||||||
return googleDriveProvider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getSyncToken(syncLocation) {
|
|
||||||
switch (syncLocation.provider) {
|
|
||||||
case 'googleDriveAppData':
|
|
||||||
default:
|
|
||||||
return store.getters['data/loginToken'];
|
|
||||||
case 'googleDrive':
|
|
||||||
return store.getters['data/googleTokens'][syncLocation.sub];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function cleanSyncedContent(syncedContent) {
|
function cleanSyncedContent(syncedContent) {
|
||||||
// Clean syncHistory from removed syncLocations
|
// Clean syncHistory from removed syncLocations
|
||||||
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
|
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
|
||||||
|
@ -123,6 +102,37 @@ function applyChanges(changes) {
|
||||||
const LAST_SENT = 0;
|
const LAST_SENT = 0;
|
||||||
const LAST_MERGED = 1;
|
const LAST_MERGED = 1;
|
||||||
|
|
||||||
|
function createSyncLocation(syncLocation) {
|
||||||
|
syncLocation.id = utils.uid();
|
||||||
|
const currentFile = store.getters['file/current'];
|
||||||
|
const fileId = currentFile.id;
|
||||||
|
syncLocation.fileId = fileId;
|
||||||
|
// Use deepCopy to freeze item
|
||||||
|
const content = utils.deepCopy(store.getters['content/current']);
|
||||||
|
store.dispatch('queue/enqueue',
|
||||||
|
() => {
|
||||||
|
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||||
|
const token = provider.getToken(syncLocation);
|
||||||
|
return provider.uploadContent(token, {
|
||||||
|
...content,
|
||||||
|
history: [content.hash],
|
||||||
|
}, syncLocation)
|
||||||
|
.then(syncLocationToStore => loadSyncedContent(fileId)
|
||||||
|
.then(() => {
|
||||||
|
const newSyncedContent = utils.deepCopy(
|
||||||
|
store.state.syncedContent.itemMap[`${fileId}/syncedContent`]);
|
||||||
|
const newSyncHistoryItem = [];
|
||||||
|
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||||
|
newSyncHistoryItem[LAST_SENT] = content.hash;
|
||||||
|
newSyncedContent.historyData[content.hash] = content;
|
||||||
|
|
||||||
|
store.commit('syncedContent/patchItem', newSyncedContent);
|
||||||
|
store.commit('syncLocation/setItem', syncLocationToStore);
|
||||||
|
store.dispatch('notification/info', `A new synchronized location was added to "${currentFile.name}".`);
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function syncFile(fileId) {
|
function syncFile(fileId) {
|
||||||
return loadSyncedContent(fileId)
|
return loadSyncedContent(fileId)
|
||||||
.then(() => loadContent(fileId))
|
.then(() => loadContent(fileId))
|
||||||
|
@ -131,121 +141,145 @@ function syncFile(fileId) {
|
||||||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||||
const downloadedLocations = {};
|
const downloadedLocations = {};
|
||||||
|
const errorLocations = {};
|
||||||
|
|
||||||
const isLocationSynced = syncLocation =>
|
const isLocationSynced = (syncLocation) => {
|
||||||
getSyncHistoryItem(syncLocation.id)[LAST_SENT] === getContent().hash;
|
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||||
|
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
||||||
|
};
|
||||||
|
|
||||||
const syncOneContentLocation = () => {
|
const syncOneContentLocation = () => {
|
||||||
const syncLocations = [
|
const syncLocations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
if (isDataSyncPossible()) {
|
if (isDataSyncPossible()) {
|
||||||
syncLocations.unshift({ id: 'main', provider: 'googleDriveAppData', fileId });
|
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
syncLocations.some((syncLocation) => {
|
syncLocations.some((syncLocation) => {
|
||||||
if (!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation)) {
|
if (!errorLocations[syncLocation.id] &&
|
||||||
const provider = getSyncProvider(syncLocation);
|
(!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation))
|
||||||
const token = getSyncToken(syncLocation);
|
) {
|
||||||
result = provider && token && provider.downloadContent(token, syncLocation)
|
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||||
.then((serverContent = null) => {
|
const token = provider.getToken(syncLocation);
|
||||||
downloadedLocations[syncLocation.id] = true;
|
result = provider && token && store.dispatch('queue/doWithLocation', {
|
||||||
|
location: syncLocation,
|
||||||
|
promise: provider.downloadContent(token, syncLocation)
|
||||||
|
.then((serverContent = null) => {
|
||||||
|
downloadedLocations[syncLocation.id] = true;
|
||||||
|
|
||||||
const syncedContent = getSyncedContent();
|
const syncedContent = getSyncedContent();
|
||||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||||
let mergedContent = (() => {
|
let mergedContent = (() => {
|
||||||
const clientContent = utils.deepCopy(getContent());
|
const clientContent = utils.deepCopy(getContent());
|
||||||
if (!serverContent) {
|
if (!serverContent) {
|
||||||
// Sync location has not been created yet
|
// Sync location has not been created yet
|
||||||
return clientContent;
|
return clientContent;
|
||||||
}
|
}
|
||||||
if (serverContent.hash === clientContent.hash) {
|
if (serverContent.hash === clientContent.hash) {
|
||||||
// Server and client contents are synced
|
// Server and client contents are synced
|
||||||
return clientContent;
|
return clientContent;
|
||||||
}
|
}
|
||||||
if (syncedContent.historyData[serverContent.hash]) {
|
if (syncedContent.historyData[serverContent.hash]) {
|
||||||
// Server content has not changed or has already been merged
|
// Server content has not changed or has already been merged
|
||||||
return clientContent;
|
return clientContent;
|
||||||
}
|
}
|
||||||
// Perform a merge with last merged content if any, or a simple fusion otherwise
|
// Perform a merge with last merged content if any, or a simple fusion otherwise
|
||||||
let lastMergedContent;
|
let lastMergedContent;
|
||||||
serverContent.history.some((hash) => {
|
serverContent.history.some((hash) => {
|
||||||
lastMergedContent = syncedContent.historyData[hash];
|
lastMergedContent = syncedContent.historyData[hash];
|
||||||
return lastMergedContent;
|
return lastMergedContent;
|
||||||
|
});
|
||||||
|
if (!lastMergedContent && syncHistoryItem) {
|
||||||
|
lastMergedContent = syncedContent.historyData[syncHistoryItem[LAST_MERGED]];
|
||||||
|
}
|
||||||
|
return diffUtils.mergeContent(serverContent, clientContent, lastMergedContent);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Update content in store
|
||||||
|
store.commit('content/patchItem', {
|
||||||
|
id: `${fileId}/content`,
|
||||||
|
...mergedContent,
|
||||||
});
|
});
|
||||||
if (!lastMergedContent && syncHistoryItem) {
|
|
||||||
lastMergedContent = syncedContent.historyData[syncHistoryItem[LAST_MERGED]];
|
// Retrieve content with new `hash` value and freeze it
|
||||||
|
mergedContent = utils.deepCopy(getContent());
|
||||||
|
|
||||||
|
// Make merged content history
|
||||||
|
const mergedContentHistory = serverContent ? serverContent.history.slice() : [];
|
||||||
|
let skipUpload = true;
|
||||||
|
if (mergedContentHistory[0] !== mergedContent.hash) {
|
||||||
|
// Put merged content hash at the beginning of history
|
||||||
|
mergedContentHistory.unshift(mergedContent.hash);
|
||||||
|
// Server content is either out of sync or its history is incomplete, do upload
|
||||||
|
skipUpload = false;
|
||||||
}
|
}
|
||||||
return diffUtils.mergeContent(serverContent, clientContent, lastMergedContent);
|
if (syncHistoryItem && syncHistoryItem[0] !== mergedContent.hash) {
|
||||||
})();
|
// Clean up by removing the hash we've previously added
|
||||||
|
const idx = mergedContentHistory.indexOf(syncHistoryItem[LAST_SENT]);
|
||||||
// Update content in store
|
if (idx !== -1) {
|
||||||
store.commit('content/patchItem', {
|
mergedContentHistory.splice(idx, 1);
|
||||||
id: `${fileId}/content`,
|
}
|
||||||
...mergedContent,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Retrieve content with new `hash` value and freeze it
|
|
||||||
mergedContent = utils.deepCopy(getContent());
|
|
||||||
|
|
||||||
// Make merged content history
|
|
||||||
const mergedContentHistory = serverContent ? serverContent.history.slice() : [];
|
|
||||||
let skipUpload = true;
|
|
||||||
if (mergedContentHistory[0] !== mergedContent.hash) {
|
|
||||||
// Put merged content hash at the beginning of history
|
|
||||||
mergedContentHistory.unshift(mergedContent.hash);
|
|
||||||
// Server content is either out of sync or its history is incomplete, do upload
|
|
||||||
skipUpload = false;
|
|
||||||
}
|
|
||||||
if (syncHistoryItem && syncHistoryItem[0] !== mergedContent.hash) {
|
|
||||||
// Clean up by removing the hash we've previously added
|
|
||||||
const idx = mergedContentHistory.indexOf(syncHistoryItem[LAST_SENT]);
|
|
||||||
if (idx !== -1) {
|
|
||||||
mergedContentHistory.splice(idx, 1);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Store last sent if it's in the server history,
|
// Store last sent if it's in the server history,
|
||||||
// and merged content which will be sent if different
|
// and merged content which will be sent if different
|
||||||
const newSyncedContent = utils.deepCopy(syncedContent);
|
const newSyncedContent = utils.deepCopy(syncedContent);
|
||||||
const newSyncHistoryItem = newSyncedContent.syncHistory[syncLocation.id] || [];
|
const newSyncHistoryItem = newSyncedContent.syncHistory[syncLocation.id] || [];
|
||||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||||
if (serverContent && (serverContent.hash === newSyncHistoryItem[LAST_SENT] ||
|
if (serverContent && (serverContent.hash === newSyncHistoryItem[LAST_SENT] ||
|
||||||
serverContent.history.indexOf(newSyncHistoryItem[LAST_SENT]) !== -1)
|
serverContent.history.indexOf(newSyncHistoryItem[LAST_SENT]) !== -1)
|
||||||
) {
|
) {
|
||||||
// The server has accepted the content we previously sent
|
// The server has accepted the content we previously sent
|
||||||
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SENT];
|
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SENT];
|
||||||
}
|
|
||||||
newSyncHistoryItem[LAST_SENT] = mergedContent.hash;
|
|
||||||
newSyncedContent.historyData[mergedContent.hash] = mergedContent;
|
|
||||||
|
|
||||||
// Clean synced content from unused revisions
|
|
||||||
cleanSyncedContent(newSyncedContent);
|
|
||||||
// Store synced content
|
|
||||||
store.commit('syncedContent/patchItem', newSyncedContent);
|
|
||||||
|
|
||||||
if (skipUpload) {
|
|
||||||
// Server content and merged content are equal, skip content upload
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent from sending new content too long after old content has been fetched
|
|
||||||
const syncStartTime = Date.now();
|
|
||||||
const ifNotTooLate = cb => (res) => {
|
|
||||||
// No time to refresh a token...
|
|
||||||
if (syncStartTime + 500 < Date.now()) {
|
|
||||||
throw new Error('TOO_LATE');
|
|
||||||
}
|
}
|
||||||
return cb(res);
|
newSyncHistoryItem[LAST_SENT] = mergedContent.hash;
|
||||||
};
|
newSyncedContent.historyData[mergedContent.hash] = mergedContent;
|
||||||
|
|
||||||
// Upload merged content
|
// Clean synced content from unused revisions
|
||||||
return provider.uploadContent(token, {
|
cleanSyncedContent(newSyncedContent);
|
||||||
...mergedContent,
|
// Store synced content
|
||||||
history: mergedContentHistory,
|
store.commit('syncedContent/patchItem', newSyncedContent);
|
||||||
}, syncLocation, ifNotTooLate);
|
|
||||||
})
|
if (skipUpload) {
|
||||||
.then(() => syncOneContentLocation());
|
// Server content and merged content are equal, skip content upload
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent from sending new content too long after old content has been fetched
|
||||||
|
const syncStartTime = Date.now();
|
||||||
|
const ifNotTooLate = cb => (res) => {
|
||||||
|
// No time to refresh a token...
|
||||||
|
if (syncStartTime + 500 < Date.now()) {
|
||||||
|
throw new Error('TOO_LATE');
|
||||||
|
}
|
||||||
|
return cb(res);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Upload merged content
|
||||||
|
return provider.uploadContent(token, {
|
||||||
|
...mergedContent,
|
||||||
|
history: mergedContentHistory,
|
||||||
|
}, syncLocation, ifNotTooLate)
|
||||||
|
.then((syncLocationToStore) => {
|
||||||
|
// Replace sync location if modified
|
||||||
|
if (utils.serializeObject(syncLocation) !==
|
||||||
|
utils.serializeObject(syncLocationToStore)
|
||||||
|
) {
|
||||||
|
store.commit('syncLocation/patchItem', syncLocationToStore);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (store.state.offline) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
store.dispatch('notification/error', err);
|
||||||
|
errorLocations[syncLocation.id] = true;
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => syncOneContentLocation());
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
@ -271,11 +305,11 @@ function syncFile(fileId) {
|
||||||
|
|
||||||
function sync() {
|
function sync() {
|
||||||
const googleToken = store.getters['data/loginToken'];
|
const googleToken = store.getters['data/loginToken'];
|
||||||
return googleDriveAppDataProvider.getChanges(googleToken)
|
return mainProvider.getChanges(googleToken)
|
||||||
.then((changes) => {
|
.then((changes) => {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
applyChanges(changes);
|
applyChanges(changes);
|
||||||
googleDriveAppDataProvider.setAppliedChanges(googleToken, changes);
|
mainProvider.setAppliedChanges(googleToken, changes);
|
||||||
|
|
||||||
// Prevent from sending items too long after changes have been retrieved
|
// Prevent from sending items too long after changes have been retrieved
|
||||||
const syncStartTime = Date.now();
|
const syncStartTime = Date.now();
|
||||||
|
@ -292,6 +326,7 @@ function sync() {
|
||||||
...store.state.file.itemMap,
|
...store.state.file.itemMap,
|
||||||
...store.state.folder.itemMap,
|
...store.state.folder.itemMap,
|
||||||
...store.state.syncLocation.itemMap,
|
...store.state.syncLocation.itemMap,
|
||||||
|
...store.state.publishLocation.itemMap,
|
||||||
// Deal with contents later
|
// Deal with contents later
|
||||||
};
|
};
|
||||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||||
|
@ -300,7 +335,7 @@ function sync() {
|
||||||
const item = storeItemMap[id];
|
const item = storeItemMap[id];
|
||||||
const existingSyncData = syncDataByItemId[id];
|
const existingSyncData = syncDataByItemId[id];
|
||||||
if (!existingSyncData || existingSyncData.hash !== item.hash) {
|
if (!existingSyncData || existingSyncData.hash !== item.hash) {
|
||||||
result = googleDriveAppDataProvider.saveItem(
|
result = mainProvider.saveItem(
|
||||||
googleToken,
|
googleToken,
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
utils.deepCopy(item),
|
utils.deepCopy(item),
|
||||||
|
@ -323,7 +358,8 @@ function sync() {
|
||||||
...store.state.file.itemMap,
|
...store.state.file.itemMap,
|
||||||
...store.state.folder.itemMap,
|
...store.state.folder.itemMap,
|
||||||
...store.state.syncLocation.itemMap,
|
...store.state.syncLocation.itemMap,
|
||||||
...store.state.content.itemMap, // Deal with contents now
|
...store.state.publishLocation.itemMap,
|
||||||
|
...store.state.content.itemMap,
|
||||||
};
|
};
|
||||||
const syncData = store.getters['data/syncData'];
|
const syncData = store.getters['data/syncData'];
|
||||||
let result;
|
let result;
|
||||||
|
@ -335,7 +371,7 @@ function sync() {
|
||||||
) {
|
) {
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||||
result = googleDriveAppDataProvider
|
result = mainProvider
|
||||||
.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||||
|
@ -350,10 +386,7 @@ function sync() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const getOneFileIdToSync = () => {
|
const getOneFileIdToSync = () => {
|
||||||
const allContentIds = Object.keys({
|
const allContentIds = Object.keys(localDbSvc.hashMap.content);
|
||||||
...store.state.content.itemMap,
|
|
||||||
...store.getters['data/syncDataByType'].content,
|
|
||||||
});
|
|
||||||
let fileId;
|
let fileId;
|
||||||
allContentIds.some((contentId) => {
|
allContentIds.some((contentId) => {
|
||||||
// Get content hash from itemMap or from localDbSvc if not loaded
|
// Get content hash from itemMap or from localDbSvc if not loaded
|
||||||
|
@ -361,7 +394,7 @@ function sync() {
|
||||||
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
const hash = loadedContent ? loadedContent.hash : localDbSvc.hashMap.content[contentId];
|
||||||
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
const syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||||
// Sync if item hash and syncData hash are different
|
// Sync if item hash and syncData hash are different
|
||||||
if (!hash || !syncData || hash !== syncData.hash) {
|
if (!syncData || hash !== syncData.hash) {
|
||||||
[fileId] = contentId.split('/');
|
[fileId] = contentId.split('/');
|
||||||
}
|
}
|
||||||
return fileId;
|
return fileId;
|
||||||
|
@ -401,7 +434,7 @@ function requestSync() {
|
||||||
let intervalId;
|
let intervalId;
|
||||||
const attempt = () => {
|
const attempt = () => {
|
||||||
// Only start syncing when these conditions are met
|
// Only start syncing when these conditions are met
|
||||||
if (userActivitySvc.isActive() && isSyncWindow()) {
|
if (utils.isUserActive() && isSyncWindow()) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
if (!isSyncPossible()) {
|
if (!isSyncPossible()) {
|
||||||
// Cancel sync
|
// Cancel sync
|
||||||
|
@ -422,6 +455,9 @@ function requestSync() {
|
||||||
return sync();
|
return sync();
|
||||||
}
|
}
|
||||||
if (hasCurrentFileSyncLocations()) {
|
if (hasCurrentFileSyncLocations()) {
|
||||||
|
// Only sync current file if data sync is unavailable.
|
||||||
|
// We also could sync files that are out-of-sync but it would
|
||||||
|
// require to load the syncedContent objects of all files.
|
||||||
return syncFile(store.getters['file/current'].id);
|
return syncFile(store.getters['file/current'].id);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -437,7 +473,7 @@ function requestSync() {
|
||||||
// Sync periodically
|
// Sync periodically
|
||||||
utils.setInterval(() => {
|
utils.setInterval(() => {
|
||||||
if (isSyncPossible() &&
|
if (isSyncPossible() &&
|
||||||
userActivitySvc.isActive() &&
|
utils.isUserActive() &&
|
||||||
isSyncWindow() &&
|
isSyncWindow() &&
|
||||||
isAutoSyncReady()
|
isAutoSyncReady()
|
||||||
) {
|
) {
|
||||||
|
@ -508,4 +544,5 @@ utils.setInterval(() => {
|
||||||
export default {
|
export default {
|
||||||
isSyncPossible,
|
isSyncPossible,
|
||||||
requestSync,
|
requestSync,
|
||||||
|
createSyncLocation,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
const inactiveAfter = 2 * 60 * 1000; // 2 minutes
|
|
||||||
let lastActivity;
|
|
||||||
let lastFocus;
|
|
||||||
const lastFocusKey = 'lastWindowFocus';
|
|
||||||
|
|
||||||
function setLastActivity() {
|
|
||||||
lastActivity = Date.now();
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLastFocus() {
|
|
||||||
lastFocus = Date.now();
|
|
||||||
localStorage[lastFocusKey] = lastFocus;
|
|
||||||
setLastActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
setLastFocus();
|
|
||||||
window.addEventListener('focus', setLastFocus);
|
|
||||||
window.document.addEventListener('mousedown', setLastActivity);
|
|
||||||
window.document.addEventListener('keydown', setLastActivity);
|
|
||||||
|
|
||||||
export default {
|
|
||||||
isFocused() {
|
|
||||||
return parseInt(localStorage[lastFocusKey], 10) === lastFocus;
|
|
||||||
},
|
|
||||||
isActive() {
|
|
||||||
return lastActivity > Date.now() - inactiveAfter && this.isFocused();
|
|
||||||
},
|
|
||||||
};
|
|
|
@ -1,14 +1,35 @@
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
// For sortObject
|
const workspaceId = 'main';
|
||||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
|
||||||
|
|
||||||
// For uid()
|
// For uid()
|
||||||
|
const uidLength = 16;
|
||||||
const crypto = window.crypto || window.msCrypto;
|
const crypto = window.crypto || window.msCrypto;
|
||||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||||
const radix = alphabet.length;
|
const radix = alphabet.length;
|
||||||
const array = new Uint32Array(16);
|
const array = new Uint32Array(uidLength);
|
||||||
|
|
||||||
|
// For isUserActive
|
||||||
|
const inactiveAfter = 2 * 60 * 1000; // 2 minutes
|
||||||
|
let lastActivity;
|
||||||
|
const setLastActivity = () => {
|
||||||
|
lastActivity = Date.now();
|
||||||
|
};
|
||||||
|
window.document.addEventListener('mousedown', setLastActivity);
|
||||||
|
window.document.addEventListener('keydown', setLastActivity);
|
||||||
|
window.document.addEventListener('touchstart', setLastActivity);
|
||||||
|
|
||||||
|
// For isWindowFocused
|
||||||
|
let lastFocus;
|
||||||
|
const lastFocusKey = `${workspaceId}/lastWindowFocus`;
|
||||||
|
const setLastFocus = () => {
|
||||||
|
lastFocus = Date.now();
|
||||||
|
localStorage[lastFocusKey] = lastFocus;
|
||||||
|
setLastActivity();
|
||||||
|
};
|
||||||
|
setLastFocus();
|
||||||
|
window.addEventListener('focus', setLastFocus);
|
||||||
|
|
||||||
// For addQueryParams()
|
// For addQueryParams()
|
||||||
const urlParser = window.document.createElement('a');
|
const urlParser = window.document.createElement('a');
|
||||||
|
@ -17,8 +38,18 @@ const urlParser = window.document.createElement('a');
|
||||||
const scriptLoadingPromises = Object.create(null);
|
const scriptLoadingPromises = Object.create(null);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
workspaceId,
|
||||||
origin: `${location.protocol}//${location.host}`,
|
origin: `${location.protocol}//${location.host}`,
|
||||||
types: ['contentState', 'syncedContent', 'content', 'file', 'folder', 'syncLocation', 'data'],
|
types: [
|
||||||
|
'contentState',
|
||||||
|
'syncedContent',
|
||||||
|
'content',
|
||||||
|
'file',
|
||||||
|
'folder',
|
||||||
|
'syncLocation',
|
||||||
|
'publishLocation',
|
||||||
|
'data',
|
||||||
|
],
|
||||||
deepCopy(obj) {
|
deepCopy(obj) {
|
||||||
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
||||||
},
|
},
|
||||||
|
@ -34,15 +65,6 @@ export default {
|
||||||
}, {});
|
}, {});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
sortObject(obj, sortFunc = key => key) {
|
|
||||||
const result = {};
|
|
||||||
const compare = (key1, key2) => collator.compare(
|
|
||||||
sortFunc(key1, obj[key1]), sortFunc(key2, obj[key2]));
|
|
||||||
Object.keys(obj).sort(compare).forEach((key) => {
|
|
||||||
result[key] = obj[key];
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
},
|
|
||||||
uid() {
|
uid() {
|
||||||
crypto.getRandomValues(array);
|
crypto.getRandomValues(array);
|
||||||
return array.cl_map(value => alphabet[value % radix]).join('');
|
return array.cl_map(value => alphabet[value % radix]).join('');
|
||||||
|
@ -84,6 +106,12 @@ export default {
|
||||||
setInterval(func, interval) {
|
setInterval(func, interval) {
|
||||||
return setInterval(() => func(), this.randomize(interval));
|
return setInterval(() => func(), this.randomize(interval));
|
||||||
},
|
},
|
||||||
|
isWindowFocused() {
|
||||||
|
return parseInt(localStorage[lastFocusKey], 10) === lastFocus;
|
||||||
|
},
|
||||||
|
isUserActive() {
|
||||||
|
return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused();
|
||||||
|
},
|
||||||
addQueryParams(url = '', params = {}) {
|
addQueryParams(url = '', params = {}) {
|
||||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
|
@ -166,7 +194,7 @@ export default {
|
||||||
event.data.state === state
|
event.data.state === state
|
||||||
) {
|
) {
|
||||||
oauth2Context.clean();
|
oauth2Context.clean();
|
||||||
if (event.data.accessToken) {
|
if (event.data.accessToken || event.data.code) {
|
||||||
resolve(event.data);
|
resolve(event.data);
|
||||||
} else {
|
} else {
|
||||||
reject(event.data);
|
reject(event.data);
|
||||||
|
@ -259,7 +287,7 @@ export default {
|
||||||
|
|
||||||
// Add query params to URL
|
// Add query params to URL
|
||||||
const url = this.addQueryParams(config.url, config.params);
|
const url = this.addQueryParams(config.url, config.params);
|
||||||
xhr.open(config.method, url);
|
xhr.open(config.method || 'GET', url);
|
||||||
Object.keys(config.headers).forEach((key) => {
|
Object.keys(config.headers).forEach((key) => {
|
||||||
xhr.setRequestHeader(key, config.headers[key]);
|
xhr.setRequestHeader(key, config.headers[key]);
|
||||||
});
|
});
|
||||||
|
@ -281,4 +309,24 @@ export default {
|
||||||
|
|
||||||
return attempt();
|
return attempt();
|
||||||
},
|
},
|
||||||
|
checkOnline() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
let timeout;
|
||||||
|
const cleaner = (cb, res) => () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
cb(res);
|
||||||
|
document.head.removeChild(script);
|
||||||
|
};
|
||||||
|
script.onload = cleaner(resolve, 'Online.');
|
||||||
|
script.onerror = cleaner(reject, 'Offline.');
|
||||||
|
script.src = `https://apis.google.com/js/api.js?${Date.now()}`;
|
||||||
|
try {
|
||||||
|
document.head.appendChild(script); // This can fail with bad network
|
||||||
|
timeout = setTimeout(cleaner(reject), 15000); // 15 sec
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -7,11 +7,13 @@ import syncedContent from './modules/syncedContent';
|
||||||
import content from './modules/content';
|
import content from './modules/content';
|
||||||
import file from './modules/file';
|
import file from './modules/file';
|
||||||
import folder from './modules/folder';
|
import folder from './modules/folder';
|
||||||
|
import publishLocation from './modules/publishLocation';
|
||||||
import syncLocation from './modules/syncLocation';
|
import syncLocation from './modules/syncLocation';
|
||||||
import data from './modules/data';
|
import data from './modules/data';
|
||||||
import layout from './modules/layout';
|
import layout from './modules/layout';
|
||||||
import explorer from './modules/explorer';
|
import explorer from './modules/explorer';
|
||||||
import modal from './modules/modal';
|
import modal from './modules/modal';
|
||||||
|
import notification from './modules/notification';
|
||||||
import queue from './modules/queue';
|
import queue from './modules/queue';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
@ -44,21 +46,41 @@ const store = new Vuex.Store({
|
||||||
content,
|
content,
|
||||||
file,
|
file,
|
||||||
folder,
|
folder,
|
||||||
|
publishLocation,
|
||||||
syncLocation,
|
syncLocation,
|
||||||
data,
|
data,
|
||||||
layout,
|
layout,
|
||||||
explorer,
|
explorer,
|
||||||
modal,
|
modal,
|
||||||
|
notification,
|
||||||
queue,
|
queue,
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: debug ? [createLogger()] : [],
|
plugins: debug ? [createLogger()] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let isConnectionDown = false;
|
||||||
|
let lastConnectionCheck = 0;
|
||||||
|
|
||||||
function checkOffline() {
|
function checkOffline() {
|
||||||
const isOffline = window.navigator.onLine === false;
|
const isBrowserOffline = window.navigator.onLine === false;
|
||||||
|
if (!isBrowserOffline && lastConnectionCheck + 30000 < Date.now() && utils.isUserActive()) {
|
||||||
|
lastConnectionCheck = Date.now();
|
||||||
|
utils.checkOnline()
|
||||||
|
.then(() => {
|
||||||
|
isConnectionDown = false;
|
||||||
|
}, () => {
|
||||||
|
isConnectionDown = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const isOffline = isBrowserOffline || isConnectionDown;
|
||||||
if (isOffline !== store.state.offline) {
|
if (isOffline !== store.state.offline) {
|
||||||
store.commit('setOffline', isOffline);
|
store.commit('setOffline', isOffline);
|
||||||
|
if (isOffline) {
|
||||||
|
store.dispatch('notification/info', 'You are offline.');
|
||||||
|
} else {
|
||||||
|
store.dispatch('notification/info', 'You are back online!');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
utils.setInterval(checkOffline, 1000);
|
utils.setInterval(checkOffline, 1000);
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import Vue from 'vue';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
@ -5,6 +6,7 @@ import defaultSettings from '../../data/defaultSettings.yml';
|
||||||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||||
|
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
||||||
|
|
||||||
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
||||||
|
|
||||||
|
@ -20,6 +22,23 @@ const empty = (id) => {
|
||||||
};
|
};
|
||||||
const module = moduleTemplate(empty, true);
|
const module = moduleTemplate(empty, true);
|
||||||
|
|
||||||
|
module.mutations.setItem = (state, value) => {
|
||||||
|
const emptyItem = empty(value.id);
|
||||||
|
let itemData = value.data || emptyItem.data;
|
||||||
|
if (typeof itemData === 'object') {
|
||||||
|
itemData = Object.assign(emptyItem.data, value.data);
|
||||||
|
}
|
||||||
|
const item = {
|
||||||
|
...emptyItem,
|
||||||
|
...value,
|
||||||
|
data: itemData,
|
||||||
|
};
|
||||||
|
if (!item.hash) {
|
||||||
|
item.hash = Date.now();
|
||||||
|
}
|
||||||
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
};
|
||||||
|
|
||||||
const getter = id => state => (state.itemMap[id] || empty(id)).data;
|
const getter = id => state => (state.itemMap[id] || empty(id)).data;
|
||||||
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
||||||
const patcher = id => ({ state, commit }, data) => {
|
const patcher = id => ({ state, commit }, data) => {
|
||||||
|
@ -86,8 +105,10 @@ const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
||||||
isAdditional: true,
|
isAdditional: true,
|
||||||
});
|
});
|
||||||
const additionalTemplates = {
|
const additionalTemplates = {
|
||||||
|
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||||
|
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
||||||
};
|
};
|
||||||
module.getters.allTemplates = (state, getters) => ({
|
module.getters.allTemplates = (state, getters) => ({
|
||||||
...getters.templates,
|
...getters.templates,
|
||||||
|
@ -154,6 +175,8 @@ module.actions.setSyncData = setter('syncData');
|
||||||
// Tokens
|
// Tokens
|
||||||
module.getters.tokens = getter('tokens');
|
module.getters.tokens = getter('tokens');
|
||||||
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
||||||
|
module.getters.dropboxTokens = (state, getters) => getters.tokens.dropbox || {};
|
||||||
|
module.getters.githubTokens = (state, getters) => getters.tokens.github || {};
|
||||||
module.getters.loginToken = (state, getters) => {
|
module.getters.loginToken = (state, getters) => {
|
||||||
// Return the first google token that has the isLogin flag
|
// Return the first google token that has the isLogin flag
|
||||||
const googleTokens = getters.googleTokens;
|
const googleTokens = getters.googleTokens;
|
||||||
|
@ -170,5 +193,21 @@ module.actions.setGoogleToken = ({ getters, dispatch }, token) => {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
module.actions.setDropboxToken = ({ getters, dispatch }, token) => {
|
||||||
|
dispatch('patchTokens', {
|
||||||
|
dropbox: {
|
||||||
|
...getters.dropboxTokens,
|
||||||
|
[token.sub]: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
module.actions.setGithubToken = ({ getters, dispatch }, token) => {
|
||||||
|
dispatch('patchTokens', {
|
||||||
|
github: {
|
||||||
|
...getters.githubTokens,
|
||||||
|
[token.sub]: token,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export default module;
|
export default module;
|
||||||
|
|
|
@ -18,8 +18,9 @@ const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||||
const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name);
|
const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name);
|
||||||
|
|
||||||
class Node {
|
class Node {
|
||||||
constructor(item, isFolder, isRoot) {
|
constructor(item, locations = [], isFolder = false, isRoot = false) {
|
||||||
this.item = item;
|
this.item = item;
|
||||||
|
this.locations = locations;
|
||||||
this.isFolder = isFolder;
|
this.isFolder = isFolder;
|
||||||
this.isRoot = isRoot;
|
this.isRoot = isRoot;
|
||||||
if (isFolder) {
|
if (isFolder) {
|
||||||
|
@ -69,12 +70,16 @@ export default {
|
||||||
nodeStructure: (state, getters, rootState, rootGetters) => {
|
nodeStructure: (state, getters, rootState, rootGetters) => {
|
||||||
const nodeMap = {};
|
const nodeMap = {};
|
||||||
rootGetters['folder/items'].forEach((item) => {
|
rootGetters['folder/items'].forEach((item) => {
|
||||||
nodeMap[item.id] = new Node(item, true);
|
nodeMap[item.id] = new Node(item, [], true);
|
||||||
});
|
});
|
||||||
rootGetters['file/items'].forEach((item) => {
|
rootGetters['file/items'].forEach((item) => {
|
||||||
nodeMap[item.id] = new Node(item);
|
const locations = [
|
||||||
|
...rootGetters['syncLocation/groupedByFileId'][item.id] || [],
|
||||||
|
...rootGetters['publishLocation/groupedByFileId'][item.id] || [],
|
||||||
|
];
|
||||||
|
nodeMap[item.id] = new Node(item, locations);
|
||||||
});
|
});
|
||||||
const rootNode = new Node(emptyFolder(), true, true);
|
const rootNode = new Node(emptyFolder(), [], true, true);
|
||||||
Object.keys(nodeMap).forEach((id) => {
|
Object.keys(nodeMap).forEach((id) => {
|
||||||
const node = nodeMap[id];
|
const node = nodeMap[id];
|
||||||
let parentNode = nodeMap[node.item.parentId];
|
let parentNode = nodeMap[node.item.parentId];
|
||||||
|
@ -121,7 +126,7 @@ export default {
|
||||||
setDragSourceId: setter('dragSourceId'),
|
setDragSourceId: setter('dragSourceId'),
|
||||||
setDragTargetId: setter('dragTargetId'),
|
setDragTargetId: setter('dragTargetId'),
|
||||||
setNewItem(state, item) {
|
setNewItem(state, item) {
|
||||||
state.newChildNode = item ? new Node(item, item.type === 'folder') : nilFileNode;
|
state.newChildNode = item ? new Node(item, [], item.type === 'folder') : nilFileNode;
|
||||||
},
|
},
|
||||||
setNewItemName(state, name) {
|
setNewItemName(state, name) {
|
||||||
state.newChildNode.item.name = name;
|
state.newChildNode.item.name = name;
|
||||||
|
|
|
@ -2,8 +2,13 @@ const editorMinWidth = 320;
|
||||||
const minPadding = 20;
|
const minPadding = 20;
|
||||||
const previewButtonWidth = 55;
|
const previewButtonWidth = 55;
|
||||||
const editorTopPadding = 10;
|
const editorTopPadding = 10;
|
||||||
const navigationBarSpaceWidth = 30;
|
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
|
||||||
const navigationBarLeftWidth = 570;
|
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||||
|
const navigationBarRightButtonWidth = 38 + 8;
|
||||||
|
const navigationBarSpinnerWidth = 18 + 15 + 5; // 5 for left margin
|
||||||
|
const navigationBarLocationWidth = 20;
|
||||||
|
const navigationBarSyncPublishButtonsWidth = 36 + 10;
|
||||||
|
const navigationBarTitleMargin = 8;
|
||||||
const maxTitleMaxWidth = 800;
|
const maxTitleMaxWidth = 800;
|
||||||
const minTitleMaxWidth = 200;
|
const minTitleMaxWidth = 200;
|
||||||
|
|
||||||
|
@ -15,7 +20,7 @@ const constants = {
|
||||||
statusBarHeight: 20,
|
statusBarHeight: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
function computeStyles(state, computedSettings, localSettings, styles = {
|
function computeStyles(state, localSettings, getters, styles = {
|
||||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||||
showStatusBar: localSettings.showStatusBar,
|
showStatusBar: localSettings.showStatusBar,
|
||||||
showEditor: localSettings.showEditor,
|
showEditor: localSettings.showEditor,
|
||||||
|
@ -49,9 +54,10 @@ function computeStyles(state, computedSettings, localSettings, styles = {
|
||||||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||||
styles.showSidePreview = false;
|
styles.showSidePreview = false;
|
||||||
styles.showPreview = false;
|
styles.showPreview = false;
|
||||||
return computeStyles(state, computedSettings, localSettings, styles);
|
return computeStyles(state, localSettings, getters, styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const computedSettings = getters['data/computedSettings'];
|
||||||
styles.fontSize = 18;
|
styles.fontSize = 18;
|
||||||
styles.textWidth = 990;
|
styles.textWidth = 990;
|
||||||
if (doublePanelWidth < 1120) {
|
if (doublePanelWidth < 1120) {
|
||||||
|
@ -89,9 +95,23 @@ function computeStyles(state, computedSettings, localSettings, styles = {
|
||||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||||
|
|
||||||
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
|
styles.titleMaxWidth = styles.innerWidth;
|
||||||
if (styles.showEditor) {
|
if (styles.showEditor) {
|
||||||
styles.titleMaxWidth -= navigationBarLeftWidth;
|
const syncLocations = getters['syncLocation/current'];
|
||||||
|
const publishLocations = getters['publishLocation/current'];
|
||||||
|
const isSyncPossible = getters['data/loginToken'] || syncLocations.length;
|
||||||
|
styles.titleMaxWidth = styles.innerWidth -
|
||||||
|
navigationBarEditButtonsWidth -
|
||||||
|
navigationBarLeftButtonWidth -
|
||||||
|
navigationBarRightButtonWidth -
|
||||||
|
navigationBarSpinnerWidth -
|
||||||
|
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
||||||
|
(isSyncPossible ? navigationBarSyncPublishButtonsWidth : 0) -
|
||||||
|
(publishLocations.length ? navigationBarSyncPublishButtonsWidth : 0) -
|
||||||
|
navigationBarTitleMargin;
|
||||||
|
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
||||||
|
styles.hideLocations = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
||||||
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
||||||
|
@ -113,9 +133,8 @@ export default {
|
||||||
getters: {
|
getters: {
|
||||||
constants: () => constants,
|
constants: () => constants,
|
||||||
styles: (state, getters, rootState, rootGetters) => {
|
styles: (state, getters, rootState, rootGetters) => {
|
||||||
const computedSettings = rootGetters['data/computedSettings'];
|
|
||||||
const localSettings = rootGetters['data/localSettings'];
|
const localSettings = rootGetters['data/localSettings'];
|
||||||
return computeStyles(state, computedSettings, localSettings);
|
return computeStyles(state, localSettings, rootGetters);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,34 +1,47 @@
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
config: null,
|
stack: [],
|
||||||
|
hidden: false,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setConfig: (state, value) => {
|
setStack: (state, value) => {
|
||||||
state.config = value;
|
state.stack = value;
|
||||||
|
},
|
||||||
|
setHidden: (state, value) => {
|
||||||
|
state.hidden = value;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
getters: {
|
||||||
|
config: state => !state.hidden && state.stack[0],
|
||||||
|
},
|
||||||
actions: {
|
actions: {
|
||||||
open({ commit }, param) {
|
open({ commit, state }, param) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let config = param;
|
const config = typeof param === 'object' ? { ...param } : { type: param };
|
||||||
if (typeof config === 'string') {
|
const clean = () => commit('setStack', state.stack.filter((otherConfig => otherConfig !== config)));
|
||||||
config = {
|
|
||||||
type: config,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
config.resolve = (result) => {
|
config.resolve = (result) => {
|
||||||
if (config.onResolve) {
|
if (config.onResolve) {
|
||||||
config.onResolve(result);
|
config.onResolve(result);
|
||||||
}
|
}
|
||||||
commit('setConfig');
|
clean();
|
||||||
resolve(result);
|
resolve(result);
|
||||||
};
|
};
|
||||||
config.reject = (error) => {
|
config.reject = (error) => {
|
||||||
commit('setConfig');
|
clean();
|
||||||
reject(error);
|
reject(error);
|
||||||
};
|
};
|
||||||
commit('setConfig', config);
|
commit('setStack', [config, ...state.stack]);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
hideUntil({ commit, state }, promise) {
|
||||||
|
commit('setHidden', true);
|
||||||
|
return promise.then((res) => {
|
||||||
|
commit('setHidden', false);
|
||||||
|
return res;
|
||||||
|
}, (err) => {
|
||||||
|
commit('setHidden', false);
|
||||||
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
notImplemented: ({ dispatch }) => dispatch('open', {
|
notImplemented: ({ dispatch }) => dispatch('open', {
|
||||||
|
|
49
src/store/modules/notification.js
Normal file
49
src/store/modules/notification.js
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
const itemTimeout = 5000;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
items: [],
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setItems: (state, value) => {
|
||||||
|
state.items = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
info({ state, commit }, info) {
|
||||||
|
const item = {
|
||||||
|
type: 'info',
|
||||||
|
content: info,
|
||||||
|
};
|
||||||
|
commit('setItems', [item, ...state.items]);
|
||||||
|
setTimeout(() =>
|
||||||
|
commit('setItems', state.items.filter(otherItem => otherItem !== item)), itemTimeout);
|
||||||
|
},
|
||||||
|
error({ state, commit, rootState }, error) {
|
||||||
|
const item = {
|
||||||
|
type: 'error',
|
||||||
|
};
|
||||||
|
if (error) {
|
||||||
|
if (error.message) {
|
||||||
|
item.content = error.message;
|
||||||
|
} else if (error.status) {
|
||||||
|
const location = rootState.queue.currentLocation;
|
||||||
|
if (location.providerId) {
|
||||||
|
item.content = `HTTP error ${error.status} on ${location.providerId} location.`;
|
||||||
|
} else {
|
||||||
|
item.content = `HTTP error ${error.status}.`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.content = error.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!item.content || item.content === '[object Object]') {
|
||||||
|
item.content = 'Unknown error.';
|
||||||
|
}
|
||||||
|
commit('setItems', [item, ...state.items]);
|
||||||
|
setTimeout(() =>
|
||||||
|
commit('setItems', state.items.filter(otherItem => otherItem !== item)), itemTimeout);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
34
src/store/modules/publishLocation.js
Normal file
34
src/store/modules/publishLocation.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import moduleTemplate from './moduleTemplate';
|
||||||
|
import empty from '../../data/emptyPublishLocation';
|
||||||
|
import providerRegistry from '../../services/providers/providerRegistry';
|
||||||
|
|
||||||
|
const module = moduleTemplate(empty);
|
||||||
|
|
||||||
|
module.getters = {
|
||||||
|
...module.getters,
|
||||||
|
groupedByFileId: (state, getters) => {
|
||||||
|
const result = {};
|
||||||
|
getters.items.forEach((item) => {
|
||||||
|
// Filter items that we can't use
|
||||||
|
if (providerRegistry.providers[item.providerId].getToken(item)) {
|
||||||
|
const list = result[item.fileId] || [];
|
||||||
|
list.push(item);
|
||||||
|
result[item.fileId] = list;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
current: (state, getters, rootState, rootGetters) => {
|
||||||
|
const locations = getters.groupedByFileId[rootGetters['file/current'].id] || [];
|
||||||
|
return locations.map((location) => {
|
||||||
|
const provider = providerRegistry.providers[location.providerId];
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
description: provider.getDescription(location),
|
||||||
|
url: provider.getUrl(location),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default module;
|
|
@ -9,26 +9,41 @@ export default {
|
||||||
state: {
|
state: {
|
||||||
isEmpty: true,
|
isEmpty: true,
|
||||||
isSyncRequested: false,
|
isSyncRequested: false,
|
||||||
|
isPublishRequested: false,
|
||||||
|
currentLocation: {},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setIsEmpty: setter('isEmpty'),
|
setIsEmpty: setter('isEmpty'),
|
||||||
setIsSyncRequested: setter('isSyncRequested'),
|
setIsSyncRequested: setter('isSyncRequested'),
|
||||||
|
setIsPublishRequested: setter('isPublishRequested'),
|
||||||
|
setCurrentLocation: setter('currentLocation'),
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
enqueue({ state, commit }, cb) {
|
enqueue({ state, commit, dispatch }, cb) {
|
||||||
|
const checkOffline = () => {
|
||||||
|
if (state.offline) {
|
||||||
|
// Empty queue
|
||||||
|
queue = Promise.resolve();
|
||||||
|
commit('setIsEmpty', true);
|
||||||
|
throw new Error('offline');
|
||||||
|
}
|
||||||
|
};
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
commit('setIsEmpty', false);
|
commit('setIsEmpty', false);
|
||||||
}
|
}
|
||||||
const newQueue = queue
|
const newQueue = queue
|
||||||
.then(cb)
|
.then(() => checkOffline())
|
||||||
.catch((err) => {
|
.then(() => cb()
|
||||||
console.error(err);
|
.catch((err) => {
|
||||||
})
|
console.error(err); // eslint-disable-line no-console
|
||||||
.then(() => {
|
checkOffline();
|
||||||
if (newQueue === queue) {
|
dispatch('notification/error', err, { root: true });
|
||||||
commit('setIsEmpty', true);
|
})
|
||||||
}
|
.then(() => {
|
||||||
});
|
if (newQueue === queue) {
|
||||||
|
commit('setIsEmpty', true);
|
||||||
|
}
|
||||||
|
}));
|
||||||
queue = newQueue;
|
queue = newQueue;
|
||||||
},
|
},
|
||||||
enqueueSyncRequest({ state, commit, dispatch }, cb) {
|
enqueueSyncRequest({ state, commit, dispatch }, cb) {
|
||||||
|
@ -41,5 +56,26 @@ export default {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
enqueuePublishRequest({ state, commit, dispatch }, cb) {
|
||||||
|
if (!state.isSyncRequested) {
|
||||||
|
commit('setIsPublishRequested', true);
|
||||||
|
const unset = () => commit('setIsPublishRequested', false);
|
||||||
|
dispatch('enqueue', () => cb().then(unset, (err) => {
|
||||||
|
unset();
|
||||||
|
throw err;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
doWithLocation({ state, commit, dispatch }, { location, promise }) {
|
||||||
|
commit('setCurrentLocation', location);
|
||||||
|
return promise
|
||||||
|
.then((res) => {
|
||||||
|
commit('setCurrentLocation', {});
|
||||||
|
return res;
|
||||||
|
}, (err) => {
|
||||||
|
commit('setCurrentLocation', {});
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import moduleTemplate from './moduleTemplate';
|
import moduleTemplate from './moduleTemplate';
|
||||||
import empty from '../../data/emptySyncLocation';
|
import empty from '../../data/emptySyncLocation';
|
||||||
|
import providerRegistry from '../../services/providers/providerRegistry';
|
||||||
|
|
||||||
const module = moduleTemplate(empty);
|
const module = moduleTemplate(empty);
|
||||||
|
|
||||||
|
@ -8,14 +9,26 @@ module.getters = {
|
||||||
groupedByFileId: (state, getters) => {
|
groupedByFileId: (state, getters) => {
|
||||||
const result = {};
|
const result = {};
|
||||||
getters.items.forEach((item) => {
|
getters.items.forEach((item) => {
|
||||||
const list = result[item.fileId] || [];
|
// Filter items that we can't use
|
||||||
list.push(item);
|
if (providerRegistry.providers[item.providerId].getToken(item)) {
|
||||||
result[item.fileId] = list;
|
const list = result[item.fileId] || [];
|
||||||
|
list.push(item);
|
||||||
|
result[item.fileId] = list;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
current: (state, getters, rootState, rootGetters) =>
|
current: (state, getters, rootState, rootGetters) => {
|
||||||
getters.groupedByFileId[rootGetters['file/current'].id] || [],
|
const locations = getters.groupedByFileId[rootGetters['file/current'].id] || [];
|
||||||
|
return locations.map((location) => {
|
||||||
|
const provider = providerRegistry.providers[location.providerId];
|
||||||
|
return {
|
||||||
|
...location,
|
||||||
|
description: provider.getDescription(location),
|
||||||
|
url: provider.getUrl(location),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default module;
|
export default module;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<script>
|
<script>
|
||||||
var state;
|
var state;
|
||||||
var accessToken;
|
var accessToken;
|
||||||
|
var code;
|
||||||
var expiresIn;
|
var expiresIn;
|
||||||
function parse(search) {
|
function parse(search) {
|
||||||
(search || '').slice(1).split('&').forEach(function (param) {
|
(search || '').slice(1).split('&').forEach(function (param) {
|
||||||
|
@ -14,6 +15,8 @@
|
||||||
state = value;
|
state = value;
|
||||||
} else if (key === 'access_token') {
|
} else if (key === 'access_token') {
|
||||||
accessToken = value;
|
accessToken = value;
|
||||||
|
} else if (key === 'code') {
|
||||||
|
code = value;
|
||||||
} else if (key === 'expires_in') {
|
} else if (key === 'expires_in') {
|
||||||
expiresIn = value;
|
expiresIn = value;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +28,7 @@
|
||||||
(window.opener || window.parent).postMessage({
|
(window.opener || window.parent).postMessage({
|
||||||
state: state,
|
state: state,
|
||||||
accessToken: accessToken,
|
accessToken: accessToken,
|
||||||
|
code: code,
|
||||||
expiresIn: expiresIn
|
expiresIn: expiresIn
|
||||||
}, origin);
|
}, origin);
|
||||||
</script>
|
</script>
|
||||||
|
|
135
yarn.lock
135
yarn.lock
|
@ -55,7 +55,7 @@ ajv@^4.11.2, ajv@^4.7.0, ajv@^4.9.1:
|
||||||
co "^4.6.0"
|
co "^4.6.0"
|
||||||
json-stable-stringify "^1.0.1"
|
json-stable-stringify "^1.0.1"
|
||||||
|
|
||||||
ajv@^5.0.0:
|
ajv@^5.0.0, ajv@^5.1.0:
|
||||||
version "5.2.2"
|
version "5.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
|
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -243,7 +243,11 @@ aws-sign2@~0.6.0:
|
||||||
version "0.6.0"
|
version "0.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.6.0.tgz#14342dd38dbcc94d0e5b87d763cd63612c0e794f"
|
||||||
|
|
||||||
aws4@^1.2.1:
|
aws-sign2@~0.7.0:
|
||||||
|
version "0.7.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8"
|
||||||
|
|
||||||
|
aws4@^1.2.1, aws4@^1.6.0:
|
||||||
version "1.6.0"
|
version "1.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||||
|
|
||||||
|
@ -889,6 +893,18 @@ boom@2.x.x:
|
||||||
dependencies:
|
dependencies:
|
||||||
hoek "2.x.x"
|
hoek "2.x.x"
|
||||||
|
|
||||||
|
boom@4.x.x:
|
||||||
|
version "4.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/boom/-/boom-4.3.1.tgz#4f8a3005cb4a7e3889f749030fd25b96e01d2e31"
|
||||||
|
dependencies:
|
||||||
|
hoek "4.x.x"
|
||||||
|
|
||||||
|
boom@5.x.x:
|
||||||
|
version "5.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/boom/-/boom-5.2.0.tgz#5dd9da6ee3a5f302077436290cb717d3f4a54e02"
|
||||||
|
dependencies:
|
||||||
|
hoek "4.x.x"
|
||||||
|
|
||||||
brace-expansion@^1.0.0:
|
brace-expansion@^1.0.0:
|
||||||
version "1.1.8"
|
version "1.1.8"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||||
|
@ -1443,6 +1459,12 @@ cryptiles@2.x.x:
|
||||||
dependencies:
|
dependencies:
|
||||||
boom "2.x.x"
|
boom "2.x.x"
|
||||||
|
|
||||||
|
cryptiles@3.x.x:
|
||||||
|
version "3.1.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe"
|
||||||
|
dependencies:
|
||||||
|
boom "5.x.x"
|
||||||
|
|
||||||
crypto-browserify@^3.11.0:
|
crypto-browserify@^3.11.0:
|
||||||
version "3.11.0"
|
version "3.11.0"
|
||||||
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
|
resolved "https://registry.yarnpkg.com/crypto-browserify/-/crypto-browserify-3.11.0.tgz#3652a0906ab9b2a7e0c3ce66a408e957a2485522"
|
||||||
|
@ -2222,7 +2244,7 @@ express@^4.14.1, express@^4.15.2:
|
||||||
utils-merge "1.0.0"
|
utils-merge "1.0.0"
|
||||||
vary "~1.1.1"
|
vary "~1.1.1"
|
||||||
|
|
||||||
extend@^3.0.0, extend@~3.0.0:
|
extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
|
||||||
|
|
||||||
|
@ -2419,6 +2441,14 @@ form-data@~2.1.1:
|
||||||
combined-stream "^1.0.5"
|
combined-stream "^1.0.5"
|
||||||
mime-types "^2.1.12"
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
|
form-data@~2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf"
|
||||||
|
dependencies:
|
||||||
|
asynckit "^0.4.0"
|
||||||
|
combined-stream "^1.0.5"
|
||||||
|
mime-types "^2.1.12"
|
||||||
|
|
||||||
forwarded@~0.1.0:
|
forwarded@~0.1.0:
|
||||||
version "0.1.0"
|
version "0.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363"
|
||||||
|
@ -2778,6 +2808,10 @@ har-schema@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
|
||||||
|
|
||||||
|
har-schema@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
|
||||||
|
|
||||||
har-validator@~4.2.1:
|
har-validator@~4.2.1:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
|
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
|
||||||
|
@ -2785,6 +2819,13 @@ har-validator@~4.2.1:
|
||||||
ajv "^4.9.1"
|
ajv "^4.9.1"
|
||||||
har-schema "^1.0.5"
|
har-schema "^1.0.5"
|
||||||
|
|
||||||
|
har-validator@~5.0.3:
|
||||||
|
version "5.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.0.3.tgz#ba402c266194f15956ef15e0fcf242993f6a7dfd"
|
||||||
|
dependencies:
|
||||||
|
ajv "^5.1.0"
|
||||||
|
har-schema "^2.0.0"
|
||||||
|
|
||||||
has-ansi@^2.0.0:
|
has-ansi@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
|
||||||
|
@ -2836,6 +2877,15 @@ hawk@~3.1.3:
|
||||||
hoek "2.x.x"
|
hoek "2.x.x"
|
||||||
sntp "1.x.x"
|
sntp "1.x.x"
|
||||||
|
|
||||||
|
hawk@~6.0.2:
|
||||||
|
version "6.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/hawk/-/hawk-6.0.2.tgz#af4d914eb065f9b5ce4d9d11c1cb2126eecc3038"
|
||||||
|
dependencies:
|
||||||
|
boom "4.x.x"
|
||||||
|
cryptiles "3.x.x"
|
||||||
|
hoek "4.x.x"
|
||||||
|
sntp "2.x.x"
|
||||||
|
|
||||||
he@1.1.x, he@^1.1.0:
|
he@1.1.x, he@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||||
|
@ -2852,6 +2902,10 @@ hoek@2.x.x:
|
||||||
version "2.16.3"
|
version "2.16.3"
|
||||||
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
|
||||||
|
|
||||||
|
hoek@4.x.x:
|
||||||
|
version "4.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
|
||||||
|
|
||||||
home-or-tmp@^2.0.0:
|
home-or-tmp@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
|
resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8"
|
||||||
|
@ -2958,6 +3012,14 @@ http-signature@~1.1.0:
|
||||||
jsprim "^1.2.2"
|
jsprim "^1.2.2"
|
||||||
sshpk "^1.7.0"
|
sshpk "^1.7.0"
|
||||||
|
|
||||||
|
http-signature@~1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||||
|
dependencies:
|
||||||
|
assert-plus "^1.0.0"
|
||||||
|
jsprim "^1.2.2"
|
||||||
|
sshpk "^1.7.0"
|
||||||
|
|
||||||
https-browserify@0.0.1:
|
https-browserify@0.0.1:
|
||||||
version "0.0.1"
|
version "0.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
|
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
|
||||||
|
@ -3871,12 +3933,22 @@ mime-db@~1.27.0:
|
||||||
version "1.27.0"
|
version "1.27.0"
|
||||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1"
|
||||||
|
|
||||||
|
mime-db@~1.30.0:
|
||||||
|
version "1.30.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01"
|
||||||
|
|
||||||
mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
|
mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.7:
|
||||||
version "2.1.15"
|
version "2.1.15"
|
||||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
|
||||||
dependencies:
|
dependencies:
|
||||||
mime-db "~1.27.0"
|
mime-db "~1.27.0"
|
||||||
|
|
||||||
|
mime-types@~2.1.17:
|
||||||
|
version "2.1.17"
|
||||||
|
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
|
||||||
|
dependencies:
|
||||||
|
mime-db "~1.30.0"
|
||||||
|
|
||||||
mime@1.3.4:
|
mime@1.3.4:
|
||||||
version "1.3.4"
|
version "1.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
|
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
|
||||||
|
@ -4197,7 +4269,7 @@ number-is-nan@^1.0.0:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||||
|
|
||||||
oauth-sign@~0.8.1:
|
oauth-sign@~0.8.1, oauth-sign@~0.8.2:
|
||||||
version "0.8.2"
|
version "0.8.2"
|
||||||
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
|
||||||
|
|
||||||
|
@ -4479,6 +4551,10 @@ performance-now@^0.2.0:
|
||||||
version "0.2.0"
|
version "0.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-0.2.0.tgz#33ef30c5c77d4ea21c5a53869d91b56d8f2555e5"
|
||||||
|
|
||||||
|
performance-now@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
|
||||||
|
|
||||||
pify@^2.0.0, pify@^2.3.0:
|
pify@^2.0.0, pify@^2.3.0:
|
||||||
version "2.3.0"
|
version "2.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
|
||||||
|
@ -4917,6 +4993,10 @@ qs@6.4.0, qs@~6.4.0:
|
||||||
version "6.4.0"
|
version "6.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
|
||||||
|
|
||||||
|
qs@~6.5.1:
|
||||||
|
version "6.5.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||||
|
|
||||||
query-string@^4.1.0:
|
query-string@^4.1.0:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
|
||||||
|
@ -5202,6 +5282,33 @@ request@2, request@^2.79.0, request@^2.81.0:
|
||||||
tunnel-agent "^0.6.0"
|
tunnel-agent "^0.6.0"
|
||||||
uuid "^3.0.0"
|
uuid "^3.0.0"
|
||||||
|
|
||||||
|
request@^2.82.0:
|
||||||
|
version "2.82.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/request/-/request-2.82.0.tgz#2ba8a92cd7ac45660ea2b10a53ae67cd247516ea"
|
||||||
|
dependencies:
|
||||||
|
aws-sign2 "~0.7.0"
|
||||||
|
aws4 "^1.6.0"
|
||||||
|
caseless "~0.12.0"
|
||||||
|
combined-stream "~1.0.5"
|
||||||
|
extend "~3.0.1"
|
||||||
|
forever-agent "~0.6.1"
|
||||||
|
form-data "~2.3.1"
|
||||||
|
har-validator "~5.0.3"
|
||||||
|
hawk "~6.0.2"
|
||||||
|
http-signature "~1.2.0"
|
||||||
|
is-typedarray "~1.0.0"
|
||||||
|
isstream "~0.1.2"
|
||||||
|
json-stringify-safe "~5.0.1"
|
||||||
|
mime-types "~2.1.17"
|
||||||
|
oauth-sign "~0.8.2"
|
||||||
|
performance-now "^2.1.0"
|
||||||
|
qs "~6.5.1"
|
||||||
|
safe-buffer "^5.1.1"
|
||||||
|
stringstream "~0.0.5"
|
||||||
|
tough-cookie "~2.3.2"
|
||||||
|
tunnel-agent "^0.6.0"
|
||||||
|
uuid "^3.1.0"
|
||||||
|
|
||||||
require-directory@^2.1.1:
|
require-directory@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
|
||||||
|
@ -5293,7 +5400,7 @@ safe-buffer@^5.0.1, safe-buffer@~5.0.1:
|
||||||
version "5.0.1"
|
version "5.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.0.1.tgz#d263ca54696cd8a306b5ca6551e92de57918fbe7"
|
||||||
|
|
||||||
safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1:
|
||||||
version "5.1.1"
|
version "5.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||||
|
|
||||||
|
@ -5437,6 +5544,12 @@ sntp@1.x.x:
|
||||||
dependencies:
|
dependencies:
|
||||||
hoek "2.x.x"
|
hoek "2.x.x"
|
||||||
|
|
||||||
|
sntp@2.x.x:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/sntp/-/sntp-2.0.2.tgz#5064110f0af85f7cfdb7d6b67a40028ce52b4b2b"
|
||||||
|
dependencies:
|
||||||
|
hoek "4.x.x"
|
||||||
|
|
||||||
sort-keys@^1.0.0:
|
sort-keys@^1.0.0:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||||
|
@ -5604,7 +5717,7 @@ string_decoder@~1.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
safe-buffer "~5.1.0"
|
safe-buffer "~5.1.0"
|
||||||
|
|
||||||
stringstream@~0.0.4:
|
stringstream@~0.0.4, stringstream@~0.0.5:
|
||||||
version "0.0.5"
|
version "0.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||||
|
|
||||||
|
@ -5883,6 +5996,12 @@ tough-cookie@~2.3.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
punycode "^1.4.1"
|
punycode "^1.4.1"
|
||||||
|
|
||||||
|
tough-cookie@~2.3.2:
|
||||||
|
version "2.3.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561"
|
||||||
|
dependencies:
|
||||||
|
punycode "^1.4.1"
|
||||||
|
|
||||||
trim-newlines@^1.0.0:
|
trim-newlines@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
|
||||||
|
@ -6051,6 +6170,10 @@ uuid@^3.0.0:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.0.1.tgz#6544bba2dfda8c1cf17e629a3a305e2bb1fee6c1"
|
||||||
|
|
||||||
|
uuid@^3.1.0:
|
||||||
|
version "3.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04"
|
||||||
|
|
||||||
v8flags@^2.0.2:
|
v8flags@^2.0.2:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
|
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
|
||||||
|
|
Loading…
Reference in New Issue
Block a user