Fixed google authorization popup
This commit is contained in:
parent
0a39710dac
commit
73cea1879d
@ -14,6 +14,10 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
'html'
|
'html'
|
||||||
],
|
],
|
||||||
|
globals: {
|
||||||
|
"NODE_ENV": false,
|
||||||
|
"VERSION": false
|
||||||
|
},
|
||||||
// check if imports actually resolve
|
// check if imports actually resolve
|
||||||
'settings': {
|
'settings': {
|
||||||
'import/resolver': {
|
'import/resolver': {
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
var path = require('path')
|
var path = require('path')
|
||||||
|
var webpack = require('webpack')
|
||||||
var utils = require('./utils')
|
var utils = require('./utils')
|
||||||
var config = require('../config')
|
var config = require('../config')
|
||||||
var vueLoaderConfig = require('./vue-loader.conf')
|
var vueLoaderConfig = require('./vue-loader.conf')
|
||||||
@ -74,6 +75,9 @@ module.exports = {
|
|||||||
plugins: [
|
plugins: [
|
||||||
new StylelintPlugin({
|
new StylelintPlugin({
|
||||||
files: ['**/*.vue', '**/*.scss']
|
files: ['**/*.vue', '**/*.scss']
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
VERSION: JSON.stringify(require('../package.json').version)
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ module.exports = merge(baseWebpackConfig, {
|
|||||||
devtool: 'source-map',
|
devtool: 'source-map',
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': config.dev.env
|
NODE_ENV: config.dev.env.NODE_ENV
|
||||||
}),
|
}),
|
||||||
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
// https://github.com/glenjamin/webpack-hot-middleware#installation--usage
|
||||||
new webpack.HotModuleReplacementPlugin(),
|
new webpack.HotModuleReplacementPlugin(),
|
||||||
|
@ -28,7 +28,7 @@ var webpackConfig = merge(baseWebpackConfig, {
|
|||||||
plugins: [
|
plugins: [
|
||||||
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
// http://vuejs.github.io/vue-loader/en/workflow/production.html
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
'process.env': env
|
NODE_ENV: env.NODE_ENV
|
||||||
}),
|
}),
|
||||||
new webpack.optimize.UglifyJsPlugin({
|
new webpack.optimize.UglifyJsPlugin({
|
||||||
compress: {
|
compress: {
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="ready" class="app" :class="{'app--loading': loading}">
|
<splash-screen v-if="!ready"></splash-screen>
|
||||||
|
<div v-else class="app" :class="{'app--loading': loading}">
|
||||||
<layout></layout>
|
<layout></layout>
|
||||||
<modal v-if="showModal"></modal>
|
<modal v-if="showModal"></modal>
|
||||||
<notification></notification>
|
<notification></notification>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="app__spash-screen"></div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -13,6 +13,7 @@ import { mapState } from 'vuex';
|
|||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
|
import SplashScreen from './SplashScreen';
|
||||||
|
|
||||||
// Global directives
|
// Global directives
|
||||||
Vue.directive('focus', {
|
Vue.directive('focus', {
|
||||||
@ -26,6 +27,7 @@ export default {
|
|||||||
Layout,
|
Layout,
|
||||||
Modal,
|
Modal,
|
||||||
Notification,
|
Notification,
|
||||||
|
SplashScreen,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
...mapState([
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="layout">
|
<div class="layout">
|
||||||
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar && !styles.showExplorer}">
|
<div class="layout__panel flex flex--row" :class="{'flex--end': styles.showSideBar}">
|
||||||
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
<div class="layout__panel layout__panel--explorer" v-show="styles.showExplorer" :style="{ width: constants.explorerWidth + 'px' }">
|
||||||
<explorer></explorer>
|
<explorer></explorer>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
<file-properties-modal v-if="config.type === 'fileProperties'"></file-properties-modal>
|
||||||
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
<settings-modal v-else-if="config.type === 'settings'"></settings-modal>
|
||||||
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
<templates-modal v-else-if="config.type === 'templates'"></templates-modal>
|
||||||
|
<about-modal v-else-if="config.type === 'about'"></about-modal>
|
||||||
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
<html-export-modal v-else-if="config.type === 'htmlExport'"></html-export-modal>
|
||||||
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
<link-modal v-else-if="config.type === 'link'"></link-modal>
|
||||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||||
@ -42,6 +43,7 @@ import editorEngineSvc from '../services/editorEngineSvc';
|
|||||||
import FilePropertiesModal from './modals/FilePropertiesModal';
|
import FilePropertiesModal from './modals/FilePropertiesModal';
|
||||||
import SettingsModal from './modals/SettingsModal';
|
import SettingsModal from './modals/SettingsModal';
|
||||||
import TemplatesModal from './modals/TemplatesModal';
|
import TemplatesModal from './modals/TemplatesModal';
|
||||||
|
import AboutModal from './modals/AboutModal';
|
||||||
import HtmlExportModal from './modals/HtmlExportModal';
|
import HtmlExportModal from './modals/HtmlExportModal';
|
||||||
import LinkModal from './modals/LinkModal';
|
import LinkModal from './modals/LinkModal';
|
||||||
import ImageModal from './modals/ImageModal';
|
import ImageModal from './modals/ImageModal';
|
||||||
@ -69,6 +71,7 @@ export default {
|
|||||||
FilePropertiesModal,
|
FilePropertiesModal,
|
||||||
SettingsModal,
|
SettingsModal,
|
||||||
TemplatesModal,
|
TemplatesModal,
|
||||||
|
AboutModal,
|
||||||
HtmlExportModal,
|
HtmlExportModal,
|
||||||
LinkModal,
|
LinkModal,
|
||||||
ImageModal,
|
ImageModal,
|
||||||
|
@ -22,13 +22,13 @@
|
|||||||
<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">
|
<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>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</a>
|
</a>
|
||||||
<button class="navigation-bar__button navigation-bar__button--sync button" v-if="isSyncPossible" :disabled="isSyncRequested || offline" @click="requestSync">
|
<button class="navigation-bar__button navigation-bar__button--sync button" :disabled="!isSyncPossible || isSyncRequested || offline" @click="requestSync">
|
||||||
<icon-sync></icon-sync>
|
<icon-sync></icon-sync>
|
||||||
</button>
|
</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">
|
<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>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</a>
|
</a>
|
||||||
<button class="navigation-bar__button navigation-bar__button--publish button" v-if="publishLocations.length" :disabled="isPublishRequested || offline" @click="requestPublish">
|
<button class="navigation-bar__button navigation-bar__button--publish button" :disabled="!publishLocations.length || isPublishRequested || offline" @click="requestPublish">
|
||||||
<icon-upload></icon-upload>
|
<icon-upload></icon-upload>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -148,12 +148,12 @@ export default {
|
|||||||
'toggleSideBar',
|
'toggleSideBar',
|
||||||
]),
|
]),
|
||||||
requestSync() {
|
requestSync() {
|
||||||
if (!this.isSyncRequested) {
|
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||||
syncSvc.requestSync();
|
syncSvc.requestSync();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestPublish() {
|
requestPublish() {
|
||||||
if (!this.isPublishRequested) {
|
if (this.publishLocations.length && !this.isPublishRequested) {
|
||||||
publishSvc.requestPublish();
|
publishSvc.requestPublish();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -90,7 +90,7 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
hr {
|
hr {
|
||||||
margin: 10px;
|
margin: 10px 15px;
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
src/components/SplashScreen.vue
Normal file
22
src/components/SplashScreen.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<div class="splash-screen">
|
||||||
|
<div class="splash-screen__inner background-logo"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.splash-screen {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 25px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.splash-screen__inner {
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 600px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
39
src/components/UserImage.vue
Normal file
39
src/components/UserImage.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-image" :style="{'background-image': url}">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import googleHelper from '../services/providers/helpers/googleHelper';
|
||||||
|
|
||||||
|
const promised = {};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['userId'],
|
||||||
|
computed: {
|
||||||
|
url() {
|
||||||
|
const userInfo = this.$store.state.userInfo.itemMap[this.userId];
|
||||||
|
return userInfo && `url('${userInfo.imageUrl}')`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (!promised[this.userId] && !this.$store.state.offline) {
|
||||||
|
promised[this.userId] = true;
|
||||||
|
googleHelper.getUser(this.userId)
|
||||||
|
.catch(() => {
|
||||||
|
promised[this.userId] = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.user-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
</style>
|
@ -90,75 +90,13 @@ textarea {
|
|||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover {
|
||||||
opacity: 0.5;
|
opacity: 0.33;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
cursor: inherit;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-entry {
|
|
||||||
margin: 1em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__label {
|
|
||||||
display: block;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #a0a0a0;
|
|
||||||
|
|
||||||
.form-entry--focused & {
|
|
||||||
color: darken($link-color, 10%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__field {
|
|
||||||
border: 1px solid #d8d8d8;
|
|
||||||
border-radius: $border-radius-base;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
.form-entry--focused & {
|
|
||||||
border-color: $link-color;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__actions {
|
|
||||||
text-align: right;
|
|
||||||
margin: 0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__button {
|
|
||||||
width: 38px;
|
|
||||||
height: 38px;
|
|
||||||
padding: 6px;
|
|
||||||
display: inline-block;
|
|
||||||
background-color: transparent;
|
|
||||||
opacity: 0.75;
|
|
||||||
|
|
||||||
&:active,
|
|
||||||
&:focus,
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background-color: rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__radio,
|
|
||||||
.form-entry__checkbox {
|
|
||||||
margin: 0.25em 1em;
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin-right: 0.25em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-entry__info {
|
|
||||||
font-size: 0.75em;
|
|
||||||
opacity: 0.5;
|
|
||||||
line-height: 1.4;
|
|
||||||
margin: 0.25em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.textfield {
|
.textfield {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
@ -289,3 +227,8 @@ textarea {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.logo-background {
|
||||||
|
background: no-repeat center url('../assets/logo.svg');
|
||||||
|
background-size: contain;
|
||||||
|
}
|
||||||
|
@ -5,6 +5,12 @@
|
|||||||
<div>Sign in with Google</div>
|
<div>Sign in with Google</div>
|
||||||
<span>Back up and sync all your files, folders and settings.</span>
|
<span>Back up and sync all your files, folders and settings.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<div v-else class="menu-entry flex flex--row flex--align-center">
|
||||||
|
<div class="menu-entry__icon menu-entry__icon--image">
|
||||||
|
<user-image :user-id="loginToken.sub"></user-image>
|
||||||
|
</div>
|
||||||
|
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('sync')">
|
<menu-entry @click.native="setPanel('sync')">
|
||||||
<icon-sync slot="icon"></icon-sync>
|
<icon-sync slot="icon"></icon-sync>
|
||||||
@ -49,12 +55,14 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './MenuEntry';
|
||||||
|
import UserImage from '../UserImage';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
|
UserImage,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('data', [
|
...mapGetters('data', [
|
||||||
|
@ -10,6 +10,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
.menu-entry {
|
.menu-entry {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
@ -30,4 +32,9 @@
|
|||||||
margin-right: 12px;
|
margin-right: 12px;
|
||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-entry__icon--image {
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -16,13 +16,16 @@
|
|||||||
<span>Sign out and clean local data.</span>
|
<span>Sign out and clean local data.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
|
<menu-entry @click.native="about">
|
||||||
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
|
<span>About StackEdit</span>
|
||||||
|
</menu-entry>
|
||||||
<a href="editor" target="_blank" class="menu-entry button flex flex--row flex--align-center">
|
<a href="editor" target="_blank" class="menu-entry button flex flex--row flex--align-center">
|
||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
<icon-alert></icon-alert>
|
<icon-open-in-new></icon-open-in-new>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex--column">
|
<div class="flex flex--column">
|
||||||
<div>StackEdit v4</div>
|
<span>Go back to StackEdit 4</span>
|
||||||
<span>Deprecated.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@ -49,6 +52,9 @@ export default {
|
|||||||
return this.$store.dispatch('modal/reset')
|
return this.$store.dispatch('modal/reset')
|
||||||
.then(() => localDbSvc.removeDb());
|
.then(() => localDbSvc.removeDb());
|
||||||
},
|
},
|
||||||
|
about() {
|
||||||
|
return this.$store.dispatch('modal/open', 'about');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
54
src/components/modals/AboutModal.vue
Normal file
54
src/components/modals/AboutModal.vue
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<div class="modal__inner-1 modal__inner-1--about-modal">
|
||||||
|
<div class="modal__inner-2">
|
||||||
|
<div class="logo-background"></div>
|
||||||
|
<div class="app-version">v{{version}} — © 2017 Benoit Schweblin</div>
|
||||||
|
<hr>
|
||||||
|
<a target="_blank" href="https://github.com/benweet/stackedit/">StackEdit on GitHub</a> /
|
||||||
|
<a target="_blank" href="https://github.com/benweet/stackedit/issues">issue tracker</a>
|
||||||
|
<br>
|
||||||
|
<a target="_blank" href="https://chrome.google.com/webstore/detail/stackedit/iiooodelglhkcpgbajoejffhijaclcdg">Chrome app</a> — thanks for your review!
|
||||||
|
<br>
|
||||||
|
<a target="_blank" href="https://twitter.com/stackedit/">StackEdit on Twitter</a>
|
||||||
|
<hr>
|
||||||
|
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.resolve()">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data: () => ({
|
||||||
|
version: VERSION,
|
||||||
|
}),
|
||||||
|
computed: mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.modal__inner-1--about-modal {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.logo-background {
|
||||||
|
height: 75px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-version {
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
width: 160px;
|
||||||
|
max-width: 100%;
|
||||||
|
margin: 1.5em auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -23,4 +23,67 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.form-entry {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-entry__label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #a0a0a0;
|
||||||
|
|
||||||
|
.form-entry--focused & {
|
||||||
|
color: darken($link-color, 10%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-entry__field {
|
||||||
|
border: 1px solid #d8d8d8;
|
||||||
|
border-radius: $border-radius-base;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.form-entry--focused & {
|
||||||
|
border-color: $link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-entry__actions {
|
||||||
|
text-align: right;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-entry__button {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
padding: 6px;
|
||||||
|
display: inline-block;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
|
import Provider from './Provider';
|
||||||
import FormatBold from './FormatBold';
|
import FormatBold from './FormatBold';
|
||||||
import FormatItalic from './FormatItalic';
|
import FormatItalic from './FormatItalic';
|
||||||
import FormatQuoteClose from './FormatQuoteClose';
|
import FormatQuoteClose from './FormatQuoteClose';
|
||||||
@ -40,9 +41,8 @@ import OpenInNew from './OpenInNew';
|
|||||||
import Information from './Information';
|
import Information from './Information';
|
||||||
import Alert from './Alert';
|
import Alert from './Alert';
|
||||||
import SignalOff from './SignalOff';
|
import SignalOff from './SignalOff';
|
||||||
// Providers
|
|
||||||
import Provider from './Provider';
|
|
||||||
|
|
||||||
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
Vue.component('iconFormatItalic', FormatItalic);
|
Vue.component('iconFormatItalic', FormatItalic);
|
||||||
Vue.component('iconFormatQuoteClose', FormatQuoteClose);
|
Vue.component('iconFormatQuoteClose', FormatQuoteClose);
|
||||||
@ -84,5 +84,3 @@ Vue.component('iconOpenInNew', OpenInNew);
|
|||||||
Vue.component('iconInformation', Information);
|
Vue.component('iconInformation', Information);
|
||||||
Vue.component('iconAlert', Alert);
|
Vue.component('iconAlert', Alert);
|
||||||
Vue.component('iconSignalOff', SignalOff);
|
Vue.component('iconSignalOff', SignalOff);
|
||||||
// Providers
|
|
||||||
Vue.component('iconProvider', Provider);
|
|
||||||
|
@ -6,7 +6,7 @@ import './icons/';
|
|||||||
import App from './components/App';
|
import App from './components/App';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (NODE_ENV === 'production') {
|
||||||
OfflinePluginRuntime.install();
|
OfflinePluginRuntime.install();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
266
src/services/networkSvc.js
Normal file
266
src/services/networkSvc.js
Normal file
@ -0,0 +1,266 @@
|
|||||||
|
import utils from './utils';
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
const scriptLoadingPromises = Object.create(null);
|
||||||
|
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
|
||||||
|
const networkTimeout = 30 * 1000; // 30 sec
|
||||||
|
let isConnectionDown = false;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
loadScript(url) {
|
||||||
|
if (!scriptLoadingPromises[url]) {
|
||||||
|
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
|
||||||
|
const script = document.createElement('script');
|
||||||
|
script.onload = resolve;
|
||||||
|
script.onerror = () => {
|
||||||
|
scriptLoadingPromises[url] = null;
|
||||||
|
reject();
|
||||||
|
};
|
||||||
|
script.src = url;
|
||||||
|
document.head.appendChild(script);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return scriptLoadingPromises[url];
|
||||||
|
},
|
||||||
|
startOauth2(url, params = {}, silent = false) {
|
||||||
|
// Build the authorize URL
|
||||||
|
const state = utils.uid();
|
||||||
|
params.state = state;
|
||||||
|
params.redirect_uri = utils.oauth2RedirectUri;
|
||||||
|
const authorizeUrl = utils.addQueryParams(url, params);
|
||||||
|
|
||||||
|
let iframeElt;
|
||||||
|
let wnd;
|
||||||
|
if (silent) {
|
||||||
|
// Use an iframe as wnd for silent mode
|
||||||
|
iframeElt = document.createElement('iframe');
|
||||||
|
iframeElt.style.position = 'absolute';
|
||||||
|
iframeElt.style.left = '-9999px';
|
||||||
|
iframeElt.src = authorizeUrl;
|
||||||
|
document.body.appendChild(iframeElt);
|
||||||
|
wnd = iframeElt.contentWindow;
|
||||||
|
} else {
|
||||||
|
// Open a tab otherwise
|
||||||
|
wnd = window.open(authorizeUrl);
|
||||||
|
if (!wnd) {
|
||||||
|
return Promise.reject('The authorize window was blocked.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let checkClosedInterval;
|
||||||
|
let closeTimeout;
|
||||||
|
let msgHandler;
|
||||||
|
let clean = () => {
|
||||||
|
clearInterval(checkClosedInterval);
|
||||||
|
if (!silent && !wnd.closed) {
|
||||||
|
wnd.close();
|
||||||
|
}
|
||||||
|
if (iframeElt) {
|
||||||
|
document.body.removeChild(iframeElt);
|
||||||
|
}
|
||||||
|
clearTimeout(closeTimeout);
|
||||||
|
window.removeEventListener('message', msgHandler);
|
||||||
|
clean = () => Promise.resolve(); // Prevent from cleaning several times
|
||||||
|
return Promise.resolve();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
iframeElt.onerror = () => clean()
|
||||||
|
.then(() => reject('Unknown error.'));
|
||||||
|
closeTimeout = setTimeout(
|
||||||
|
() => clean()
|
||||||
|
.then(() => {
|
||||||
|
isConnectionDown = true;
|
||||||
|
store.commit('setOffline', true);
|
||||||
|
store.commit('updateLastOfflineCheck');
|
||||||
|
reject('You are offline.');
|
||||||
|
}),
|
||||||
|
networkTimeout);
|
||||||
|
} else {
|
||||||
|
closeTimeout = setTimeout(
|
||||||
|
() => clean()
|
||||||
|
.then(() => reject('Timeout.')),
|
||||||
|
oauth2AuthorizationTimeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean()
|
||||||
|
.then(() => {
|
||||||
|
const data = {};
|
||||||
|
`${event.data}`.slice(1).split('&').forEach((param) => {
|
||||||
|
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||||
|
data[key] = value;
|
||||||
|
});
|
||||||
|
if (data.error || data.state !== state) {
|
||||||
|
reject('Could not get required authorization.');
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
accessToken: data.access_token,
|
||||||
|
code: data.code,
|
||||||
|
expiresIn: data.expires_in,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('message', msgHandler);
|
||||||
|
if (!silent) {
|
||||||
|
checkClosedInterval = setInterval(() => wnd.closed && clean()
|
||||||
|
.then(() => reject('Authorize window was closed.')), 250);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
request(configParam, offlineCheck = false) {
|
||||||
|
let retryAfter = 500; // 500 ms
|
||||||
|
const maxRetryAfter = 30 * 1000; // 30 sec
|
||||||
|
const config = Object.assign({}, configParam);
|
||||||
|
config.timeout = config.timeout || networkTimeout;
|
||||||
|
config.headers = Object.assign({}, config.headers);
|
||||||
|
if (config.body && typeof config.body === 'object') {
|
||||||
|
config.body = JSON.stringify(config.body);
|
||||||
|
config.headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseHeaders(xhr) {
|
||||||
|
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
||||||
|
return pairs.reduce((headers, header) => {
|
||||||
|
const split = header.trim().split(':');
|
||||||
|
const key = split.shift().trim().toLowerCase();
|
||||||
|
const value = split.join(':').trim();
|
||||||
|
headers[key] = value;
|
||||||
|
return headers;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isRetriable(err) {
|
||||||
|
if (err.status === 403) {
|
||||||
|
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
||||||
|
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
|
||||||
|
}
|
||||||
|
return err.status === 429 || (err.status >= 500 && err.status < 600);
|
||||||
|
}
|
||||||
|
|
||||||
|
const attempt =
|
||||||
|
() => new Promise((resolve, reject) => {
|
||||||
|
if (offlineCheck) {
|
||||||
|
store.commit('updateLastOfflineCheck');
|
||||||
|
}
|
||||||
|
const xhr = new window.XMLHttpRequest();
|
||||||
|
let timeoutId;
|
||||||
|
|
||||||
|
xhr.onload = () => {
|
||||||
|
if (offlineCheck) {
|
||||||
|
store.commit('setOffline', false);
|
||||||
|
}
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
const result = {
|
||||||
|
status: xhr.status,
|
||||||
|
headers: parseHeaders(xhr),
|
||||||
|
body: xhr.responseText,
|
||||||
|
};
|
||||||
|
if (!config.raw) {
|
||||||
|
try {
|
||||||
|
result.body = JSON.parse(result.body);
|
||||||
|
} catch (e) {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result.status >= 200 && result.status < 300) {
|
||||||
|
resolve(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reject(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
xhr.onerror = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
if (offlineCheck) {
|
||||||
|
store.commit('setOffline', true);
|
||||||
|
reject('You are offline.');
|
||||||
|
} else {
|
||||||
|
reject('Network request failed.');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
timeoutId = setTimeout(() => {
|
||||||
|
xhr.abort();
|
||||||
|
if (offlineCheck) {
|
||||||
|
store.commit('setOffline', true);
|
||||||
|
reject('You are offline.');
|
||||||
|
} else {
|
||||||
|
reject('Network request timeout.');
|
||||||
|
}
|
||||||
|
}, config.timeout);
|
||||||
|
|
||||||
|
const url = utils.addQueryParams(config.url, config.params);
|
||||||
|
xhr.open(config.method || 'GET', url);
|
||||||
|
Object.keys(config.headers).forEach((key) => {
|
||||||
|
const value = config.headers[key];
|
||||||
|
if (value) {
|
||||||
|
xhr.setRequestHeader(key, `${value}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
xhr.send(config.body || null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// Try again later in case of retriable error
|
||||||
|
if (isRetriable(err) && retryAfter < maxRetryAfter) {
|
||||||
|
return new Promise(
|
||||||
|
(resolve) => {
|
||||||
|
setTimeout(resolve, retryAfter);
|
||||||
|
// Exponential backoff
|
||||||
|
retryAfter *= 2;
|
||||||
|
})
|
||||||
|
.then(attempt);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return attempt();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function checkOffline() {
|
||||||
|
const isBrowserOffline = window.navigator.onLine === false;
|
||||||
|
if (!isBrowserOffline &&
|
||||||
|
store.state.lastOfflineCheck + networkTimeout + 5000 < Date.now() &&
|
||||||
|
utils.isUserActive()
|
||||||
|
) {
|
||||||
|
store.commit('updateLastOfflineCheck');
|
||||||
|
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);
|
||||||
|
script.onerror = cleaner(reject);
|
||||||
|
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), networkTimeout);
|
||||||
|
} catch (e) {
|
||||||
|
reject(e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
isConnectionDown = false;
|
||||||
|
}, () => {
|
||||||
|
isConnectionDown = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const offline = isBrowserOffline || isConnectionDown;
|
||||||
|
if (store.state.offline !== offline) {
|
||||||
|
store.commit('setOffline', offline);
|
||||||
|
if (offline) {
|
||||||
|
store.dispatch('notification/error', 'You are offline.');
|
||||||
|
} else {
|
||||||
|
store.dispatch('notification/info', 'You are back online!');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
utils.setInterval(checkOffline, 1000);
|
||||||
|
window.addEventListener('online', checkOffline);
|
||||||
|
window.addEventListener('offline', checkOffline);
|
@ -1,4 +1,4 @@
|
|||||||
import utils from '../../utils';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
let Dropbox;
|
let Dropbox;
|
||||||
@ -10,7 +10,7 @@ const getAppKey = (fullAccess) => {
|
|||||||
return 'sw0hlixhr8q1xk0';
|
return 'sw0hlixhr8q1xk0';
|
||||||
};
|
};
|
||||||
|
|
||||||
const request = (token, options, args) => utils.request({
|
const request = (token, options, args) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
@ -23,7 +23,7 @@ const request = (token, options, args) => utils.request({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
startOauth2(fullAccess, sub = null, silent = false) {
|
startOauth2(fullAccess, sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
'https://www.dropbox.com/oauth2/authorize', {
|
'https://www.dropbox.com/oauth2/authorize', {
|
||||||
client_id: getAppKey(fullAccess),
|
client_id: getAppKey(fullAccess),
|
||||||
response_type: 'token',
|
response_type: 'token',
|
||||||
@ -54,7 +54,7 @@ export default {
|
|||||||
if (Dropbox) {
|
if (Dropbox) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return utils.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
|
return networkSvc.loadScript('https://www.dropbox.com/static/api/2/dropins.js')
|
||||||
.then(() => {
|
.then(() => {
|
||||||
Dropbox = window.Dropbox;
|
Dropbox = window.Dropbox;
|
||||||
});
|
});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import utils from '../../utils';
|
import utils from '../../utils';
|
||||||
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
let clientId = 'cbf0cf25cfd026be23e1';
|
let clientId = 'cbf0cf25cfd026be23e1';
|
||||||
@ -7,7 +8,7 @@ if (utils.origin === 'https://stackedit.io') {
|
|||||||
}
|
}
|
||||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||||
|
|
||||||
const request = (token, options) => utils.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
@ -21,13 +22,13 @@ const request = (token, options) => utils.request({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
startOauth2(scopes, sub = null, silent = false) {
|
startOauth2(scopes, sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
'https://github.com/login/oauth/authorize', {
|
'https://github.com/login/oauth/authorize', {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
scope: scopes.join(' '),
|
scope: scopes.join(' '),
|
||||||
}, silent)
|
}, silent)
|
||||||
// Exchange code with token
|
// Exchange code with token
|
||||||
.then(data => utils.request({
|
.then(data => networkSvc.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'oauth2/githubToken',
|
url: 'oauth2/githubToken',
|
||||||
params: {
|
params: {
|
||||||
@ -37,7 +38,7 @@ export default {
|
|||||||
})
|
})
|
||||||
.then(res => res.body))
|
.then(res => res.body))
|
||||||
// Call the user info endpoint
|
// Call the user info endpoint
|
||||||
.then(accessToken => utils.request({
|
.then(accessToken => networkSvc.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://api.github.com/user',
|
url: 'https://api.github.com/user',
|
||||||
params: {
|
params: {
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
import utils from '../../utils';
|
import utils from '../../utils';
|
||||||
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
|
const clientId = '241271498917-t4t7d07qis7oc0ahaskbif3ft6tk63cd.apps.googleusercontent.com';
|
||||||
|
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||||
const appsDomain = null;
|
const appsDomain = null;
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
||||||
let gapi;
|
let gapi;
|
||||||
@ -17,74 +19,100 @@ const photosScopes = ['https://www.googleapis.com/auth/photos'];
|
|||||||
|
|
||||||
const libraries = ['picker'];
|
const libraries = ['picker'];
|
||||||
|
|
||||||
const request = (token, options) => utils.request({
|
|
||||||
...options,
|
|
||||||
headers: {
|
|
||||||
...options.headers || {},
|
|
||||||
Authorization: `Bearer ${token.accessToken}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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(() => {
|
|
||||||
const options = {
|
|
||||||
method: 'POST',
|
|
||||||
url: 'https://www.googleapis.com/drive/v3/files',
|
|
||||||
};
|
|
||||||
const metadata = { name };
|
|
||||||
if (fileId) {
|
|
||||||
options.method = 'PATCH';
|
|
||||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
|
||||||
} else if (parents) {
|
|
||||||
// Parents field is not patchable
|
|
||||||
metadata.parents = parents;
|
|
||||||
}
|
|
||||||
if (media) {
|
|
||||||
const boundary = `-------${utils.uid()}`;
|
|
||||||
const delimiter = `\r\n--${boundary}\r\n`;
|
|
||||||
const closeDelimiter = `\r\n--${boundary}--`;
|
|
||||||
let multipartRequestBody = '';
|
|
||||||
multipartRequestBody += delimiter;
|
|
||||||
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
|
||||||
multipartRequestBody += JSON.stringify(metadata);
|
|
||||||
multipartRequestBody += delimiter;
|
|
||||||
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
|
||||||
multipartRequestBody += media;
|
|
||||||
multipartRequestBody += closeDelimiter;
|
|
||||||
options.url = options.url.replace(
|
|
||||||
'https://www.googleapis.com/',
|
|
||||||
'https://www.googleapis.com/upload/');
|
|
||||||
return request(refreshedToken, {
|
|
||||||
...options,
|
|
||||||
params: {
|
|
||||||
uploadType: 'multipart',
|
|
||||||
},
|
|
||||||
headers: {
|
|
||||||
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
|
||||||
},
|
|
||||||
body: multipartRequestBody,
|
|
||||||
}).then(res => res.body);
|
|
||||||
}
|
|
||||||
return request(refreshedToken, {
|
|
||||||
...options,
|
|
||||||
body: metadata,
|
|
||||||
}).then(res => res.body);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadFile(refreshedToken, id) {
|
|
||||||
return request(refreshedToken, {
|
|
||||||
method: 'GET',
|
|
||||||
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
|
||||||
raw: true,
|
|
||||||
}).then(res => res.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
request(token, options) {
|
||||||
|
return networkSvc.request({
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
...options.headers || {},
|
||||||
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
.catch((err) => {
|
||||||
|
const reason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
||||||
|
if (reason === 'authError') {
|
||||||
|
// Mark the token as revoked and get a new one
|
||||||
|
store.dispatch('data/setGoogleToken', {
|
||||||
|
...token,
|
||||||
|
expiresOn: 0,
|
||||||
|
});
|
||||||
|
// Refresh token and retry
|
||||||
|
return this.refreshToken(token.scopes, token)
|
||||||
|
.then(refreshedToken => this.request(refreshedToken, options));
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
uploadFileInternal(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, make sure it's not too late
|
||||||
|
.then(ifNotTooLate(() => {
|
||||||
|
const options = {
|
||||||
|
method: 'POST',
|
||||||
|
url: 'https://www.googleapis.com/drive/v3/files',
|
||||||
|
};
|
||||||
|
const metadata = { name };
|
||||||
|
if (fileId) {
|
||||||
|
options.method = 'PATCH';
|
||||||
|
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||||
|
} else if (parents) {
|
||||||
|
// Parents field is not patchable
|
||||||
|
metadata.parents = parents;
|
||||||
|
}
|
||||||
|
if (media) {
|
||||||
|
const boundary = `-------${utils.uid()}`;
|
||||||
|
const delimiter = `\r\n--${boundary}\r\n`;
|
||||||
|
const closeDelimiter = `\r\n--${boundary}--`;
|
||||||
|
let multipartRequestBody = '';
|
||||||
|
multipartRequestBody += delimiter;
|
||||||
|
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
||||||
|
multipartRequestBody += JSON.stringify(metadata);
|
||||||
|
multipartRequestBody += delimiter;
|
||||||
|
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
||||||
|
multipartRequestBody += media;
|
||||||
|
multipartRequestBody += closeDelimiter;
|
||||||
|
options.url = options.url.replace(
|
||||||
|
'https://www.googleapis.com/',
|
||||||
|
'https://www.googleapis.com/upload/');
|
||||||
|
return this.request(refreshedToken, {
|
||||||
|
...options,
|
||||||
|
params: {
|
||||||
|
uploadType: 'multipart',
|
||||||
|
},
|
||||||
|
headers: {
|
||||||
|
'Content-Type': `multipart/mixed; boundary="${boundary}"`,
|
||||||
|
},
|
||||||
|
body: multipartRequestBody,
|
||||||
|
}).then(res => res.body);
|
||||||
|
}
|
||||||
|
return this.request(refreshedToken, {
|
||||||
|
...options,
|
||||||
|
body: metadata,
|
||||||
|
}).then(res => res.body);
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
downloadFileInternal(refreshedToken, id) {
|
||||||
|
return this.request(refreshedToken, {
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://www.googleapis.com/drive/v3/files/${id}?alt=media`,
|
||||||
|
raw: true,
|
||||||
|
}).then(res => res.body);
|
||||||
|
},
|
||||||
|
getUser(userId) {
|
||||||
|
return networkSvc.request({
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://www.googleapis.com/plus/v1/people/${userId}?key=${apiKey}`,
|
||||||
|
}, true)
|
||||||
|
.then((res) => {
|
||||||
|
store.commit('userInfo/addItem', {
|
||||||
|
id: res.body.id,
|
||||||
|
imageUrl: res.body.image.url,
|
||||||
|
});
|
||||||
|
return res.body;
|
||||||
|
});
|
||||||
|
},
|
||||||
startOauth2(scopes, sub = null, silent = false) {
|
startOauth2(scopes, sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
'https://accounts.google.com/o/oauth2/v2/auth', {
|
'https://accounts.google.com/o/oauth2/v2/auth', {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
response_type: 'token',
|
response_type: 'token',
|
||||||
@ -94,13 +122,13 @@ export default {
|
|||||||
prompt: silent ? 'none' : null,
|
prompt: silent ? 'none' : null,
|
||||||
}, silent)
|
}, silent)
|
||||||
// Call the token info endpoint
|
// Call the token info endpoint
|
||||||
.then(data => utils.request({
|
.then(data => networkSvc.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
url: 'https://www.googleapis.com/oauth2/v3/tokeninfo',
|
||||||
params: {
|
params: {
|
||||||
access_token: data.accessToken,
|
access_token: data.accessToken,
|
||||||
},
|
},
|
||||||
}).then((res) => {
|
}, true).then((res) => {
|
||||||
// Check the returned client ID consistency
|
// Check the returned client ID consistency
|
||||||
if (res.body.aud !== clientId) {
|
if (res.body.aud !== clientId) {
|
||||||
throw new Error('Client ID inconsistent.');
|
throw new Error('Client ID inconsistent.');
|
||||||
@ -125,29 +153,27 @@ export default {
|
|||||||
};
|
};
|
||||||
}))
|
}))
|
||||||
// Call the user info endpoint
|
// Call the user info endpoint
|
||||||
.then(token => request(token, {
|
.then(token => this.getUser(token.sub)
|
||||||
method: 'GET',
|
.then((user) => {
|
||||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
// Add name to token
|
||||||
}).then((res) => {
|
token.name = user.displayName;
|
||||||
// Add name to token
|
const existingToken = store.getters['data/googleTokens'][token.sub];
|
||||||
token.name = res.body.displayName;
|
if (existingToken) {
|
||||||
const existingToken = store.getters['data/googleTokens'][token.sub];
|
// We probably retrieved a new token with restricted scopes.
|
||||||
if (existingToken) {
|
// That's no problem, token will be refreshed later with merged scopes.
|
||||||
// We probably retrieved a new token with restricted scopes.
|
// Save flags
|
||||||
// That's no problem, token will be refreshed later with merged scopes.
|
token.isLogin = existingToken.isLogin || token.isLogin;
|
||||||
// Save flags
|
token.isDrive = existingToken.isDrive || token.isDrive;
|
||||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
||||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
||||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
||||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
// Save nextPageToken
|
||||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
token.nextPageToken = existingToken.nextPageToken;
|
||||||
// Save nextPageToken
|
}
|
||||||
token.nextPageToken = existingToken.nextPageToken;
|
// Add token to googleTokens
|
||||||
}
|
store.dispatch('data/setGoogleToken', token);
|
||||||
// Add token to googleTokens
|
return token;
|
||||||
store.dispatch('data/setGoogleToken', token);
|
}));
|
||||||
return token;
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
refreshToken(scopes, token) {
|
refreshToken(scopes, token) {
|
||||||
const sub = token.sub;
|
const sub = token.sub;
|
||||||
@ -168,18 +194,22 @@ export default {
|
|||||||
// Try to get a new token in background
|
// Try to get a new token in background
|
||||||
return this.startOauth2(mergedScopes, sub, true)
|
return this.startOauth2(mergedScopes, sub, true)
|
||||||
// If it fails try to popup a window
|
// If it fails try to popup a window
|
||||||
.catch(() => utils.checkOnline() // Check that we are online, silent mode is a hack
|
.catch((err) => {
|
||||||
.then(() => store.dispatch('modal/providerRedirection', {
|
if (store.state.offline) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
return store.dispatch('modal/providerRedirection', {
|
||||||
providerName: 'Google',
|
providerName: 'Google',
|
||||||
onResolve: () => this.startOauth2(mergedScopes, sub),
|
onResolve: () => this.startOauth2(mergedScopes, sub),
|
||||||
})));
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
loadClientScript() {
|
loadClientScript() {
|
||||||
if (gapi) {
|
if (gapi) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return utils.loadScript('https://apis.google.com/js/api.js')
|
return networkSvc.loadScript('https://apis.google.com/js/api.js')
|
||||||
.then(() => Promise.all(libraries.map(
|
.then(() => Promise.all(libraries.map(
|
||||||
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
library => new Promise((resolve, reject) => window.gapi.load(library, {
|
||||||
callback: resolve,
|
callback: resolve,
|
||||||
@ -210,7 +240,7 @@ export default {
|
|||||||
};
|
};
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(driveAppDataScopes, token)
|
||||||
.then((refreshedToken) => {
|
.then((refreshedToken) => {
|
||||||
const getPage = (pageToken = '1') => request(refreshedToken, {
|
const getPage = (pageToken = '1') => this.request(refreshedToken, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'https://www.googleapis.com/drive/v3/changes',
|
url: 'https://www.googleapis.com/drive/v3/changes',
|
||||||
params: {
|
params: {
|
||||||
@ -233,26 +263,26 @@ export default {
|
|||||||
},
|
},
|
||||||
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(getDriveScopes(token), token)
|
||||||
.then(refreshedToken => uploadFile(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(driveAppDataScopes, token)
|
||||||
.then(refreshedToken => uploadFile(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
downloadFile(token, id) {
|
downloadFile(token, id) {
|
||||||
return this.refreshToken(getDriveScopes(token), token)
|
return this.refreshToken(getDriveScopes(token), token)
|
||||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||||
},
|
},
|
||||||
downloadAppDataFile(token, id) {
|
downloadAppDataFile(token, id) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(driveAppDataScopes, token)
|
||||||
.then(refreshedToken => downloadFile(refreshedToken, id));
|
.then(refreshedToken => this.downloadFileInternal(refreshedToken, id));
|
||||||
},
|
},
|
||||||
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
removeAppDataFile(token, id, ifNotTooLate = cb => res => cb(res)) {
|
||||||
return this.refreshToken(driveAppDataScopes, token)
|
return this.refreshToken(driveAppDataScopes, token)
|
||||||
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
// Refreshing a token can take a while if an oauth window pops up, so check if it's too late
|
||||||
.then(ifNotTooLate(refreshedToken => request(refreshedToken, {
|
.then(ifNotTooLate(refreshedToken => this.request(refreshedToken, {
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||||
})));
|
})));
|
||||||
@ -266,7 +296,7 @@ export default {
|
|||||||
if (blogId) {
|
if (blogId) {
|
||||||
return blogId;
|
return blogId;
|
||||||
}
|
}
|
||||||
return request(refreshedToken, {
|
return this.request(refreshedToken, {
|
||||||
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
|
url: 'https://www.googleapis.com/blogger/v3/blogs/byurl',
|
||||||
params: {
|
params: {
|
||||||
url: blogUrl,
|
url: blogUrl,
|
||||||
@ -299,7 +329,7 @@ export default {
|
|||||||
options.url += postId;
|
options.url += postId;
|
||||||
options.body.id = postId;
|
options.body.id = postId;
|
||||||
}
|
}
|
||||||
return request(refreshedToken, options)
|
return this.request(refreshedToken, options)
|
||||||
.then(res => res.body);
|
.then(res => res.body);
|
||||||
})
|
})
|
||||||
.then((post) => {
|
.then((post) => {
|
||||||
@ -319,7 +349,7 @@ export default {
|
|||||||
options.params.publishDate = published.toISOString();
|
options.params.publishDate = published.toISOString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return request(refreshedToken, options)
|
return this.request(refreshedToken, options)
|
||||||
.then(res => res.body);
|
.then(res => res.body);
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import utils from '../../utils';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
const clientId = '23361';
|
const clientId = '23361';
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
||||||
|
|
||||||
const request = (token, options) => utils.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
@ -14,7 +14,7 @@ const request = (token, options) => utils.request({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
startOauth2(sub = null, silent = false) {
|
startOauth2(sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
'https://public-api.wordpress.com/oauth2/authorize', {
|
'https://public-api.wordpress.com/oauth2/authorize', {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
response_type: 'token',
|
response_type: 'token',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import utils from '../../utils';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
|
||||||
const request = (token, options) => utils.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
...options.headers || {},
|
...options.headers || {},
|
||||||
@ -11,7 +11,7 @@ const request = (token, options) => utils.request({
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
startOauth2(subdomain, clientId, sub = null, silent = false) {
|
startOauth2(subdomain, clientId, sub = null, silent = false) {
|
||||||
return utils.startOauth2(
|
return networkSvc.startOauth2(
|
||||||
`https://${subdomain}.zendesk.com/oauth/authorizations/new`, {
|
`https://${subdomain}.zendesk.com/oauth/authorizations/new`, {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
response_type: 'token',
|
response_type: 'token',
|
||||||
|
@ -35,15 +35,6 @@ window.addEventListener('focus', setLastFocus);
|
|||||||
// For addQueryParams()
|
// For addQueryParams()
|
||||||
const urlParser = window.document.createElement('a');
|
const urlParser = window.document.createElement('a');
|
||||||
|
|
||||||
// For loadScript()
|
|
||||||
const scriptLoadingPromises = Object.create(null);
|
|
||||||
|
|
||||||
// For startOauth2
|
|
||||||
const oauth2AuthorizationTimeout = 120 * 1000; // 2 minutes
|
|
||||||
|
|
||||||
// For checkOnline
|
|
||||||
const checkOnlineTimeout = 15 * 1000; // 15 sec
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
origin,
|
origin,
|
||||||
@ -142,216 +133,4 @@ export default {
|
|||||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||||
return urlParser.href;
|
return urlParser.href;
|
||||||
},
|
},
|
||||||
loadScript(url) {
|
|
||||||
if (!scriptLoadingPromises[url]) {
|
|
||||||
scriptLoadingPromises[url] = new Promise((resolve, reject) => {
|
|
||||||
const script = document.createElement('script');
|
|
||||||
script.onload = resolve;
|
|
||||||
script.onerror = () => {
|
|
||||||
scriptLoadingPromises[url] = null;
|
|
||||||
reject();
|
|
||||||
};
|
|
||||||
script.src = url;
|
|
||||||
document.head.appendChild(script);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return scriptLoadingPromises[url];
|
|
||||||
},
|
|
||||||
startOauth2(url, params = {}, silent = false) {
|
|
||||||
const oauth2Context = {};
|
|
||||||
|
|
||||||
// Build the authorize URL
|
|
||||||
const state = this.uid();
|
|
||||||
params.state = state;
|
|
||||||
params.redirect_uri = this.oauth2RedirectUri;
|
|
||||||
const authorizeUrl = this.addQueryParams(url, params);
|
|
||||||
if (silent) {
|
|
||||||
// Use an iframe as wnd for silent mode
|
|
||||||
oauth2Context.iframeElt = document.createElement('iframe');
|
|
||||||
oauth2Context.iframeElt.style.position = 'absolute';
|
|
||||||
oauth2Context.iframeElt.style.left = '-9999px';
|
|
||||||
oauth2Context.closeTimeout = setTimeout(
|
|
||||||
() => oauth2Context.clean('Unknown error.'),
|
|
||||||
checkOnlineTimeout);
|
|
||||||
oauth2Context.iframeElt.onerror = () => oauth2Context.clean('Unknown error.');
|
|
||||||
oauth2Context.iframeElt.src = authorizeUrl;
|
|
||||||
document.body.appendChild(oauth2Context.iframeElt);
|
|
||||||
oauth2Context.wnd = oauth2Context.iframeElt.contentWindow;
|
|
||||||
} else {
|
|
||||||
// Open a new tab otherwise
|
|
||||||
oauth2Context.wnd = window.open(authorizeUrl);
|
|
||||||
// If window opening has been blocked by the browser
|
|
||||||
if (!oauth2Context.wnd) {
|
|
||||||
return Promise.reject();
|
|
||||||
}
|
|
||||||
oauth2Context.closeTimeout = setTimeout(
|
|
||||||
() => oauth2Context.clean('Timeout.'),
|
|
||||||
oauth2AuthorizationTimeout);
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
oauth2Context.clean = (errorMsg) => {
|
|
||||||
clearInterval(oauth2Context.checkClosedInterval);
|
|
||||||
if (!silent && !oauth2Context.wnd.closed) {
|
|
||||||
oauth2Context.wnd.close();
|
|
||||||
}
|
|
||||||
if (oauth2Context.iframeElt) {
|
|
||||||
document.body.removeChild(oauth2Context.iframeElt);
|
|
||||||
}
|
|
||||||
clearTimeout(oauth2Context.closeTimeout);
|
|
||||||
window.removeEventListener('message', oauth2Context.msgHandler);
|
|
||||||
oauth2Context.clean = () => {
|
|
||||||
// Prevent from cleaning several times
|
|
||||||
};
|
|
||||||
if (errorMsg) {
|
|
||||||
reject(new Error(errorMsg));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
oauth2Context.msgHandler = (event) => {
|
|
||||||
if (event.source === oauth2Context.wnd &&
|
|
||||||
event.origin === this.origin
|
|
||||||
) {
|
|
||||||
oauth2Context.clean();
|
|
||||||
const data = {};
|
|
||||||
`${event.data}`.slice(1).split('&').forEach((param) => {
|
|
||||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
|
||||||
if (key === 'state') {
|
|
||||||
data.state = value;
|
|
||||||
} else if (key === 'access_token') {
|
|
||||||
data.accessToken = value;
|
|
||||||
} else if (key === 'code') {
|
|
||||||
data.code = value;
|
|
||||||
} else if (key === 'expires_in') {
|
|
||||||
data.expiresIn = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (data.state === state) {
|
|
||||||
resolve(data);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject('Could not get required authorization.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
window.addEventListener('message', oauth2Context.msgHandler);
|
|
||||||
|
|
||||||
oauth2Context.checkClosedInterval = !silent && setInterval(() => {
|
|
||||||
if (oauth2Context.wnd.closed) {
|
|
||||||
oauth2Context.clean('Authorize window was closed.');
|
|
||||||
}
|
|
||||||
}, 250);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
request(configParam) {
|
|
||||||
let retryAfter = 500; // 500 ms
|
|
||||||
const maxRetryAfter = 30 * 1000; // 30 sec
|
|
||||||
const config = Object.assign({}, configParam);
|
|
||||||
config.timeout = config.timeout || 30 * 1000; // 30 sec
|
|
||||||
config.headers = Object.assign({}, config.headers);
|
|
||||||
if (config.body && typeof config.body === 'object') {
|
|
||||||
config.body = JSON.stringify(config.body);
|
|
||||||
config.headers['Content-Type'] = 'application/json';
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseHeaders(xhr) {
|
|
||||||
const pairs = xhr.getAllResponseHeaders().trim().split('\n');
|
|
||||||
return pairs.reduce((headers, header) => {
|
|
||||||
const split = header.trim().split(':');
|
|
||||||
const key = split.shift().trim().toLowerCase();
|
|
||||||
const value = split.join(':').trim();
|
|
||||||
headers[key] = value;
|
|
||||||
return headers;
|
|
||||||
}, {});
|
|
||||||
}
|
|
||||||
|
|
||||||
function isRetriable(err) {
|
|
||||||
switch (err.status) {
|
|
||||||
case 403:
|
|
||||||
{
|
|
||||||
const googleReason = ((((err.body || {}).error || {}).errors || [])[0] || {}).reason;
|
|
||||||
return googleReason === 'rateLimitExceeded' || googleReason === 'userRateLimitExceeded';
|
|
||||||
}
|
|
||||||
case 429:
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
if (err.status >= 500 && err.status < 600) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const attempt =
|
|
||||||
() => new Promise((resolve, reject) => {
|
|
||||||
const xhr = new window.XMLHttpRequest();
|
|
||||||
let timeoutId;
|
|
||||||
|
|
||||||
xhr.onload = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
const result = {
|
|
||||||
status: xhr.status,
|
|
||||||
headers: parseHeaders(xhr),
|
|
||||||
body: xhr.responseText,
|
|
||||||
};
|
|
||||||
if (!config.raw) {
|
|
||||||
try {
|
|
||||||
result.body = JSON.parse(result.body);
|
|
||||||
} catch (e) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (result.status >= 200 && result.status < 300) {
|
|
||||||
resolve(result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
reject(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.onerror = () => {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
reject(new Error('Network request failed.'));
|
|
||||||
};
|
|
||||||
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
xhr.abort();
|
|
||||||
reject(new Error('Network request timeout.'));
|
|
||||||
}, config.timeout);
|
|
||||||
|
|
||||||
const url = this.addQueryParams(config.url, config.params);
|
|
||||||
xhr.open(config.method || 'GET', url);
|
|
||||||
Object.keys(config.headers).forEach((key) => {
|
|
||||||
const value = config.headers[key];
|
|
||||||
if (value) {
|
|
||||||
xhr.setRequestHeader(key, `${value}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
xhr.send(config.body || null);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Try again later in case of retriable error
|
|
||||||
if (isRetriable(err) && retryAfter < maxRetryAfter) {
|
|
||||||
return new Promise(
|
|
||||||
(resolve) => {
|
|
||||||
setTimeout(resolve, retryAfter);
|
|
||||||
// Exponential backoff
|
|
||||||
retryAfter *= 2;
|
|
||||||
})
|
|
||||||
.then(attempt);
|
|
||||||
}
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
return attempt();
|
|
||||||
},
|
|
||||||
checkOnline() {
|
|
||||||
const checkStatus = (res) => {
|
|
||||||
if (!res.status || res.status < 200) {
|
|
||||||
throw new Error('Offline...');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return this.request({
|
|
||||||
url: 'https://www.googleapis.com/plus/v1/people/me',
|
|
||||||
timeout: checkOnlineTimeout,
|
|
||||||
})
|
|
||||||
.then(checkStatus, checkStatus);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -15,15 +15,17 @@ import explorer from './modules/explorer';
|
|||||||
import modal from './modules/modal';
|
import modal from './modules/modal';
|
||||||
import notification from './modules/notification';
|
import notification from './modules/notification';
|
||||||
import queue from './modules/queue';
|
import queue from './modules/queue';
|
||||||
|
import userInfo from './modules/userInfo';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
const debug = process.env.NODE_ENV !== 'production';
|
const debug = NODE_ENV !== 'production';
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
ready: false,
|
ready: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
|
lastOfflineCheck: 0,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
allItemMap: (state) => {
|
allItemMap: (state) => {
|
||||||
@ -39,6 +41,21 @@ const store = new Vuex.Store({
|
|||||||
setOffline: (state, value) => {
|
setOffline: (state, value) => {
|
||||||
state.offline = value;
|
state.offline = value;
|
||||||
},
|
},
|
||||||
|
updateLastOfflineCheck: (state) => {
|
||||||
|
state.lastOfflineCheck = Date.now();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
setOffline: ({ state, commit }, value) => {
|
||||||
|
if (state.offline !== value) {
|
||||||
|
commit('setOffline', value);
|
||||||
|
if (state.offline) {
|
||||||
|
return Promise.reject('You are offline.');
|
||||||
|
}
|
||||||
|
store.dispatch('notification/info', 'You are back online!');
|
||||||
|
}
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modules: {
|
modules: {
|
||||||
contentState,
|
contentState,
|
||||||
@ -54,37 +71,10 @@ const store = new Vuex.Store({
|
|||||||
modal,
|
modal,
|
||||||
notification,
|
notification,
|
||||||
queue,
|
queue,
|
||||||
|
userInfo,
|
||||||
},
|
},
|
||||||
strict: debug,
|
strict: debug,
|
||||||
plugins: debug ? [createLogger()] : [],
|
plugins: debug ? [createLogger()] : [],
|
||||||
});
|
});
|
||||||
|
|
||||||
let isConnectionDown = false;
|
|
||||||
let lastConnectionCheck = 0;
|
|
||||||
|
|
||||||
function checkOffline() {
|
|
||||||
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);
|
|
||||||
window.addEventListener('online', checkOffline);
|
|
||||||
window.addEventListener('offline', checkOffline);
|
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
@ -16,7 +16,7 @@ const constants = {
|
|||||||
explorerWidth: 250,
|
explorerWidth: 250,
|
||||||
sideBarWidth: 280,
|
sideBarWidth: 280,
|
||||||
navigationBarHeight: 44,
|
navigationBarHeight: 44,
|
||||||
buttonBarWidth: 30,
|
buttonBarWidth: 26,
|
||||||
statusBarHeight: 20,
|
statusBarHeight: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -99,15 +99,13 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
if (styles.showEditor) {
|
if (styles.showEditor) {
|
||||||
const syncLocations = getters['syncLocation/current'];
|
const syncLocations = getters['syncLocation/current'];
|
||||||
const publishLocations = getters['publishLocation/current'];
|
const publishLocations = getters['publishLocation/current'];
|
||||||
const isSyncPossible = getters['data/loginToken'] || syncLocations.length;
|
|
||||||
styles.titleMaxWidth = styles.innerWidth -
|
styles.titleMaxWidth = styles.innerWidth -
|
||||||
navigationBarEditButtonsWidth -
|
navigationBarEditButtonsWidth -
|
||||||
navigationBarLeftButtonWidth -
|
navigationBarLeftButtonWidth -
|
||||||
navigationBarRightButtonWidth -
|
navigationBarRightButtonWidth -
|
||||||
navigationBarSpinnerWidth -
|
navigationBarSpinnerWidth -
|
||||||
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
||||||
(isSyncPossible ? navigationBarSyncPublishButtonsWidth : 0) -
|
(navigationBarSyncPublishButtonsWidth * 2) -
|
||||||
(publishLocations.length ? navigationBarSyncPublishButtonsWidth : 0) -
|
|
||||||
navigationBarTitleMargin;
|
navigationBarTitleMargin;
|
||||||
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
||||||
styles.hideLocations = true;
|
styles.hideLocations = true;
|
||||||
|
13
src/store/modules/userInfo.js
Normal file
13
src/store/modules/userInfo.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
itemMap: {},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
addItem: (state, item) => {
|
||||||
|
Vue.set(state.itemMap, item.id, item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user