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 compiler = webpack(webpackConfig)
|
||||
|
||||
// StackEdit custom middlewares
|
||||
require('./server')(app);
|
||||
|
||||
var devMiddleware = require('webpack-dev-middleware')(compiler, {
|
||||
publicPath: webpackConfig.output.publicPath,
|
||||
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",
|
||||
"prismjs": "^1.6.0",
|
||||
"raw-loader": "^0.5.1",
|
||||
"request": "^2.82.0",
|
||||
"vue": "^2.3.3",
|
||||
"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}">
|
||||
<layout></layout>
|
||||
<modal v-if="showModal"></modal>
|
||||
<notification></notification>
|
||||
</div>
|
||||
<div v-else class="app__spash-screen"></div>
|
||||
</template>
|
||||
|
@ -11,6 +12,7 @@ import Vue from 'vue';
|
|||
import { mapState } from 'vuex';
|
||||
import Layout from './Layout';
|
||||
import Modal from './Modal';
|
||||
import Notification from './Notification';
|
||||
|
||||
// Global directives
|
||||
Vue.directive('focus', {
|
||||
|
@ -23,6 +25,7 @@ export default {
|
|||
components: {
|
||||
Layout,
|
||||
Modal,
|
||||
Notification,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
@ -32,7 +35,7 @@ export default {
|
|||
return !this.$store.getters['content/current'].id;
|
||||
},
|
||||
showModal() {
|
||||
return !!this.$store.state.modal.config;
|
||||
return !!this.$store.getters['modal/config'];
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
<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 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">
|
||||
</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}}
|
||||
<icon-provider class="explorer-node__location" v-for="location in node.locations" :key="location.id" :provider-id="location.providerId"></icon-provider>
|
||||
</div>
|
||||
<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>
|
||||
|
@ -112,10 +113,11 @@ export default {
|
|||
this.$store.commit('explorer/setNewItem', null);
|
||||
},
|
||||
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;
|
||||
if (!cancel && id && value) {
|
||||
this.$store.commit('file/patchItem', {
|
||||
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
||||
id,
|
||||
name: value.slice(0, 250),
|
||||
});
|
||||
|
@ -166,6 +168,7 @@ $item-font-size: 14px;
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
padding-right: 5px;
|
||||
|
||||
.explorer-node--selected > & {
|
||||
background-color: rgba(0, 0, 0, 0.2);
|
||||
|
@ -179,6 +182,13 @@ $item-font-size: 14px;
|
|||
.explorer__tree--new-item & {
|
||||
opacity: 0.33;
|
||||
}
|
||||
|
||||
.explorer-node__location {
|
||||
float: right;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin: 2px 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.explorer-node__item--folder,
|
||||
|
|
|
@ -98,7 +98,6 @@ export default {
|
|||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,10 +3,22 @@
|
|||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||
<settings-modal v-else-if="config.type === 'settings'"></settings-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>
|
||||
<image-modal v-else-if="config.type === 'image'"></image-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 class="modal__inner-2">
|
||||
<div class="modal__content" v-html="config.content"></div>
|
||||
|
@ -20,35 +32,56 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapMutations } 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 { mapGetters } from 'vuex';
|
||||
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 {
|
||||
components: {
|
||||
FilePropertiesModal,
|
||||
SettingsModal,
|
||||
TemplatesModal,
|
||||
HtmlExportModal,
|
||||
LinkModal,
|
||||
ImageModal,
|
||||
GooglePhotoModal,
|
||||
HtmlExportModal,
|
||||
SyncManagementModal,
|
||||
PublishManagementModal,
|
||||
GoogleDriveSyncModal,
|
||||
GoogleDrivePublishModal,
|
||||
DropboxSyncModal,
|
||||
DropboxPublishModal,
|
||||
GithubSyncModal,
|
||||
GithubPublishModal,
|
||||
GistSyncModal,
|
||||
GistPublishModal,
|
||||
BloggerPublishModal,
|
||||
BloggerPagePublishModal,
|
||||
},
|
||||
computed: mapState('modal', [
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
methods: {
|
||||
...mapMutations('modal', [
|
||||
'setConfig',
|
||||
]),
|
||||
onEscape() {
|
||||
this.setConfig();
|
||||
this.config.reject();
|
||||
editorEngineSvc.clEditor.focus();
|
||||
},
|
||||
onFocusInOut(evt) {
|
||||
|
@ -69,7 +102,7 @@ export default {
|
|||
}
|
||||
target = target.parentNode;
|
||||
}
|
||||
this.setConfig();
|
||||
this.config.reject();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -78,7 +111,7 @@ export default {
|
|||
window.addEventListener('focusout', this.onFocusInOut);
|
||||
const eltToFocus = this.$el.querySelector('input.text-input')
|
||||
|| this.$el.querySelector('.textfield')
|
||||
|| this.$el.querySelector('button.button');
|
||||
|| this.$el.querySelector('.button');
|
||||
if (eltToFocus) {
|
||||
eltToFocus.focus();
|
||||
}
|
||||
|
@ -103,7 +136,6 @@ export default {
|
|||
|
||||
.modal__inner-1 {
|
||||
margin: 0 auto;
|
||||
display: table;
|
||||
width: 100%;
|
||||
min-width: 320px;
|
||||
max-width: 500px;
|
||||
|
@ -142,10 +174,30 @@ export default {
|
|||
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 {
|
||||
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 {
|
||||
margin-top: 1.75rem;
|
||||
text-align: right;
|
||||
|
|
|
@ -2,27 +2,38 @@
|
|||
<div class="navigation-bar" :class="{'navigation-bar--editor': styles.showEditor}">
|
||||
<div class="navigation-bar__inner navigation-bar__inner--left navigation-bar__inner--button">
|
||||
<button class="navigation-bar__button button" @click="toggleExplorer()">
|
||||
<icon-folder-multiple></icon-folder-multiple>
|
||||
<icon-folder-open></icon-folder-open>
|
||||
</button>
|
||||
</div>
|
||||
<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()">
|
||||
<icon-stackedit></icon-stackedit>
|
||||
<icon-provider provider-id="stackedit"></icon-provider>
|
||||
</button>
|
||||
</div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right flex flex--row">
|
||||
<div class="navigation-bar__spinner" v-show="showSpinner">
|
||||
<div class="spinner"></div>
|
||||
<div class="navigation-bar__inner navigation-bar__inner--right navigation-bar__inner--title flex flex--row">
|
||||
<div class="navigation-bar__spinner">
|
||||
<div v-show="showSpinner" class="spinner"></div>
|
||||
</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>
|
||||
<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">
|
||||
<button v-if="!offline && isSyncPossible" class="navigation-bar__button button" :disabled="isSyncRequested" @click="requestSync">
|
||||
<icon-sync></icon-sync>
|
||||
</button>
|
||||
<button v-if="offline && isSyncPossible" class="navigation-bar__button navigation-bar__button--sync-off button" disabled="disabled">
|
||||
<icon-sync-off></icon-sync-off>
|
||||
</button>
|
||||
<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}" :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">
|
||||
<div class="flex flex--row" :class="{'navigation-bar__hidden': styles.hideLocations}">
|
||||
<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">
|
||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||
</a>
|
||||
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="!offline && isSyncPossible" :disabled="isSyncRequested" @click="requestSync">
|
||||
<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 class="navigation-bar__inner navigation-bar__inner--edit-buttons">
|
||||
<button class="navigation-bar__button button" @click="pagedownClick('bold')">
|
||||
|
@ -69,6 +80,7 @@
|
|||
import { mapState, mapGetters, mapActions } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
|
||||
export default {
|
||||
|
@ -84,10 +96,18 @@ export default {
|
|||
]),
|
||||
...mapState('queue', [
|
||||
'isSyncRequested',
|
||||
'isPublishRequested',
|
||||
'currentLocation',
|
||||
]),
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
...mapGetters('syncLocation', {
|
||||
syncLocations: 'current',
|
||||
}),
|
||||
...mapGetters('publishLocation', {
|
||||
publishLocations: 'current',
|
||||
}),
|
||||
isSyncPossible() {
|
||||
return this.$store.getters['data/loginToken'] ||
|
||||
this.$store.getters['syncLocation/current'].length;
|
||||
|
@ -134,6 +154,11 @@ export default {
|
|||
syncSvc.requestSync();
|
||||
}
|
||||
},
|
||||
requestPublish() {
|
||||
if (!this.isPublishRequested) {
|
||||
publishSvc.requestPublish();
|
||||
}
|
||||
},
|
||||
pagedownClick(name) {
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
},
|
||||
|
@ -183,6 +208,10 @@ export default {
|
|||
overflow: hidden;
|
||||
}
|
||||
|
||||
.navigation-bar__hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navigation-bar__inner--left {
|
||||
float: left;
|
||||
|
||||
|
@ -201,23 +230,30 @@ export default {
|
|||
|
||||
.navigation-bar__inner--edit-buttons {
|
||||
margin-left: 15px;
|
||||
|
||||
.navigation-bar__button {
|
||||
float: left;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__inner--title * {
|
||||
flex: none;
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
width: 34px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 7px;
|
||||
padding: 0 8px;
|
||||
|
||||
/* prevent from seeing wrapped buttons */
|
||||
margin-bottom: 20px;
|
||||
|
||||
.navigation-bar__inner--button & {
|
||||
padding: 6px;
|
||||
padding: 0 4px;
|
||||
width: 38px;
|
||||
|
||||
&.navigation-bar__button--stackedit {
|
||||
opacity: 0.8;
|
||||
padding: 4px;
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
|
@ -228,6 +264,10 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.navigation-bar__title,
|
||||
.navigation-bar__button {
|
||||
display: inline-block;
|
||||
|
@ -236,6 +276,13 @@ export default {
|
|||
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] {
|
||||
&,
|
||||
&:active,
|
||||
|
@ -250,7 +297,7 @@ export default {
|
|||
&:active,
|
||||
&:focus,
|
||||
&: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 {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
|
@ -283,7 +354,8 @@ export default {
|
|||
|
||||
.navigation-bar__title--input,
|
||||
.navigation-bar__inner--edit-buttons,
|
||||
.navigation-bar__inner--button {
|
||||
.navigation-bar__inner--button,
|
||||
.navigation-bar__spinner {
|
||||
display: none;
|
||||
|
||||
.navigation-bar--editor & {
|
||||
|
@ -291,6 +363,14 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
.navigation-bar__button {
|
||||
display: none;
|
||||
|
||||
.navigation-bar--editor & {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-bar__title--input {
|
||||
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;
|
||||
$d: $r * 2;
|
||||
$b: $d/10;
|
||||
$t: 1500ms;
|
||||
|
||||
.navigation-bar__spinner {
|
||||
width: $d;
|
||||
margin: 10px 5px 0 10px;
|
||||
color: rgba(255, 255, 255, 0.67);
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: $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>
|
||||
</div>
|
||||
<div class="side-bar__inner">
|
||||
<!-- Main menu -->
|
||||
<div v-if="panel === 'menu'" 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 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 -->
|
||||
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||
<more-menu v-else-if="panel === 'more'"></more-menu>
|
||||
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||
</div>
|
||||
<!-- TOC -->
|
||||
<div class="side-bar__panel side-bar__panel--toc" :class="{'side-bar__panel--hidden': panel !== 'toc'}">
|
||||
<toc>
|
||||
</toc>
|
||||
|
@ -125,16 +29,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import { mapActions } from 'vuex';
|
||||
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 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 = {
|
||||
menu: 'Menu',
|
||||
|
@ -150,27 +54,22 @@ export default {
|
|||
components: {
|
||||
Toc,
|
||||
MenuEntry,
|
||||
MainMenu,
|
||||
SyncMenu,
|
||||
PublishMenu,
|
||||
ExportMenu,
|
||||
MoreMenu,
|
||||
},
|
||||
data: () => ({
|
||||
markdownSample: markdownConversionSvc.highlight(markdownSample),
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('data', [
|
||||
'loginToken',
|
||||
]),
|
||||
panel() {
|
||||
return this.$store.getters['data/localSettings'].sideBarPanel;
|
||||
},
|
||||
panelName() {
|
||||
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: {
|
||||
...mapActions('data', [
|
||||
|
@ -179,47 +78,6 @@ export default {
|
|||
...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 }));
|
||||
},
|
||||
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>
|
||||
|
@ -233,6 +91,15 @@ export default {
|
|||
|
||||
hr {
|
||||
margin: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
* + hr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
hr + hr {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -253,7 +120,7 @@ export default {
|
|||
}
|
||||
|
||||
.side-bar__panel--menu {
|
||||
padding: 10px;
|
||||
padding: 10px 10px 50px;
|
||||
}
|
||||
|
||||
.side-bar__panel--help {
|
||||
|
@ -275,4 +142,16 @@ export default {
|
|||
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>
|
||||
|
|
|
@ -1,14 +1,24 @@
|
|||
<template>
|
||||
<div class="toc">
|
||||
<div class="toc__inner">
|
||||
</div>
|
||||
<div class="toc__mask" :style="{top: (maskY - 5) + 'px'}"></div>
|
||||
<div class="toc__inner"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import editorSvc from '../services/editorSvc';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
maskY: -999,
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('layout', [
|
||||
'styles',
|
||||
]),
|
||||
},
|
||||
mounted() {
|
||||
const tocElt = this.$el.querySelector('.toc__inner');
|
||||
|
||||
|
@ -50,6 +60,27 @@ export default {
|
|||
tocElt.addEventListener('mousemove', (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>
|
||||
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
|
@ -168,42 +184,30 @@ textarea {
|
|||
}
|
||||
|
||||
.flex {
|
||||
display: -webkit-box;
|
||||
display: -webkit-flex;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex--row {
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-flex-direction: row;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.flex--column {
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.flex--center {
|
||||
-webkit-justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flex--end {
|
||||
-webkit-justify-content: flex-end;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.flex--space-between {
|
||||
-webkit-justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.flex--align-center {
|
||||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
@ -212,7 +216,6 @@ textarea {
|
|||
line-height: 36px;
|
||||
padding: 4px 4px 0;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
|
||||
|
@ -223,7 +226,6 @@ textarea {
|
|||
display: inline-block;
|
||||
background-color: transparent;
|
||||
opacity: 0.75;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
|
||||
/* prevent from seeing wrapped buttons */
|
||||
|
@ -261,7 +263,8 @@ textarea {
|
|||
width: 50%;
|
||||
float: left;
|
||||
text-align: center;
|
||||
line-height: 2.5em;
|
||||
line-height: 1.4;
|
||||
padding: 0.67em 0.33em;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid transparent;
|
||||
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-border-radius: 2px;
|
||||
$link-color: #0c93e4;
|
||||
$error-color: #f20;
|
||||
$border-radius-base: 2px;
|
||||
$hr-color: rgba(128, 128, 128, 0.2);
|
||||
$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;
|
||||
width: 20px;
|
||||
margin-right: 12px;
|
||||
-webkit-flex: none;
|
||||
flex: none;
|
||||
}
|
||||
</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>
|
||||
<div class="modal__inner-1 modal__inner-1--file-properties">
|
||||
<div class="modal__inner-2">
|
||||
<div class="tabs">
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
<div class="tabs flex flex--row">
|
||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
Current file properties
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,9 +27,9 @@
|
|||
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapState } from 'vuex';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import defaultProperties from '../data/defaultFileProperties.yml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||
|
||||
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
||||
|
||||
|
@ -44,7 +44,7 @@ export default {
|
|||
error: null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
strippedCustomProperties() {
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.modal__inner-1--file-properties {
|
||||
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>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const makeThumbnail = (url, size) => `${url}=s${size}`;
|
||||
|
||||
|
@ -36,7 +36,7 @@ export default {
|
|||
thumbnailUrl() {
|
||||
return `url(${makeThumbnail(this.config.url, 320)})`;
|
||||
},
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
},
|
|
@ -4,7 +4,7 @@
|
|||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="template">Template</label>
|
||||
<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">
|
||||
{{ template.name }}
|
||||
</option>
|
||||
|
@ -24,16 +24,16 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Clipboard from 'clipboard';
|
||||
import exportSvc from '../services/exportSvc';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
result: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
|
@ -41,11 +41,11 @@ export default {
|
|||
]),
|
||||
selectedTemplate: {
|
||||
get() {
|
||||
return this.$store.getters['data/localSettings'].htmlExportLastTemplate;
|
||||
return this.$store.getters['data/localSettings'].htmlExportTemplate;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
htmlExportLastTemplate: value,
|
||||
htmlExportTemplate: value,
|
||||
});
|
||||
},
|
||||
},
|
||||
|
@ -54,8 +54,8 @@ export default {
|
|||
this.$watch('selectedTemplate', (selectedTemplate) => {
|
||||
const currentFile = this.$store.getters['file/current'];
|
||||
exportSvc.applyTemplate(currentFile.id, this.allTemplates[selectedTemplate])
|
||||
.then((res) => {
|
||||
this.result = res;
|
||||
.then((html) => {
|
||||
this.result = html;
|
||||
});
|
||||
}, {
|
||||
immediate: true,
|
||||
|
@ -69,13 +69,16 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
configureTemplates() {
|
||||
const reopen = () => this.$store.dispatch('modal/open', 'htmlExport');
|
||||
this.$store.dispatch('modal/open', {
|
||||
type: 'templates',
|
||||
selectedKey: this.selectedTemplate,
|
||||
selectedId: this.selectedTemplate,
|
||||
})
|
||||
.then(templates => this.$store.dispatch('data/setTemplates', templates))
|
||||
.then(reopen, reopen);
|
||||
.then(({ templates, selectedId }) => {
|
||||
this.$store.dispatch('data/setTemplates', templates);
|
||||
this.$store.dispatch('data/patchLocalSettings', {
|
||||
htmlExportTemplate: selectedId,
|
||||
});
|
||||
});
|
||||
},
|
||||
resolve() {
|
||||
const currentFile = this.$store.getters['file/current'];
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--image">
|
||||
<div class="modal__inner-2">
|
||||
<p>Please provide a <b>URL</b> for your image.
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="url">URL</label>
|
||||
<div class="form-entry__field">
|
||||
|
@ -8,12 +9,12 @@
|
|||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<span>{{token.name}}</span>
|
||||
</menu-entry>
|
||||
<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>
|
||||
</menu-entry>
|
||||
<div class="modal__button-bar">
|
||||
|
@ -25,9 +26,9 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import MenuEntry from './MenuEntry';
|
||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
import { mapGetters } from 'vuex';
|
||||
import MenuEntry from '../menus/MenuEntry';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -37,7 +38,7 @@ export default {
|
|||
url: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
googlePhotosTokens() {
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<div class="modal__inner-1 modal__inner-1--link" @keyup.enter="resolve()">
|
||||
<div class="modal__inner-2">
|
||||
<p>Please provide a <b>URL</b> for your link.
|
||||
<div class="form-entry">
|
||||
<label class="form-entry__label" for="url">URL</label>
|
||||
<div class="form-entry__field">
|
||||
|
@ -16,13 +17,13 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
url: '',
|
||||
}),
|
||||
computed: mapState('modal', [
|
||||
computed: mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
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>
|
||||
<div class="modal__inner-1 modal__inner-1--settings">
|
||||
<div class="modal__inner-2">
|
||||
<div class="tabs">
|
||||
<div class="tabs__tab" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
<div class="tabs flex flex--row">
|
||||
<div class="tabs__tab flex flex--column flex--center" :class="{'tabs__tab--active': tab === 'custom'}" @click="tab = 'custom'">
|
||||
Custom settings
|
||||
</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
|
||||
</div>
|
||||
</div>
|
||||
|
@ -27,9 +27,9 @@
|
|||
|
||||
<script>
|
||||
import yaml from 'js-yaml';
|
||||
import { mapState } from 'vuex';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import defaultSettings from '../data/defaultSettings.yml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultSettings from '../../data/defaultSettings.yml';
|
||||
|
||||
const emptySettings = '# Add your custom settings here to override the default settings.\n';
|
||||
|
||||
|
@ -44,7 +44,7 @@ export default {
|
|||
error: null,
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
strippedCustomSettings() {
|
||||
|
@ -70,7 +70,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import 'common/variables.scss';
|
||||
@import '../common/variables.scss';
|
||||
|
||||
.modal__inner-1--settings {
|
||||
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>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex';
|
||||
import utils from '../services/utils';
|
||||
import CodeEditor from './CodeEditor';
|
||||
import emptyTemplateValue from '../data/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
import { mapGetters } from 'vuex';
|
||||
import utils from '../../services/utils';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import emptyTemplateValue from '../../data/emptyTemplateValue.html';
|
||||
import emptyTemplateHelpers from '!raw-loader!../../data/emptyTemplateHelpers.js'; // eslint-disable-line
|
||||
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
|
||||
function fillEmptyFields(template) {
|
||||
if (template.value === '\n') {
|
||||
|
@ -78,7 +80,7 @@ export default {
|
|||
editingName: '',
|
||||
}),
|
||||
computed: {
|
||||
...mapState('modal', [
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
isReadOnly() {
|
||||
|
@ -89,13 +91,17 @@ export default {
|
|||
this.$watch(
|
||||
() => this.$store.getters['data/allTemplates'],
|
||||
(allTemplates) => {
|
||||
const templates = utils.sortObject(
|
||||
utils.deepCopy(allTemplates),
|
||||
(key, template) => template.name,
|
||||
);
|
||||
Object.keys(templates).forEach(id => fillEmptyFields(templates[id]));
|
||||
const templates = {};
|
||||
// Sort templates by name
|
||||
Object.keys(allTemplates)
|
||||
.sort((id1, id2) => collator.compare(allTemplates[id1].name, allTemplates[id2].name))
|
||||
.forEach((id) => {
|
||||
const template = utils.deepCopy(allTemplates[id]);
|
||||
fillEmptyFields(template);
|
||||
templates[id] = template;
|
||||
});
|
||||
this.templates = templates;
|
||||
this.selectedId = this.$store.state.modal.config.selectedId;
|
||||
this.selectedId = this.config.selectedId;
|
||||
if (!templates[this.selectedId]) {
|
||||
this.selectedId = Object.keys(templates)[0];
|
||||
}
|
||||
|
@ -143,7 +149,10 @@ export default {
|
|||
}, 1);
|
||||
},
|
||||
resolve() {
|
||||
this.config.resolve(this.templates);
|
||||
this.config.resolve({
|
||||
templates: this.templates,
|
||||
selectedId: this.selectedId,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -151,6 +160,6 @@ export default {
|
|||
|
||||
<style lang="scss">
|
||||
.modal__inner-1--templates {
|
||||
max-width: 720px;
|
||||
max-width: 680px;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,12 @@
|
|||
### File properties can contain metadata used for your publications (Wordpress, Blogger...).
|
||||
|
||||
### For example, you can specify a blog post title:
|
||||
#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
|
||||
extensions:
|
||||
|
|
|
@ -7,5 +7,15 @@ export default () => ({
|
|||
showExplorer: false,
|
||||
focusMode: false,
|
||||
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
|
||||
newFileProperties: |
|
||||
# extensions:
|
||||
# markdown:
|
||||
# breaks: true
|
||||
#extensions:
|
||||
# markdown:
|
||||
# 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 () => ({
|
||||
id: null,
|
||||
type: 'syncLocation',
|
||||
provider: null,
|
||||
providerId: null,
|
||||
fileId: null,
|
||||
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:
|
||||
- {{#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}}{{/tocToHtml}} will produce a nice TOC.
|
||||
{{#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>
|
||||
<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>
|
||||
</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 Delete from './Delete';
|
||||
import Close from './Close';
|
||||
import FolderMultiple from './FolderMultiple';
|
||||
import FolderOpen from './FolderOpen';
|
||||
import Pen from './Pen';
|
||||
import Target from './Target';
|
||||
import ArrowLeft from './ArrowLeft';
|
||||
|
@ -36,10 +36,11 @@ import HardDisk from './HardDisk';
|
|||
import Download from './Download';
|
||||
import CodeTags from './CodeTags';
|
||||
import CodeBraces from './CodeBraces';
|
||||
import OpenInNew from './OpenInNew';
|
||||
import Information from './Information';
|
||||
import Alert from './Alert';
|
||||
// Providers
|
||||
import Stackedit from './Stackedit';
|
||||
import GoogleDrive from './GoogleDrive';
|
||||
import GooglePhotos from './GooglePhotos';
|
||||
import Provider from './Provider';
|
||||
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
Vue.component('iconFormatItalic', FormatItalic);
|
||||
|
@ -62,7 +63,7 @@ Vue.component('iconFileMultiple', FileMultiple);
|
|||
Vue.component('iconFolderPlus', FolderPlus);
|
||||
Vue.component('iconDelete', Delete);
|
||||
Vue.component('iconClose', Close);
|
||||
Vue.component('iconFolderMultiple', FolderMultiple);
|
||||
Vue.component('iconFolderOpen', FolderOpen);
|
||||
Vue.component('iconPen', Pen);
|
||||
Vue.component('iconTarget', Target);
|
||||
Vue.component('iconArrowLeft', ArrowLeft);
|
||||
|
@ -78,7 +79,8 @@ Vue.component('iconHardDisk', HardDisk);
|
|||
Vue.component('iconDownload', Download);
|
||||
Vue.component('iconCodeTags', CodeTags);
|
||||
Vue.component('iconCodeBraces', CodeBraces);
|
||||
Vue.component('iconOpenInNew', OpenInNew);
|
||||
Vue.component('iconInformation', Information);
|
||||
Vue.component('iconAlert', Alert);
|
||||
// Providers
|
||||
Vue.component('iconStackedit', Stackedit);
|
||||
Vue.component('iconGoogleDrive', GoogleDrive);
|
||||
Vue.component('iconGooglePhotos', GooglePhotos);
|
||||
Vue.component('iconProvider', Provider);
|
||||
|
|
|
@ -356,7 +356,7 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||
if (sectionDesc) {
|
||||
const scrollTop = sectionDesc[objectToScroll.dimensionKey].startOffset +
|
||||
(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.previewElt.parentNode.addEventListener('scroll', () => this.saveContentState(true));
|
||||
|
||||
const refreshPreview = () => {
|
||||
this.convert();
|
||||
|
@ -594,12 +595,6 @@ const editorSvc = Object.assign(new Vue(), { // Use a vue instance as an event b
|
|||
|
||||
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'))
|
||||
// var previewElt = element[0].querySelector('.preview')
|
||||
// clEditorSvc.isPreviewTop = previewElt.scrollTop < 10
|
||||
|
|
|
@ -4,8 +4,8 @@ import utils from './utils';
|
|||
import store from '../store';
|
||||
|
||||
const indexedDB = window.indexedDB;
|
||||
const localStorage = window.localStorage;
|
||||
const dbVersion = 1;
|
||||
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||
const dbStoreName = 'objects';
|
||||
|
||||
if (!indexedDB) {
|
||||
|
@ -27,7 +27,7 @@ class Connection {
|
|||
|
||||
request.onsuccess = (event) => {
|
||||
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.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
||||
|
@ -68,7 +68,7 @@ class Connection {
|
|||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
|
@ -292,8 +292,8 @@ export default {
|
|||
request.onsuccess = resolve;
|
||||
})
|
||||
.then(() => {
|
||||
localStorage.removeItem('localDbVersion');
|
||||
localStorage.removeItem(dbVersionKey);
|
||||
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.getters['layout/styles'],
|
||||
() => {
|
||||
isScrollEditor = true;
|
||||
isScrollPreview = false;
|
||||
(styles) => {
|
||||
isScrollEditor = styles.showEditor;
|
||||
isScrollPreview = !styles.showEditor;
|
||||
skipAnimation = true;
|
||||
});
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import editorSvc from '../../services/editorSvc';
|
|||
import syncSvc from '../../services/syncSvc';
|
||||
|
||||
// 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);
|
||||
|
||||
|
|
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 googleHelper from './helpers/googleHelper';
|
||||
import providerRegistry from './providerRegistry';
|
||||
|
||||
export default {
|
||||
export default providerRegistry.register({
|
||||
id: 'googleDriveAppData',
|
||||
getToken() {
|
||||
return store.getters['data/loginToken'];
|
||||
},
|
||||
getChanges(token) {
|
||||
return googleHelper.getChanges(token)
|
||||
.then((result) => {
|
||||
|
@ -37,7 +42,7 @@ export default {
|
|||
}
|
||||
},
|
||||
saveItem(token, item, syncData, ifNotTooLate) {
|
||||
return googleHelper.saveAppDataFile(
|
||||
return googleHelper.uploadAppDataFile(
|
||||
token,
|
||||
JSON.stringify(item), ['appDataFolder'],
|
||||
null,
|
||||
|
@ -76,19 +81,19 @@ export default {
|
|||
return item;
|
||||
});
|
||||
},
|
||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||
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 googleHelper.saveAppDataFile(
|
||||
return googleHelper.uploadAppDataFile(
|
||||
token,
|
||||
JSON.stringify({
|
||||
id: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
id: content.id,
|
||||
type: content.type,
|
||||
hash: content.hash,
|
||||
}), ['appDataFolder'],
|
||||
JSON.stringify(item),
|
||||
JSON.stringify(content),
|
||||
syncData && syncData.id,
|
||||
ifNotTooLate,
|
||||
)
|
||||
|
@ -97,10 +102,10 @@ export default {
|
|||
[file.id]: {
|
||||
// Build sync data
|
||||
id: file.id,
|
||||
itemId: item.id,
|
||||
type: item.type,
|
||||
hash: item.hash,
|
||||
itemId: content.id,
|
||||
type: content.type,
|
||||
hash: content.hash,
|
||||
},
|
||||
}));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
|
|
@ -1,28 +1,42 @@
|
|||
import store from '../../store';
|
||||
import googleHelper from './helpers/googleHelper';
|
||||
import providerUtils from './providerUtils';
|
||||
import providerRegistry from './providerRegistry';
|
||||
import utils from '../utils';
|
||||
|
||||
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) {
|
||||
return googleHelper.downloadFile(token, syncLocation.driveFileId)
|
||||
.then(content => providerUtils.parseContent(content));
|
||||
},
|
||||
uploadContent(token, item, syncLocation, ifNotTooLate) {
|
||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||
const name = (file && file.name) || defaultFilename;
|
||||
const parents = [];
|
||||
if (syncLocation.driveParentId) {
|
||||
parents.push(syncLocation.driveParentId);
|
||||
}
|
||||
return googleHelper.saveFile(
|
||||
return googleHelper.uploadFile(
|
||||
token,
|
||||
name,
|
||||
parents,
|
||||
providerUtils.serializeContent(item),
|
||||
syncLocation && syncLocation.driveFileId,
|
||||
providerUtils.serializeContent(content),
|
||||
undefined,
|
||||
syncLocation.driveFileId,
|
||||
ifNotTooLate,
|
||||
)
|
||||
.then(driveFile => ({
|
||||
|
@ -30,6 +44,20 @@ export default {
|
|||
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) {
|
||||
const openOneFile = () => {
|
||||
const file = files.pop();
|
||||
|
@ -39,20 +67,22 @@ export default {
|
|||
let syncLocation;
|
||||
// Try to find an existing sync location
|
||||
store.getters['syncLocation/items'].some((existingSyncLocation) => {
|
||||
if (existingSyncLocation.driveFileId === file.id) {
|
||||
if (existingSyncLocation.providerId === this.id &&
|
||||
existingSyncLocation.driveFileId === file.id
|
||||
) {
|
||||
syncLocation = existingSyncLocation;
|
||||
}
|
||||
return syncLocation;
|
||||
});
|
||||
if (syncLocation) {
|
||||
// Sync location already exists, just open the file
|
||||
this.$store.commit('file/setCurrentId', syncLocation.fileId);
|
||||
store.commit('file/setCurrentId', syncLocation.fileId);
|
||||
return openOneFile();
|
||||
}
|
||||
// Sync location does not exist, download content from Google Drive and create the file
|
||||
syncLocation = {
|
||||
driveFileId: file.id,
|
||||
provider: 'googleDrive',
|
||||
providerId: this.id,
|
||||
sub: token.sub,
|
||||
};
|
||||
return this.downloadContent(token, syncLocation)
|
||||
|
@ -74,12 +104,26 @@ export default {
|
|||
fileId: 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());
|
||||
};
|
||||
return Promise.resolve()
|
||||
.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.file',
|
||||
'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 libraries = ['picker'];
|
||||
|
@ -25,9 +25,7 @@ const request = (token, options) => utils.request({
|
|||
},
|
||||
});
|
||||
|
||||
function saveFile(refreshedToken, name, parents, media = null, fileId = null,
|
||||
ifNotTooLate = cb => res => cb(res),
|
||||
) {
|
||||
function uploadFile(refreshedToken, name, parents, media = null, mediaType = 'text/plain', fileId = null, ifNotTooLate = cb => res => cb(res)) {
|
||||
return Promise.resolve()
|
||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||
.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 += JSON.stringify(metadata);
|
||||
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 += closeDelimiter;
|
||||
options.url = options.url.replace(
|
||||
|
@ -95,7 +93,7 @@ export default {
|
|||
login_hint: sub,
|
||||
prompt: silent ? 'none' : null,
|
||||
}, silent)
|
||||
// Call the tokeninfo endpoint
|
||||
// Call the token info endpoint
|
||||
.then(data => utils.request({
|
||||
method: 'POST',
|
||||
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
||||
|
@ -121,11 +119,12 @@ export default {
|
|||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -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,
|
||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||
};
|
||||
}))
|
||||
// Call the tokeninfo endpoint
|
||||
// Call the user info endpoint
|
||||
.then(token => request(token, {
|
||||
method: 'GET',
|
||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
||||
|
@ -139,6 +138,7 @@ export default {
|
|||
// Save flags
|
||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||
// Save nextPageToken
|
||||
|
@ -168,7 +168,8 @@ export default {
|
|||
// Try to get a new token in background
|
||||
return this.startOauth2(mergedScopes, sub, true)
|
||||
// If it fails try to popup a window
|
||||
.catch(() => this.startOauth2(mergedScopes, sub));
|
||||
.catch(() => utils.checkOnline()
|
||||
.then(() => this.startOauth2(mergedScopes, sub)));
|
||||
});
|
||||
},
|
||||
loadClientScript() {
|
||||
|
@ -176,17 +177,17 @@ export default {
|
|||
return Promise.resolve();
|
||||
}
|
||||
return utils.loadScript('https://apis.google.com/js/api.js')
|
||||
.then(() => Promise.all(libraries.map(
|
||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||
callback: resolve,
|
||||
onerror: reject,
|
||||
timeout: 30000,
|
||||
ontimeout: reject,
|
||||
})))))
|
||||
.then(() => {
|
||||
gapi = window.gapi;
|
||||
google = window.google;
|
||||
});
|
||||
.then(() => Promise.all(libraries.map(
|
||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||
callback: resolve,
|
||||
onerror: reject,
|
||||
timeout: 30000,
|
||||
ontimeout: reject,
|
||||
})))))
|
||||
.then(() => {
|
||||
gapi = window.gapi;
|
||||
google = window.google;
|
||||
});
|
||||
},
|
||||
signin() {
|
||||
return this.startOauth2(driveAppDataScopes);
|
||||
|
@ -194,6 +195,9 @@ export default {
|
|||
addDriveAccount(fullAccess = false) {
|
||||
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }));
|
||||
},
|
||||
addBloggerAccount() {
|
||||
return this.startOauth2(bloggerScopes);
|
||||
},
|
||||
addPhotosAccount() {
|
||||
return this.startOauth2(photosScopes);
|
||||
},
|
||||
|
@ -224,13 +228,15 @@ export default {
|
|||
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)
|
||||
.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)
|
||||
.then(refreshedToken => saveFile(refreshedToken, name, parents, media, fileId, ifNotTooLate));
|
||||
.then(refreshedToken => uploadFile(
|
||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||
},
|
||||
downloadFile(token, id) {
|
||||
return this.refreshToken(getDriveScopes(token), token)
|
||||
|
@ -248,6 +254,72 @@ export default {
|
|||
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') {
|
||||
const scopes = type === 'img' ? photosScopes : getDriveScopes(token);
|
||||
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 utils from './utils';
|
||||
import diffUtils from './diffUtils';
|
||||
import userActivitySvc from './userActivitySvc';
|
||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||
import googleDriveProvider from './providers/googleDriveProvider';
|
||||
import providerRegistry from './providers/providerRegistry';
|
||||
import mainProvider from './providers/googleDriveAppDataProvider';
|
||||
|
||||
const lastSyncActivityKey = 'lastSyncActivity';
|
||||
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
||||
let lastSyncActivity;
|
||||
const getStoredLastSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
|
@ -37,26 +36,6 @@ function setLastSyncActivity() {
|
|||
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) {
|
||||
// Clean syncHistory from removed syncLocations
|
||||
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
|
||||
|
@ -123,6 +102,37 @@ function applyChanges(changes) {
|
|||
const LAST_SENT = 0;
|
||||
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) {
|
||||
return loadSyncedContent(fileId)
|
||||
.then(() => loadContent(fileId))
|
||||
|
@ -131,121 +141,145 @@ function syncFile(fileId) {
|
|||
const getSyncedContent = () => store.state.syncedContent.itemMap[`${fileId}/syncedContent`];
|
||||
const getSyncHistoryItem = syncLocationId => getSyncedContent().syncHistory[syncLocationId];
|
||||
const downloadedLocations = {};
|
||||
const errorLocations = {};
|
||||
|
||||
const isLocationSynced = syncLocation =>
|
||||
getSyncHistoryItem(syncLocation.id)[LAST_SENT] === getContent().hash;
|
||||
const isLocationSynced = (syncLocation) => {
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
return syncHistoryItem && syncHistoryItem[LAST_SENT] === getContent().hash;
|
||||
};
|
||||
|
||||
const syncOneContentLocation = () => {
|
||||
const syncLocations = [
|
||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||
];
|
||||
if (isDataSyncPossible()) {
|
||||
syncLocations.unshift({ id: 'main', provider: 'googleDriveAppData', fileId });
|
||||
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
||||
}
|
||||
let result;
|
||||
syncLocations.some((syncLocation) => {
|
||||
if (!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation)) {
|
||||
const provider = getSyncProvider(syncLocation);
|
||||
const token = getSyncToken(syncLocation);
|
||||
result = provider && token && provider.downloadContent(token, syncLocation)
|
||||
.then((serverContent = null) => {
|
||||
downloadedLocations[syncLocation.id] = true;
|
||||
if (!errorLocations[syncLocation.id] &&
|
||||
(!downloadedLocations[syncLocation.id] || !isLocationSynced(syncLocation))
|
||||
) {
|
||||
const provider = providerRegistry.providers[syncLocation.providerId];
|
||||
const token = provider.getToken(syncLocation);
|
||||
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 syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
let mergedContent = (() => {
|
||||
const clientContent = utils.deepCopy(getContent());
|
||||
if (!serverContent) {
|
||||
// Sync location has not been created yet
|
||||
return clientContent;
|
||||
}
|
||||
if (serverContent.hash === clientContent.hash) {
|
||||
// Server and client contents are synced
|
||||
return clientContent;
|
||||
}
|
||||
if (syncedContent.historyData[serverContent.hash]) {
|
||||
// Server content has not changed or has already been merged
|
||||
return clientContent;
|
||||
}
|
||||
// Perform a merge with last merged content if any, or a simple fusion otherwise
|
||||
let lastMergedContent;
|
||||
serverContent.history.some((hash) => {
|
||||
lastMergedContent = syncedContent.historyData[hash];
|
||||
return lastMergedContent;
|
||||
const syncedContent = getSyncedContent();
|
||||
const syncHistoryItem = getSyncHistoryItem(syncLocation.id);
|
||||
let mergedContent = (() => {
|
||||
const clientContent = utils.deepCopy(getContent());
|
||||
if (!serverContent) {
|
||||
// Sync location has not been created yet
|
||||
return clientContent;
|
||||
}
|
||||
if (serverContent.hash === clientContent.hash) {
|
||||
// Server and client contents are synced
|
||||
return clientContent;
|
||||
}
|
||||
if (syncedContent.historyData[serverContent.hash]) {
|
||||
// Server content has not changed or has already been merged
|
||||
return clientContent;
|
||||
}
|
||||
// Perform a merge with last merged content if any, or a simple fusion otherwise
|
||||
let lastMergedContent;
|
||||
serverContent.history.some((hash) => {
|
||||
lastMergedContent = syncedContent.historyData[hash];
|
||||
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);
|
||||
})();
|
||||
|
||||
// Update content in store
|
||||
store.commit('content/patchItem', {
|
||||
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);
|
||||
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,
|
||||
// and merged content which will be sent if different
|
||||
const newSyncedContent = utils.deepCopy(syncedContent);
|
||||
const newSyncHistoryItem = newSyncedContent.syncHistory[syncLocation.id] || [];
|
||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||
if (serverContent && (serverContent.hash === newSyncHistoryItem[LAST_SENT] ||
|
||||
serverContent.history.indexOf(newSyncHistoryItem[LAST_SENT]) !== -1)
|
||||
) {
|
||||
// The server has accepted the content we previously 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');
|
||||
// Store last sent if it's in the server history,
|
||||
// and merged content which will be sent if different
|
||||
const newSyncedContent = utils.deepCopy(syncedContent);
|
||||
const newSyncHistoryItem = newSyncedContent.syncHistory[syncLocation.id] || [];
|
||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||
if (serverContent && (serverContent.hash === newSyncHistoryItem[LAST_SENT] ||
|
||||
serverContent.history.indexOf(newSyncHistoryItem[LAST_SENT]) !== -1)
|
||||
) {
|
||||
// The server has accepted the content we previously sent
|
||||
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SENT];
|
||||
}
|
||||
return cb(res);
|
||||
};
|
||||
newSyncHistoryItem[LAST_SENT] = mergedContent.hash;
|
||||
newSyncedContent.historyData[mergedContent.hash] = mergedContent;
|
||||
|
||||
// Upload merged content
|
||||
return provider.uploadContent(token, {
|
||||
...mergedContent,
|
||||
history: mergedContentHistory,
|
||||
}, syncLocation, ifNotTooLate);
|
||||
})
|
||||
.then(() => syncOneContentLocation());
|
||||
// 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);
|
||||
};
|
||||
|
||||
// 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;
|
||||
});
|
||||
|
@ -271,11 +305,11 @@ function syncFile(fileId) {
|
|||
|
||||
function sync() {
|
||||
const googleToken = store.getters['data/loginToken'];
|
||||
return googleDriveAppDataProvider.getChanges(googleToken)
|
||||
return mainProvider.getChanges(googleToken)
|
||||
.then((changes) => {
|
||||
// Apply changes
|
||||
applyChanges(changes);
|
||||
googleDriveAppDataProvider.setAppliedChanges(googleToken, changes);
|
||||
mainProvider.setAppliedChanges(googleToken, changes);
|
||||
|
||||
// Prevent from sending items too long after changes have been retrieved
|
||||
const syncStartTime = Date.now();
|
||||
|
@ -292,6 +326,7 @@ function sync() {
|
|||
...store.state.file.itemMap,
|
||||
...store.state.folder.itemMap,
|
||||
...store.state.syncLocation.itemMap,
|
||||
...store.state.publishLocation.itemMap,
|
||||
// Deal with contents later
|
||||
};
|
||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||
|
@ -300,7 +335,7 @@ function sync() {
|
|||
const item = storeItemMap[id];
|
||||
const existingSyncData = syncDataByItemId[id];
|
||||
if (!existingSyncData || existingSyncData.hash !== item.hash) {
|
||||
result = googleDriveAppDataProvider.saveItem(
|
||||
result = mainProvider.saveItem(
|
||||
googleToken,
|
||||
// Use deepCopy to freeze objects
|
||||
utils.deepCopy(item),
|
||||
|
@ -323,7 +358,8 @@ function sync() {
|
|||
...store.state.file.itemMap,
|
||||
...store.state.folder.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'];
|
||||
let result;
|
||||
|
@ -335,7 +371,7 @@ function sync() {
|
|||
) {
|
||||
// Use deepCopy to freeze objects
|
||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||
result = googleDriveAppDataProvider
|
||||
result = mainProvider
|
||||
.removeItem(googleToken, syncDataToRemove, ifNotTooLate)
|
||||
.then(() => {
|
||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||
|
@ -350,10 +386,7 @@ function sync() {
|
|||
});
|
||||
|
||||
const getOneFileIdToSync = () => {
|
||||
const allContentIds = Object.keys({
|
||||
...store.state.content.itemMap,
|
||||
...store.getters['data/syncDataByType'].content,
|
||||
});
|
||||
const allContentIds = Object.keys(localDbSvc.hashMap.content);
|
||||
let fileId;
|
||||
allContentIds.some((contentId) => {
|
||||
// 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 syncData = store.getters['data/syncDataByItemId'][contentId];
|
||||
// Sync if item hash and syncData hash are different
|
||||
if (!hash || !syncData || hash !== syncData.hash) {
|
||||
if (!syncData || hash !== syncData.hash) {
|
||||
[fileId] = contentId.split('/');
|
||||
}
|
||||
return fileId;
|
||||
|
@ -401,7 +434,7 @@ function requestSync() {
|
|||
let intervalId;
|
||||
const attempt = () => {
|
||||
// Only start syncing when these conditions are met
|
||||
if (userActivitySvc.isActive() && isSyncWindow()) {
|
||||
if (utils.isUserActive() && isSyncWindow()) {
|
||||
clearInterval(intervalId);
|
||||
if (!isSyncPossible()) {
|
||||
// Cancel sync
|
||||
|
@ -422,6 +455,9 @@ function requestSync() {
|
|||
return sync();
|
||||
}
|
||||
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 null;
|
||||
|
@ -437,7 +473,7 @@ function requestSync() {
|
|||
// Sync periodically
|
||||
utils.setInterval(() => {
|
||||
if (isSyncPossible() &&
|
||||
userActivitySvc.isActive() &&
|
||||
utils.isUserActive() &&
|
||||
isSyncWindow() &&
|
||||
isAutoSyncReady()
|
||||
) {
|
||||
|
@ -508,4 +544,5 @@ utils.setInterval(() => {
|
|||
export default {
|
||||
isSyncPossible,
|
||||
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 defaultProperties from '../data/defaultFileProperties.yml';
|
||||
|
||||
// For sortObject
|
||||
const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||
const workspaceId = 'main';
|
||||
|
||||
// For uid()
|
||||
const uidLength = 16;
|
||||
const crypto = window.crypto || window.msCrypto;
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
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()
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
@ -17,8 +38,18 @@ const urlParser = window.document.createElement('a');
|
|||
const scriptLoadingPromises = Object.create(null);
|
||||
|
||||
export default {
|
||||
workspaceId,
|
||||
origin: `${location.protocol}//${location.host}`,
|
||||
types: ['contentState', 'syncedContent', 'content', 'file', 'folder', 'syncLocation', 'data'],
|
||||
types: [
|
||||
'contentState',
|
||||
'syncedContent',
|
||||
'content',
|
||||
'file',
|
||||
'folder',
|
||||
'syncLocation',
|
||||
'publishLocation',
|
||||
'data',
|
||||
],
|
||||
deepCopy(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() {
|
||||
crypto.getRandomValues(array);
|
||||
return array.cl_map(value => alphabet[value % radix]).join('');
|
||||
|
@ -84,6 +106,12 @@ export default {
|
|||
setInterval(func, 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 = {}) {
|
||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||
if (!keys.length) {
|
||||
|
@ -166,7 +194,7 @@ export default {
|
|||
event.data.state === state
|
||||
) {
|
||||
oauth2Context.clean();
|
||||
if (event.data.accessToken) {
|
||||
if (event.data.accessToken || event.data.code) {
|
||||
resolve(event.data);
|
||||
} else {
|
||||
reject(event.data);
|
||||
|
@ -259,7 +287,7 @@ export default {
|
|||
|
||||
// Add query params to URL
|
||||
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) => {
|
||||
xhr.setRequestHeader(key, config.headers[key]);
|
||||
});
|
||||
|
@ -281,4 +309,24 @@ export default {
|
|||
|
||||
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 file from './modules/file';
|
||||
import folder from './modules/folder';
|
||||
import publishLocation from './modules/publishLocation';
|
||||
import syncLocation from './modules/syncLocation';
|
||||
import data from './modules/data';
|
||||
import layout from './modules/layout';
|
||||
import explorer from './modules/explorer';
|
||||
import modal from './modules/modal';
|
||||
import notification from './modules/notification';
|
||||
import queue from './modules/queue';
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
@ -44,21 +46,41 @@ const store = new Vuex.Store({
|
|||
content,
|
||||
file,
|
||||
folder,
|
||||
publishLocation,
|
||||
syncLocation,
|
||||
data,
|
||||
layout,
|
||||
explorer,
|
||||
modal,
|
||||
notification,
|
||||
queue,
|
||||
},
|
||||
strict: debug,
|
||||
plugins: debug ? [createLogger()] : [],
|
||||
});
|
||||
|
||||
let isConnectionDown = false;
|
||||
let lastConnectionCheck = 0;
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Vue from 'vue';
|
||||
import yaml from 'js-yaml';
|
||||
import moduleTemplate from './moduleTemplate';
|
||||
import utils from '../../services/utils';
|
||||
|
@ -5,6 +6,7 @@ import defaultSettings from '../../data/defaultSettings.yml';
|
|||
import defaultLocalSettings from '../../data/defaultLocalSettings';
|
||||
import plainHtmlTemplate from '../../data/plainHtmlTemplate.html';
|
||||
import styledHtmlTemplate from '../../data/styledHtmlTemplate.html';
|
||||
import jekyllSiteTemplate from '../../data/jekyllSiteTemplate.html';
|
||||
|
||||
const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
||||
|
||||
|
@ -20,6 +22,23 @@ const empty = (id) => {
|
|||
};
|
||||
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 setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
||||
const patcher = id => ({ state, commit }, data) => {
|
||||
|
@ -86,8 +105,10 @@ const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
|||
isAdditional: true,
|
||||
});
|
||||
const additionalTemplates = {
|
||||
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
||||
};
|
||||
module.getters.allTemplates = (state, getters) => ({
|
||||
...getters.templates,
|
||||
|
@ -154,6 +175,8 @@ module.actions.setSyncData = setter('syncData');
|
|||
// Tokens
|
||||
module.getters.tokens = getter('tokens');
|
||||
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) => {
|
||||
// Return the first google token that has the isLogin flag
|
||||
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;
|
||||
|
|
|
@ -18,8 +18,9 @@ const collator = new Intl.Collator(undefined, { sensitivity: 'base' });
|
|||
const compare = (node1, node2) => collator.compare(node1.item.name, node2.item.name);
|
||||
|
||||
class Node {
|
||||
constructor(item, isFolder, isRoot) {
|
||||
constructor(item, locations = [], isFolder = false, isRoot = false) {
|
||||
this.item = item;
|
||||
this.locations = locations;
|
||||
this.isFolder = isFolder;
|
||||
this.isRoot = isRoot;
|
||||
if (isFolder) {
|
||||
|
@ -69,12 +70,16 @@ export default {
|
|||
nodeStructure: (state, getters, rootState, rootGetters) => {
|
||||
const nodeMap = {};
|
||||
rootGetters['folder/items'].forEach((item) => {
|
||||
nodeMap[item.id] = new Node(item, true);
|
||||
nodeMap[item.id] = new Node(item, [], true);
|
||||
});
|
||||
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) => {
|
||||
const node = nodeMap[id];
|
||||
let parentNode = nodeMap[node.item.parentId];
|
||||
|
@ -121,7 +126,7 @@ export default {
|
|||
setDragSourceId: setter('dragSourceId'),
|
||||
setDragTargetId: setter('dragTargetId'),
|
||||
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) {
|
||||
state.newChildNode.item.name = name;
|
||||
|
|
|
@ -2,8 +2,13 @@ const editorMinWidth = 320;
|
|||
const minPadding = 20;
|
||||
const previewButtonWidth = 55;
|
||||
const editorTopPadding = 10;
|
||||
const navigationBarSpaceWidth = 30;
|
||||
const navigationBarLeftWidth = 570;
|
||||
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
|
||||
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 minTitleMaxWidth = 200;
|
||||
|
||||
|
@ -15,7 +20,7 @@ const constants = {
|
|||
statusBarHeight: 20,
|
||||
};
|
||||
|
||||
function computeStyles(state, computedSettings, localSettings, styles = {
|
||||
function computeStyles(state, localSettings, getters, styles = {
|
||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
||||
showStatusBar: localSettings.showStatusBar,
|
||||
showEditor: localSettings.showEditor,
|
||||
|
@ -49,9 +54,10 @@ function computeStyles(state, computedSettings, localSettings, styles = {
|
|||
if (styles.showSidePreview && doublePanelWidth / 2 < editorMinWidth) {
|
||||
styles.showSidePreview = 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.textWidth = 990;
|
||||
if (doublePanelWidth < 1120) {
|
||||
|
@ -89,9 +95,23 @@ function computeStyles(state, computedSettings, localSettings, styles = {
|
|||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||
|
||||
styles.titleMaxWidth = styles.innerWidth - navigationBarSpaceWidth;
|
||||
styles.titleMaxWidth = styles.innerWidth;
|
||||
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.max(styles.titleMaxWidth, minTitleMaxWidth);
|
||||
|
@ -113,9 +133,8 @@ export default {
|
|||
getters: {
|
||||
constants: () => constants,
|
||||
styles: (state, getters, rootState, rootGetters) => {
|
||||
const computedSettings = rootGetters['data/computedSettings'];
|
||||
const localSettings = rootGetters['data/localSettings'];
|
||||
return computeStyles(state, computedSettings, localSettings);
|
||||
return computeStyles(state, localSettings, rootGetters);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,34 +1,47 @@
|
|||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
config: null,
|
||||
stack: [],
|
||||
hidden: false,
|
||||
},
|
||||
mutations: {
|
||||
setConfig: (state, value) => {
|
||||
state.config = value;
|
||||
setStack: (state, value) => {
|
||||
state.stack = value;
|
||||
},
|
||||
setHidden: (state, value) => {
|
||||
state.hidden = value;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
config: state => !state.hidden && state.stack[0],
|
||||
},
|
||||
actions: {
|
||||
open({ commit }, param) {
|
||||
open({ commit, state }, param) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let config = param;
|
||||
if (typeof config === 'string') {
|
||||
config = {
|
||||
type: config,
|
||||
};
|
||||
}
|
||||
const config = typeof param === 'object' ? { ...param } : { type: param };
|
||||
const clean = () => commit('setStack', state.stack.filter((otherConfig => otherConfig !== config)));
|
||||
config.resolve = (result) => {
|
||||
if (config.onResolve) {
|
||||
config.onResolve(result);
|
||||
}
|
||||
commit('setConfig');
|
||||
clean();
|
||||
resolve(result);
|
||||
};
|
||||
config.reject = (error) => {
|
||||
commit('setConfig');
|
||||
clean();
|
||||
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', {
|
||||
|
|
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: {
|
||||
isEmpty: true,
|
||||
isSyncRequested: false,
|
||||
isPublishRequested: false,
|
||||
currentLocation: {},
|
||||
},
|
||||
mutations: {
|
||||
setIsEmpty: setter('isEmpty'),
|
||||
setIsSyncRequested: setter('isSyncRequested'),
|
||||
setIsPublishRequested: setter('isPublishRequested'),
|
||||
setCurrentLocation: setter('currentLocation'),
|
||||
},
|
||||
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) {
|
||||
commit('setIsEmpty', false);
|
||||
}
|
||||
const newQueue = queue
|
||||
.then(cb)
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
})
|
||||
.then(() => {
|
||||
if (newQueue === queue) {
|
||||
commit('setIsEmpty', true);
|
||||
}
|
||||
});
|
||||
.then(() => checkOffline())
|
||||
.then(() => cb()
|
||||
.catch((err) => {
|
||||
console.error(err); // eslint-disable-line no-console
|
||||
checkOffline();
|
||||
dispatch('notification/error', err, { root: true });
|
||||
})
|
||||
.then(() => {
|
||||
if (newQueue === queue) {
|
||||
commit('setIsEmpty', true);
|
||||
}
|
||||
}));
|
||||
queue = newQueue;
|
||||
},
|
||||
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 empty from '../../data/emptySyncLocation';
|
||||
import providerRegistry from '../../services/providers/providerRegistry';
|
||||
|
||||
const module = moduleTemplate(empty);
|
||||
|
||||
|
@ -8,14 +9,26 @@ module.getters = {
|
|||
groupedByFileId: (state, getters) => {
|
||||
const result = {};
|
||||
getters.items.forEach((item) => {
|
||||
const list = result[item.fileId] || [];
|
||||
list.push(item);
|
||||
result[item.fileId] = list;
|
||||
// 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) =>
|
||||
getters.groupedByFileId[rootGetters['file/current'].id] || [],
|
||||
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;
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
<script>
|
||||
var state;
|
||||
var accessToken;
|
||||
var code;
|
||||
var expiresIn;
|
||||
function parse(search) {
|
||||
(search || '').slice(1).split('&').forEach(function (param) {
|
||||
|
@ -14,6 +15,8 @@
|
|||
state = value;
|
||||
} else if (key === 'access_token') {
|
||||
accessToken = value;
|
||||
} else if (key === 'code') {
|
||||
code = value;
|
||||
} else if (key === 'expires_in') {
|
||||
expiresIn = value;
|
||||
}
|
||||
|
@ -25,6 +28,7 @@
|
|||
(window.opener || window.parent).postMessage({
|
||||
state: state,
|
||||
accessToken: accessToken,
|
||||
code: code,
|
||||
expiresIn: expiresIn
|
||||
}, origin);
|
||||
</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"
|
||||
json-stable-stringify "^1.0.1"
|
||||
|
||||
ajv@^5.0.0:
|
||||
ajv@^5.0.0, ajv@^5.1.0:
|
||||
version "5.2.2"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.2.2.tgz#47c68d69e86f5d953103b0074a9430dc63da5e39"
|
||||
dependencies:
|
||||
|
@ -243,7 +243,11 @@ aws-sign2@~0.6.0:
|
|||
version "0.6.0"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
|
||||
|
||||
|
@ -889,6 +893,18 @@ boom@2.x.x:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.8"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292"
|
||||
|
@ -1443,6 +1459,12 @@ cryptiles@2.x.x:
|
|||
dependencies:
|
||||
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:
|
||||
version "3.11.0"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "0.1.0"
|
||||
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"
|
||||
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:
|
||||
version "4.2.1"
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/he/-/he-1.1.1.tgz#93410fd21b009735151f8868c2f271f3427e23fd"
|
||||
|
@ -2852,6 +2902,10 @@ hoek@2.x.x:
|
|||
version "2.16.3"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
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:
|
||||
version "0.0.1"
|
||||
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"
|
||||
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:
|
||||
version "2.1.15"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed"
|
||||
dependencies:
|
||||
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:
|
||||
version "1.3.4"
|
||||
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"
|
||||
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"
|
||||
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"
|
||||
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:
|
||||
version "2.3.0"
|
||||
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"
|
||||
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:
|
||||
version "4.3.4"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853"
|
||||
|
||||
|
@ -5437,6 +5544,12 @@ sntp@1.x.x:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
|
||||
|
@ -5604,7 +5717,7 @@ string_decoder@~1.0.3:
|
|||
dependencies:
|
||||
safe-buffer "~5.1.0"
|
||||
|
||||
stringstream@~0.0.4:
|
||||
stringstream@~0.0.4, stringstream@~0.0.5:
|
||||
version "0.0.5"
|
||||
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
|
||||
|
||||
|
@ -5883,6 +5996,12 @@ tough-cookie@~2.3.0:
|
|||
dependencies:
|
||||
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:
|
||||
version "1.0.0"
|
||||
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"
|
||||
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:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4"
|
||||
|
|
Loading…
Reference in New Issue
Block a user