Workspaces (part 2)
This commit is contained in:
parent
8263e14bcc
commit
abbe1804e2
@ -82,6 +82,12 @@ export default {
|
|||||||
networkSvc.init();
|
networkSvc.init();
|
||||||
sponsorSvc.init();
|
sponsorSvc.init();
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
if (err && err.message !== 'reload') {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
this.$store.dispatch('notification/error', err);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -8,12 +8,12 @@
|
|||||||
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'">
|
<button class="side-title__button button" @click="newItem(true)" v-title="'New folder'">
|
||||||
<icon-folder-plus></icon-folder-plus>
|
<icon-folder-plus></icon-folder-plus>
|
||||||
</button>
|
</button>
|
||||||
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
|
||||||
<icon-pen></icon-pen>
|
|
||||||
</button>
|
|
||||||
<button class="side-title__button button" @click="deleteItem()" v-title="'Remove'">
|
<button class="side-title__button button" @click="deleteItem()" v-title="'Remove'">
|
||||||
<icon-delete></icon-delete>
|
<icon-delete></icon-delete>
|
||||||
</button>
|
</button>
|
||||||
|
<button class="side-title__button button" @click="editItem()" v-title="'Rename'">
|
||||||
|
<icon-pen></icon-pen>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'">
|
<button class="side-title__button button" @click="toggleExplorer(false)" v-title="'Close explorer'">
|
||||||
<icon-close></icon-close>
|
<icon-close></icon-close>
|
||||||
|
@ -141,7 +141,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.layout__panel--navigation-bar {
|
.layout__panel--navigation-bar {
|
||||||
background-color: #2c2c2c;
|
background-color: $navbar-bg;
|
||||||
}
|
}
|
||||||
|
|
||||||
.layout__panel--status-bar {
|
.layout__panel--status-bar {
|
||||||
|
@ -92,7 +92,7 @@ export default {
|
|||||||
publishLocations: 'current',
|
publishLocations: 'current',
|
||||||
}),
|
}),
|
||||||
isSyncPossible() {
|
isSyncPossible() {
|
||||||
return this.$store.getters['data/loginToken'] ||
|
return this.$store.getters['workspace/syncToken'] ||
|
||||||
this.$store.getters['syncLocation/current'].length;
|
this.$store.getters['syncLocation/current'].length;
|
||||||
},
|
},
|
||||||
showSpinner() {
|
showSpinner() {
|
||||||
|
@ -22,7 +22,7 @@ import { mapGetters, mapActions } from 'vuex';
|
|||||||
import CommentList from './gutters/CommentList';
|
import CommentList from './gutters/CommentList';
|
||||||
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
|
import PreviewNewDiscussionButton from './gutters/PreviewNewDiscussionButton';
|
||||||
|
|
||||||
const appUri = `${window.location.protocol}//${window.location.host}`;
|
const appUri = `${location.protocol}//${location.host}`;
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
22
src/components/ProviderName.vue
Normal file
22
src/components/ProviderName.vue
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<template>
|
||||||
|
<span class="provider-name">{{name}}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import userSvc from '../services/userSvc';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['providerId'],
|
||||||
|
computed: {
|
||||||
|
name() {
|
||||||
|
switch (this.userId) {
|
||||||
|
default:
|
||||||
|
return 'Google Drive';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
userSvc.getInfo(this.userId);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
@ -13,7 +13,8 @@ $link-color: #0c93e4;
|
|||||||
$error-color: #f31;
|
$error-color: #f31;
|
||||||
$border-radius-base: 2px;
|
$border-radius-base: 2px;
|
||||||
$hr-color: rgba(128, 128, 128, 0.2);
|
$hr-color: rgba(128, 128, 128, 0.2);
|
||||||
$navbar-color: rgba(255, 255, 255, 0.67);
|
$navbar-bg: #2c2c2c;
|
||||||
|
$navbar-color: mix($navbar-bg, #fff, 33%);
|
||||||
$navbar-hover-color: #fff;
|
$navbar-hover-color: #fff;
|
||||||
$navbar-hover-background: rgba(255, 255, 255, 0.1);
|
$navbar-hover-background: rgba(255, 255, 255, 0.1);
|
||||||
|
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
UserImage,
|
UserImage,
|
||||||
},
|
},
|
||||||
computed: mapGetters('data', [
|
computed: mapGetters('workspace', [
|
||||||
'loginToken',
|
'loginToken',
|
||||||
]),
|
]),
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -41,5 +41,9 @@ export default {
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-bottom: 2px solid;
|
border-bottom: 2px solid;
|
||||||
|
|
||||||
|
.current-discussion & {
|
||||||
|
width: auto !important;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -12,12 +12,12 @@
|
|||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="exportPdf">
|
<menu-entry @click.native="exportPdf">
|
||||||
<icon-download slot="icon"></icon-download>
|
<icon-download slot="icon"></icon-download>
|
||||||
<div><div class="menu-entry__sponsor">sponsor</div> Export as PDF</div>
|
<div><div class="menu-entry__label">sponsor</div> Export as PDF</div>
|
||||||
<span>Produce a PDF from an HTML template.</span>
|
<span>Produce a PDF from an HTML template.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="exportPandoc">
|
<menu-entry @click.native="exportPandoc">
|
||||||
<icon-download slot="icon"></icon-download>
|
<icon-download slot="icon"></icon-download>
|
||||||
<div><div class="menu-entry__sponsor">sponsor</div> Export with Pandoc</div>
|
<div><div class="menu-entry__label">sponsor</div> Export with Pandoc</div>
|
||||||
<span>Convert to PDF, Word, EPUB...</span>
|
<span>Convert to PDF, Word, EPUB...</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="history__spacer history__spacer--last"></div>
|
<div class="history__spacer history__spacer--last" v-if="revisions.length"></div>
|
||||||
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
||||||
<button class="history__button button" @click="showMore">More</button>
|
<button class="history__button button" @click="showMore">More</button>
|
||||||
</div>
|
</div>
|
||||||
@ -79,7 +79,7 @@ export default {
|
|||||||
let revisionContentPromise = revisionContentPromises[revision.id];
|
let revisionContentPromise = revisionContentPromises[revision.id];
|
||||||
if (!revisionContentPromise) {
|
if (!revisionContentPromise) {
|
||||||
revisionContentPromise = new Promise((resolve, reject) => {
|
revisionContentPromise = new Promise((resolve, reject) => {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const loginToken = this.$store.getters['workspace/loginToken'];
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
this.$store.dispatch('queue/enqueue',
|
this.$store.dispatch('queue/enqueue',
|
||||||
() => Promise.resolve()
|
() => Promise.resolve()
|
||||||
@ -133,7 +133,7 @@ export default {
|
|||||||
this.setRevisionContent();
|
this.setRevisionContent();
|
||||||
cachedFileId = id;
|
cachedFileId = id;
|
||||||
revisionContentPromises = {};
|
revisionContentPromises = {};
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const loginToken = this.$store.getters['workspace/loginToken'];
|
||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
revisionsPromise = new Promise((resolve, reject) => {
|
revisionsPromise = new Promise((resolve, reject) => {
|
||||||
this.$store.dispatch('queue/enqueue',
|
this.$store.dispatch('queue/enqueue',
|
||||||
|
@ -1,16 +1,32 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<menu-entry v-if="!loginToken" @click.native="signin">
|
<div v-if="!loginToken">
|
||||||
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||||
|
<div class="menu-entry__icon menu-entry__icon--disabled">
|
||||||
|
<icon-sync-off></icon-sync-off>
|
||||||
|
</div>
|
||||||
|
<span><b>{{currentWorkspace.name}}</b> not synced.</span>
|
||||||
|
</div>
|
||||||
|
<menu-entry @click.native="signin">
|
||||||
<icon-login slot="icon"></icon-login>
|
<icon-login slot="icon"></icon-login>
|
||||||
<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>
|
||||||
|
<div v-else>
|
||||||
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||||
<div class="menu-entry__icon menu-entry__icon--image">
|
<div class="menu-entry__icon menu-entry__icon--image">
|
||||||
<user-image :user-id="loginToken.sub"></user-image>
|
<user-image :user-id="loginToken.sub"></user-image>
|
||||||
</div>
|
</div>
|
||||||
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="menu-entry menu-entry--info flex flex--row flex--align-center">
|
||||||
|
<div class="menu-entry__icon menu-entry__icon--image">
|
||||||
|
<icon-provider :provider-id="currentWorkspace.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<span><b>{{currentWorkspace.name}}</b> synced.</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<menu-entry @click.native="setPanel('workspaces')">
|
<menu-entry @click.native="setPanel('workspaces')">
|
||||||
<icon-database slot="icon"></icon-database>
|
<icon-database slot="icon"></icon-database>
|
||||||
<div>Workspaces</div>
|
<div>Workspaces</div>
|
||||||
@ -46,9 +62,10 @@
|
|||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
Markdown cheat sheet
|
Markdown cheat sheet
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="print">
|
<hr>
|
||||||
<icon-printer slot="icon"></icon-printer>
|
<menu-entry @click.native="setPanel('export')">
|
||||||
Print
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
|
Export to disk
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<input class="hidden-file" id="import-disk-file-input" type="file" @change="onImportFile">
|
<input class="hidden-file" id="import-disk-file-input" type="file" @change="onImportFile">
|
||||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-disk-file-input">
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-disk-file-input">
|
||||||
@ -59,9 +76,9 @@
|
|||||||
Import from disk
|
Import from disk
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<menu-entry @click.native="setPanel('export')">
|
<menu-entry @click.native="print">
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
<icon-printer slot="icon"></icon-printer>
|
||||||
Export to disk
|
Print
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('more')">
|
<menu-entry @click.native="setPanel('more')">
|
||||||
@ -84,7 +101,8 @@ export default {
|
|||||||
UserImage,
|
UserImage,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('data', [
|
...mapGetters('workspace', [
|
||||||
|
'currentWorkspace',
|
||||||
'loginToken',
|
'loginToken',
|
||||||
]),
|
]),
|
||||||
},
|
},
|
||||||
@ -123,8 +141,7 @@ export default {
|
|||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
history() {
|
history() {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
if (!this.loginToken) {
|
||||||
if (!loginToken) {
|
|
||||||
this.$store.dispatch('modal/signInForHistory', {
|
this.$store.dispatch('modal/signInForHistory', {
|
||||||
onResolve: () => googleHelper.signin()
|
onResolve: () => googleHelper.signin()
|
||||||
.then(() => syncSvc.requestSync()),
|
.then(() => syncSvc.requestSync()),
|
||||||
|
@ -13,22 +13,22 @@
|
|||||||
<menu-entry @click.native="reset">
|
<menu-entry @click.native="reset">
|
||||||
<icon-logout slot="icon"></icon-logout>
|
<icon-logout slot="icon"></icon-logout>
|
||||||
<div>Reset application</div>
|
<div>Reset application</div>
|
||||||
<span>Sign out and clean local data.</span>
|
<span>Sign out and clean all workspaces.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
|
<menu-entry @click.native="exportWorkspace">
|
||||||
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
|
Export workspace backup
|
||||||
|
</menu-entry>
|
||||||
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
||||||
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
||||||
<div class="menu-entry__icon flex flex--column flex--center">
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
<icon-content-save></icon-content-save>
|
<icon-content-save></icon-content-save>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex--column">
|
<div class="flex flex--column">
|
||||||
Import backup
|
Import workspace backup
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<menu-entry href="#exportBackup=true" target="_blank">
|
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
|
||||||
Export backup
|
|
||||||
</menu-entry>
|
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="about">
|
<menu-entry @click.native="about">
|
||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
@ -47,8 +47,8 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import MenuEntry from './common/MenuEntry';
|
import MenuEntry from './common/MenuEntry';
|
||||||
import localDbSvc from '../../services/localDbSvc';
|
|
||||||
import backupSvc from '../../services/backupSvc';
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
import welcomeFile from '../../data/welcomeFile.md';
|
import welcomeFile from '../../data/welcomeFile.md';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -72,6 +72,17 @@ export default {
|
|||||||
reader.readAsText(blob);
|
reader.readAsText(blob);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
exportWorkspace() {
|
||||||
|
const url = utils.addQueryParams('app', {
|
||||||
|
...utils.queryParams,
|
||||||
|
exportWorkspace: true,
|
||||||
|
}, true);
|
||||||
|
const iframeElt = utils.createHiddenIframe(url);
|
||||||
|
document.body.appendChild(iframeElt);
|
||||||
|
setTimeout(() => {
|
||||||
|
document.body.removeChild(iframeElt);
|
||||||
|
}, 60000);
|
||||||
|
},
|
||||||
settings() {
|
settings() {
|
||||||
return this.$store.dispatch('modal/open', 'settings')
|
return this.$store.dispatch('modal/open', 'settings')
|
||||||
.then(
|
.then(
|
||||||
@ -88,10 +99,10 @@ export default {
|
|||||||
},
|
},
|
||||||
reset() {
|
reset() {
|
||||||
return this.$store.dispatch('modal/reset')
|
return this.$store.dispatch('modal/reset')
|
||||||
.then(
|
.then(() => {
|
||||||
() => localDbSvc.removeDb(),
|
location.href = '#reset=true';
|
||||||
() => {}, // Cancel
|
location.reload();
|
||||||
);
|
});
|
||||||
},
|
},
|
||||||
welcomeFile() {
|
welcomeFile() {
|
||||||
return this.$store.dispatch('createFile', {
|
return this.$store.dispatch('createFile', {
|
||||||
|
@ -106,8 +106,8 @@ export default {
|
|||||||
...mapState('queue', [
|
...mapState('queue', [
|
||||||
'isSyncRequested',
|
'isSyncRequested',
|
||||||
]),
|
]),
|
||||||
...mapGetters('data', [
|
...mapGetters('workspace', [
|
||||||
'loginToken',
|
'syncToken',
|
||||||
]),
|
]),
|
||||||
...mapGetters('syncLocation', {
|
...mapGetters('syncLocation', {
|
||||||
syncLocations: 'current',
|
syncLocations: 'current',
|
||||||
@ -116,7 +116,7 @@ export default {
|
|||||||
return this.$store.getters['file/current'].name;
|
return this.$store.getters['file/current'].name;
|
||||||
},
|
},
|
||||||
isSyncPossible() {
|
isSyncPossible() {
|
||||||
return this.$store.getters['data/loginToken'] ||
|
return this.syncToken ||
|
||||||
this.$store.getters['syncLocation/current'].length;
|
this.$store.getters['syncLocation/current'].length;
|
||||||
},
|
},
|
||||||
googleDriveTokens() {
|
googleDriveTokens() {
|
||||||
|
@ -2,9 +2,8 @@
|
|||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="workspace" v-for="(workspace, id) in workspaces" :key="id">
|
<div class="workspace" v-for="(workspace, id) in workspaces" :key="id">
|
||||||
<menu-entry :href="workspace.url" target="_blank">
|
<menu-entry :href="workspace.url" target="_blank">
|
||||||
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
<icon-provider slot="icon" :provider-id="workspace.providerId"></icon-provider>
|
||||||
<div class="workspace__name">{{workspace.name}}</div>
|
<div class="workspace__name"><div class="menu-entry__label" v-if="currentWorkspace === workspace">current</div>{{workspace.name}}</div>
|
||||||
<span>{{workspace.url}}</span>
|
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
@ -32,6 +31,9 @@ export default {
|
|||||||
...mapGetters('data', [
|
...mapGetters('data', [
|
||||||
'workspaces',
|
'workspaces',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('workspace', [
|
||||||
|
'currentWorkspace',
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addGoogleDriveWorkspace() {
|
addGoogleDriveWorkspace() {
|
||||||
@ -50,8 +52,16 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.workspace .menu-entry {
|
||||||
|
padding-top: 12px;
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace__name {
|
.workspace__name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
|
||||||
.menu-entry div & {
|
.menu-entry div & {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -19,12 +19,13 @@
|
|||||||
font-size: 17px;
|
font-size: 17px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
text-transform: none;
|
text-transform: none;
|
||||||
|
white-space: normal;
|
||||||
|
|
||||||
div div {
|
div div {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-skip: ink;
|
text-decoration-skip: ink;
|
||||||
|
|
||||||
.menu-entry__sponsor {
|
.menu-entry__label {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,9 +34,19 @@
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
white-space: normal;
|
|
||||||
|
span {
|
||||||
|
display: inline;
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-entry--info {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-entry__icon {
|
.menu-entry__icon {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
@ -44,6 +55,10 @@
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-entry__icon--disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
.menu-entry__icon--image {
|
.menu-entry__icon--image {
|
||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -54,7 +69,7 @@
|
|||||||
top: -999px;
|
top: -999px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-entry__sponsor {
|
.menu-entry__label {
|
||||||
float: right;
|
float: right;
|
||||||
font-size: 0.6rem;
|
font-size: 0.6rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -66,5 +81,6 @@
|
|||||||
|
|
||||||
.menu-entry__text {
|
.menu-entry__text {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -45,17 +45,17 @@ export default modalTemplate({
|
|||||||
const selectedFormat = this.selectedFormat;
|
const selectedFormat = this.selectedFormat;
|
||||||
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||||
return loginToken && googleHelper.refreshToken(loginToken);
|
return sponsorToken && googleHelper.refreshToken(sponsorToken);
|
||||||
}),
|
}),
|
||||||
sponsorSvc.getToken(),
|
sponsorSvc.getToken(),
|
||||||
])
|
])
|
||||||
.then(([loginToken, token]) => networkSvc.request({
|
.then(([sponsorToken, token]) => networkSvc.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'pandocExport',
|
url: 'pandocExport',
|
||||||
params: {
|
params: {
|
||||||
token,
|
token,
|
||||||
idToken: loginToken && loginToken.idToken,
|
idToken: sponsorToken && sponsorToken.idToken,
|
||||||
format: selectedFormat,
|
format: selectedFormat,
|
||||||
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc),
|
options: JSON.stringify(this.$store.getters['data/computedSettings'].pandoc),
|
||||||
metadata: JSON.stringify(currentContent.properties),
|
metadata: JSON.stringify(currentContent.properties),
|
||||||
|
@ -38,19 +38,19 @@ export default modalTemplate({
|
|||||||
const currentFile = this.$store.getters['file/current'];
|
const currentFile = this.$store.getters['file/current'];
|
||||||
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
this.$store.dispatch('queue/enqueue', () => Promise.all([
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||||
return loginToken && googleHelper.refreshToken(loginToken);
|
return sponsorToken && googleHelper.refreshToken(sponsorToken);
|
||||||
}),
|
}),
|
||||||
sponsorSvc.getToken(),
|
sponsorSvc.getToken(),
|
||||||
exportSvc.applyTemplate(
|
exportSvc.applyTemplate(
|
||||||
currentFile.id, this.allTemplates[this.selectedTemplate], true),
|
currentFile.id, this.allTemplates[this.selectedTemplate], true),
|
||||||
])
|
])
|
||||||
.then(([loginToken, token, html]) => networkSvc.request({
|
.then(([sponsorToken, token, html]) => networkSvc.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'pdfExport',
|
url: 'pdfExport',
|
||||||
params: {
|
params: {
|
||||||
token,
|
token,
|
||||||
idToken: loginToken && loginToken.idToken,
|
idToken: sponsorToken && sponsorToken.idToken,
|
||||||
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf),
|
options: JSON.stringify(this.$store.getters['data/computedSettings'].wkhtmltopdf),
|
||||||
},
|
},
|
||||||
body: html,
|
body: html,
|
||||||
|
@ -25,12 +25,12 @@ export default {
|
|||||||
ModalInner,
|
ModalInner,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const sponsorToken = this.$store.getters['workspace/sponsorToken'];
|
||||||
const makeButton = (id, price, description, offer) => {
|
const makeButton = (id, price, description, offer) => {
|
||||||
const params = {
|
const params = {
|
||||||
cmd: '_s-xclick',
|
cmd: '_s-xclick',
|
||||||
hosted_button_id: id,
|
hosted_button_id: id,
|
||||||
custom: loginToken.sub,
|
custom: sponsorToken.sub,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@ -42,7 +42,7 @@ export default {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
buttons: loginToken ? [
|
buttons: sponsorToken ? [
|
||||||
makeButton('TDAPH47B3J2JW', '$5', '3 months sponsorship'),
|
makeButton('TDAPH47B3J2JW', '$5', '3 months sponsorship'),
|
||||||
makeButton('6CTKPKF8868UA', '$15', '1 year sponsorship', '-25%'),
|
makeButton('6CTKPKF8868UA', '$15', '1 year sponsorship', '-25%'),
|
||||||
makeButton('A5ZDYW6SYDLBE', '$25', '2 years sponsorship', '-37%'),
|
makeButton('A5ZDYW6SYDLBE', '$25', '2 years sponsorship', '-37%'),
|
||||||
|
@ -5,18 +5,20 @@
|
|||||||
<div class="workspace-entry__icon flex flex--column flex--center">
|
<div class="workspace-entry__icon flex flex--column flex--center">
|
||||||
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="workspace-entry__description flex flex--column">
|
||||||
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
|
<input class="text-input" type="text" v-if="editedId === id" v-focus @blur="submitEdit()" @keyup.enter="submitEdit()" @keyup.esc.stop="submitEdit(true)" v-model="editingName">
|
||||||
<div class="workspace-entry__name" v-else>
|
<div class="workspace-entry__name" v-else>
|
||||||
{{workspace.name}}
|
{{workspace.name}}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="workspace-entry__url">
|
||||||
|
{{workspace.url}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="workspace-entry__buttons flex flex--row flex--center">
|
<div class="workspace-entry__buttons flex flex--row flex--center">
|
||||||
<button class="workspace-entry__button button" @click="edit(id)">
|
<button class="workspace-entry__button button" @click="edit(id)">
|
||||||
<icon-pen></icon-pen>
|
<icon-pen></icon-pen>
|
||||||
</button>
|
</button>
|
||||||
<a class="workspace-entry__button button" :href="workspace.url" target="_blank">
|
<button class="workspace-entry__button button" v-if="workspace !== currentWorkspace && workspace !== mainWorkspace" @click="remove(id)">
|
||||||
<icon-open-in-new></icon-open-in-new>
|
|
||||||
</a>
|
|
||||||
<button class="workspace-entry__button button" @click="remove(id)">
|
|
||||||
<icon-delete></icon-delete>
|
<icon-delete></icon-delete>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -31,6 +33,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -47,6 +50,10 @@ export default {
|
|||||||
...mapGetters('data', [
|
...mapGetters('data', [
|
||||||
'workspaces',
|
'workspaces',
|
||||||
]),
|
]),
|
||||||
|
...mapGetters('workspace', [
|
||||||
|
'mainWorkspace',
|
||||||
|
'currentWorkspace',
|
||||||
|
]),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
edit(id) {
|
edit(id) {
|
||||||
@ -68,11 +75,11 @@ export default {
|
|||||||
this.editedId = null;
|
this.editedId = null;
|
||||||
},
|
},
|
||||||
remove(id) {
|
remove(id) {
|
||||||
const workspaces = {
|
return this.$store.dispatch('modal/removeWorkspace')
|
||||||
...this.workspaces,
|
.then(
|
||||||
};
|
() => localDbSvc.removeWorkspace(id),
|
||||||
delete workspaces[id];
|
() => {}, // Cancel
|
||||||
this.$store.dispatch('data/setWorkspaces', workspaces);
|
);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@ -97,6 +104,11 @@ export default {
|
|||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__icon {
|
.workspace-entry__icon {
|
||||||
@ -106,12 +118,22 @@ export default {
|
|||||||
flex: none;
|
flex: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__name {
|
.workspace-entry__description {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry__name {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.workspace-entry__url {
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.75em;
|
||||||
|
}
|
||||||
|
|
||||||
.workspace-entry__buttons {
|
.workspace-entry__buttons {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
@ -31,12 +31,12 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
sponsor() {
|
sponsor() {
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => !this.$store.getters['data/loginToken'] &&
|
.then(() => !this.$store.getters['workspace/sponsorToken'] &&
|
||||||
// If user has to sign in
|
// If user has to sign in
|
||||||
this.$store.dispatch('modal/signInForSponsorship', {
|
this.$store.dispatch('modal/signInForSponsorship', {
|
||||||
onResolve: () => googleHelper.signin()
|
onResolve: () => googleHelper.signin()
|
||||||
.then(() => syncSvc.requestSync()),
|
.then(() => syncSvc.requestSync()),
|
||||||
})
|
}))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (!this.$store.getters.isSponsor) {
|
if (!this.$store.getters.isSponsor) {
|
||||||
this.$store.dispatch('modal/open', 'sponsor');
|
this.$store.dispatch('modal/open', 'sponsor');
|
||||||
|
@ -28,11 +28,8 @@ import modalTemplate from '../common/modalTemplate';
|
|||||||
import utils from '../../../services/utils';
|
import utils from '../../../services/utils';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
|
||||||
fileId: '',
|
|
||||||
}),
|
|
||||||
computedLocalSettings: {
|
computedLocalSettings: {
|
||||||
folderId: 'googleDriveFolderId',
|
folderId: 'googleDriveWorkspaceFolderId',
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
openFolder() {
|
openFolder() {
|
||||||
@ -41,7 +38,7 @@ export default modalTemplate({
|
|||||||
googleHelper.openPicker(this.config.token, 'folder')
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
.then((folders) => {
|
.then((folders) => {
|
||||||
this.$store.dispatch('data/patchLocalSettings', {
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
googleDriveFolderId: folders[0].id,
|
googleDriveWorkspaceFolderId: folders[0].id,
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
@ -50,7 +47,7 @@ export default modalTemplate({
|
|||||||
providerId: 'googleDriveWorkspace',
|
providerId: 'googleDriveWorkspace',
|
||||||
folderId: this.folderId,
|
folderId: this.folderId,
|
||||||
sub: this.config.token.sub,
|
sub: this.config.token.sub,
|
||||||
});
|
}, true);
|
||||||
this.config.resolve();
|
this.config.resolve();
|
||||||
window.open(url);
|
window.open(url);
|
||||||
},
|
},
|
||||||
|
@ -4,6 +4,7 @@ export default () => ({
|
|||||||
pdfExportTemplate: 'styledHtml',
|
pdfExportTemplate: 'styledHtml',
|
||||||
pandocExportFormat: 'pdf',
|
pandocExportFormat: 'pdf',
|
||||||
googleDriveFolderId: '',
|
googleDriveFolderId: '',
|
||||||
|
googleDriveWorkspaceFolderId: '',
|
||||||
googleDrivePublishFormat: 'markdown',
|
googleDrivePublishFormat: 'markdown',
|
||||||
googleDrivePublishTemplate: 'styledHtml',
|
googleDrivePublishTemplate: 'styledHtml',
|
||||||
bloggerBlogUrl: '',
|
bloggerBlogUrl: '',
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
# Auto-sync frequency (in ms). Minimum is 60000.
|
||||||
|
autoSyncEvery: 90000
|
||||||
# Adjust font size in editor and preview
|
# Adjust font size in editor and preview
|
||||||
fontSizeFactor: 1
|
fontSizeFactor: 1
|
||||||
# Adjust maximum text width in editor and preview
|
# Adjust maximum text width in editor and preview
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
main: {
|
main: {
|
||||||
name: 'Main workspace',
|
name: 'Main workspace',
|
||||||
|
// The rest will be filled by the data/workspaces getter
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -10,6 +10,7 @@ export default {
|
|||||||
classState() {
|
classState() {
|
||||||
switch (this.providerId) {
|
switch (this.providerId) {
|
||||||
case 'googleDrive':
|
case 'googleDrive':
|
||||||
|
case 'googleDriveAppData':
|
||||||
case 'googleDriveWorkspace':
|
case 'googleDriveWorkspace':
|
||||||
return 'google-drive';
|
return 'google-drive';
|
||||||
case 'googlePhotos':
|
case 'googlePhotos':
|
||||||
|
@ -24,7 +24,7 @@ if (NODE_ENV === 'production') {
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
localStorage.updated = true;
|
localStorage.updated = true;
|
||||||
// Reload the webpage to load into the new version
|
// Reload the webpage to load into the new version
|
||||||
window.location.reload();
|
location.reload();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -5,24 +5,25 @@ import welcomeFile from '../data/welcomeFile.md';
|
|||||||
|
|
||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
const exportBackup = utils.queryParams.exportBackup;
|
const exportWorkspace = utils.queryParams.exportWorkspace;
|
||||||
if (exportBackup) {
|
|
||||||
location.hash = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteMarkerMaxAge = 1000;
|
const deleteMarkerMaxAge = 1000;
|
||||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||||
|
|
||||||
|
const getDbName = (workspaceId) => {
|
||||||
|
let dbName = 'stackedit-db';
|
||||||
|
if (workspaceId !== 'main') {
|
||||||
|
dbName += `-${workspaceId}`;
|
||||||
|
}
|
||||||
|
return dbName;
|
||||||
|
};
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.getTxCbs = [];
|
this.getTxCbs = [];
|
||||||
|
|
||||||
// Make the DB name
|
// Make the DB name
|
||||||
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
const workspaceId = store.getters['workspace/currentWorkspace'].id;
|
||||||
this.dbName = 'stackedit-db';
|
this.dbName = getDbName(workspaceId);
|
||||||
if (workspaceId !== 'main') {
|
|
||||||
this.dbName += `-${workspaceId}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Init connection
|
// Init connection
|
||||||
const request = indexedDB.open(this.dbName, dbVersion);
|
const request = indexedDB.open(this.dbName, dbVersion);
|
||||||
@ -33,7 +34,7 @@ class Connection {
|
|||||||
|
|
||||||
request.onsuccess = (event) => {
|
request.onsuccess = (event) => {
|
||||||
this.db = event.target.result;
|
this.db = event.target.result;
|
||||||
this.db.onversionchange = () => window.location.reload();
|
this.db.onversionchange = () => location.reload();
|
||||||
|
|
||||||
this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
this.getTxCbs.forEach(({ onTx, onError }) => this.createTx(onTx, onError));
|
||||||
this.getTxCbs = null;
|
this.getTxCbs = null;
|
||||||
@ -101,14 +102,33 @@ const localDbSvc = {
|
|||||||
* Create the connection and start syncing.
|
* Create the connection and start syncing.
|
||||||
*/
|
*/
|
||||||
init() {
|
init() {
|
||||||
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
// Reset the app if reset flag was passed
|
||||||
|
if (utils.queryParams.reset) {
|
||||||
|
return Promise.all(
|
||||||
|
Object.keys(store.getters['data/workspaces'])
|
||||||
|
.map(workspaceId => localDbSvc.removeWorkspace(workspaceId)),
|
||||||
|
)
|
||||||
|
.then(() => utils.localStorageDataIds.forEach((id) => {
|
||||||
|
// Clean data stored in localStorage
|
||||||
|
localStorage.removeItem(`data/${id}`);
|
||||||
|
}))
|
||||||
|
.then(() => {
|
||||||
|
location.replace(utils.resolveUrl('app'));
|
||||||
|
throw new Error('reload');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Create the connection
|
// Create the connection
|
||||||
this.connection = new Connection();
|
this.connection = new Connection();
|
||||||
|
|
||||||
// Load the DB
|
// Load the DB
|
||||||
return localDbSvc.sync()
|
return localDbSvc.sync();
|
||||||
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// If exportBackup parameter was provided
|
// If exportWorkspace parameter was provided
|
||||||
if (exportBackup) {
|
if (exportWorkspace) {
|
||||||
const backup = JSON.stringify(store.getters.allItemMap);
|
const backup = JSON.stringify(store.getters.allItemMap);
|
||||||
const blob = new Blob([backup], {
|
const blob = new Blob([backup], {
|
||||||
type: 'text/plain;charset=utf-8',
|
type: 'text/plain;charset=utf-8',
|
||||||
@ -130,8 +150,8 @@ const localDbSvc = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If app was last opened 7 days ago and synchronization is off
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
if (!store.getters['data/loginToken'] &&
|
if (!store.getters['workspace/syncToken'] &&
|
||||||
(store.getters['workspace/lastFocus'] + utils.cleanTrashAfter < Date.now())
|
(store.state.workspace.lastFocus + utils.cleanTrashAfter < Date.now())
|
||||||
) {
|
) {
|
||||||
// Clean files
|
// Clean files
|
||||||
store.getters['file/items']
|
store.getters['file/items']
|
||||||
@ -141,14 +161,14 @@ const localDbSvc = {
|
|||||||
|
|
||||||
// Enable sponsorship
|
// Enable sponsorship
|
||||||
if (utils.queryParams.paymentSuccess) {
|
if (utils.queryParams.paymentSuccess) {
|
||||||
location.hash = '';
|
location.hash = ''; // PaymentSuccess param is always on its own
|
||||||
store.dispatch('modal/paymentSuccess');
|
store.dispatch('modal/paymentSuccess');
|
||||||
const loginToken = store.getters['data/loginToken'];
|
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||||
// Force check sponsorship after a few seconds
|
// Force check sponsorship after a few seconds
|
||||||
const currentDate = Date.now();
|
const currentDate = Date.now();
|
||||||
if (loginToken && loginToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
if (sponsorToken && sponsorToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||||
store.dispatch('data/setGoogleToken', {
|
store.dispatch('data/setGoogleToken', {
|
||||||
...loginToken,
|
...sponsorToken,
|
||||||
expiresOn: currentDate - checkSponsorshipAfter,
|
expiresOn: currentDate - checkSponsorshipAfter,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -288,7 +308,7 @@ const localDbSvc = {
|
|||||||
lastTx = item.tx;
|
lastTx = item.tx;
|
||||||
if (this.lastTx && item.tx - this.lastTx > deleteMarkerMaxAge) {
|
if (this.lastTx && item.tx - this.lastTx > deleteMarkerMaxAge) {
|
||||||
// We may have missed some delete markers
|
// We may have missed some delete markers
|
||||||
window.location.reload();
|
location.reload();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -376,7 +396,7 @@ const localDbSvc = {
|
|||||||
// DB item is different from the corresponding store item
|
// DB item is different from the corresponding store item
|
||||||
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
||||||
// Update content only if it exists in the store
|
// Update content only if it exists in the store
|
||||||
if (existingStoreItem || !contentTypes[dbItem.type] || exportBackup) {
|
if (existingStoreItem || !contentTypes[dbItem.type] || exportWorkspace) {
|
||||||
// Put item in the store
|
// Put item in the store
|
||||||
dbItem.tx = undefined;
|
dbItem.tx = undefined;
|
||||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||||
@ -438,17 +458,25 @@ const localDbSvc = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Drop the database
|
* Drop the database and clean the localStorage for the specified workspaceId.
|
||||||
*/
|
*/
|
||||||
removeDb() {
|
removeWorkspace(id) {
|
||||||
|
const workspaces = {
|
||||||
|
...this.workspaces,
|
||||||
|
};
|
||||||
|
delete workspaces[id];
|
||||||
|
store.dispatch('data/setWorkspaces', workspaces);
|
||||||
|
this.syncLocalStorage();
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.deleteDatabase('stackedit-db');
|
const dbName = getDbName(id);
|
||||||
|
const request = indexedDB.deleteDatabase(dbName);
|
||||||
request.onerror = reject;
|
request.onerror = reject;
|
||||||
request.onsuccess = resolve;
|
request.onsuccess = resolve;
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
window.location.reload();
|
localStorage.removeItem(`${id}/lastSyncActivity`);
|
||||||
}, () => store.dispatch('notification/error', 'Could not delete local database.'));
|
localStorage.removeItem(`${id}/lastWindowFocus`);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,7 +7,6 @@ const networkTimeout = 30 * 1000; // 30 sec
|
|||||||
let isConnectionDown = false;
|
let isConnectionDown = false;
|
||||||
const userInactiveAfter = 2 * 60 * 1000; // 2 minutes
|
const userInactiveAfter = 2 * 60 * 1000; // 2 minutes
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
init() {
|
init() {
|
||||||
// Keep track of the last user activity
|
// Keep track of the last user activity
|
||||||
@ -82,7 +81,9 @@ export default {
|
|||||||
window.addEventListener('offline', checkOffline);
|
window.addEventListener('offline', checkOffline);
|
||||||
},
|
},
|
||||||
isWindowFocused() {
|
isWindowFocused() {
|
||||||
return parseInt(localStorage.getItem(this.lastFocusKey), 10) === this.lastFocus;
|
// We don't use state.workspace.lastFocus as it's not reactive
|
||||||
|
const storedLastFocus = localStorage.getItem(store.getters['workspace/lastFocusKey']);
|
||||||
|
return parseInt(storedLastFocus, 10) === this.lastFocus;
|
||||||
},
|
},
|
||||||
isUserActive() {
|
isUserActive() {
|
||||||
return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
return this.lastActivity > Date.now() - userInactiveAfter && this.isWindowFocused();
|
||||||
@ -113,10 +114,7 @@ export default {
|
|||||||
let wnd;
|
let wnd;
|
||||||
if (silent) {
|
if (silent) {
|
||||||
// Use an iframe as wnd for silent mode
|
// Use an iframe as wnd for silent mode
|
||||||
iframeElt = document.createElement('iframe');
|
iframeElt = utils.createHiddenIframe(authorizeUrl);
|
||||||
iframeElt.style.position = 'absolute';
|
|
||||||
iframeElt.style.left = '-9999px';
|
|
||||||
iframeElt.src = authorizeUrl;
|
|
||||||
document.body.appendChild(iframeElt);
|
document.body.appendChild(iframeElt);
|
||||||
wnd = iframeElt.contentWindow;
|
wnd = iframeElt.contentWindow;
|
||||||
} else {
|
} else {
|
||||||
|
@ -5,17 +5,19 @@ import providerRegistry from './providerRegistry';
|
|||||||
export default providerRegistry.register({
|
export default providerRegistry.register({
|
||||||
id: 'googleDriveAppData',
|
id: 'googleDriveAppData',
|
||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['data/loginToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
initWorkspace() {
|
initWorkspace() {
|
||||||
// Nothing to do since the main workspace isn't necessarily synchronized
|
// Nothing to do since the main workspace isn't necessarily synchronized
|
||||||
return Promise.resolve();
|
return Promise.resolve(store.getters['data/workspaces'].main);
|
||||||
},
|
},
|
||||||
getChanges(token) {
|
getChanges(token) {
|
||||||
return googleHelper.getChanges(token)
|
const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
|
||||||
|
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const changes = result.changes.filter((change) => {
|
const changes = result.changes.filter((change) => {
|
||||||
if (change.file) {
|
if (change.file) {
|
||||||
|
// Parse item from file name
|
||||||
try {
|
try {
|
||||||
change.item = JSON.parse(change.file.name);
|
change.item = JSON.parse(change.file.name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -30,20 +32,17 @@ export default providerRegistry.register({
|
|||||||
};
|
};
|
||||||
change.file = undefined;
|
change.file = undefined;
|
||||||
}
|
}
|
||||||
|
change.syncDataId = change.fileId;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
changes.nextPageToken = result.nextPageToken;
|
changes.startPageToken = result.startPageToken;
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setAppliedChanges(token, changes) {
|
setAppliedChanges(changes) {
|
||||||
const lastToken = store.getters['data/googleTokens'][token.sub];
|
store.dispatch('data/patchLocalSettings', {
|
||||||
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
workspaceSyncStartPageToken: changes.startPageToken,
|
||||||
store.dispatch('data/setGoogleToken', {
|
|
||||||
...lastToken,
|
|
||||||
nextPageToken: changes.nextPageToken,
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
},
|
},
|
||||||
saveItem(token, item, syncData, ifNotTooLate) {
|
saveItem(token, item, syncData, ifNotTooLate) {
|
||||||
return googleHelper.uploadAppDataFile(
|
return googleHelper.uploadAppDataFile(
|
||||||
|
@ -3,20 +3,21 @@ import googleHelper from './helpers/googleHelper';
|
|||||||
import providerRegistry from './providerRegistry';
|
import providerRegistry from './providerRegistry';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
let workspaceFolderId;
|
|
||||||
|
|
||||||
const makeWorkspaceId = () => {
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default providerRegistry.register({
|
||||||
id: 'googleDriveWorkspace',
|
id: 'googleDriveWorkspace',
|
||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['data/loginToken'];
|
return store.getters['workspace/syncToken'];
|
||||||
},
|
},
|
||||||
initWorkspace() {
|
initWorkspace() {
|
||||||
|
const makeWorkspaceId = folderId => folderId && Math.abs(utils.hash(utils.serializeObject({
|
||||||
|
providerId: this.id,
|
||||||
|
folderId,
|
||||||
|
}))).toString(36);
|
||||||
|
|
||||||
|
const getWorkspace = folderId => store.getters['data/workspaces'][makeWorkspaceId(folderId)];
|
||||||
|
|
||||||
const initFolder = (token, folder) => Promise.resolve({
|
const initFolder = (token, folder) => Promise.resolve({
|
||||||
workspaceId: this.makeWorkspaceId(folder.id),
|
folderId: folder.id,
|
||||||
dataFolderId: folder.appProperties.dataFolderId,
|
dataFolderId: folder.appProperties.dataFolderId,
|
||||||
trashFolderId: folder.appProperties.trashFolderId,
|
trashFolderId: folder.appProperties.trashFolderId,
|
||||||
})
|
})
|
||||||
@ -29,7 +30,7 @@ export default providerRegistry.register({
|
|||||||
token,
|
token,
|
||||||
'.stackedit-data',
|
'.stackedit-data',
|
||||||
[folder.id],
|
[folder.id],
|
||||||
{ workspaceId: properties.workspaceId },
|
{ folderId: folder.id },
|
||||||
undefined,
|
undefined,
|
||||||
'application/vnd.google-apps.folder',
|
'application/vnd.google-apps.folder',
|
||||||
)
|
)
|
||||||
@ -47,7 +48,7 @@ export default providerRegistry.register({
|
|||||||
token,
|
token,
|
||||||
'.stackedit-trash',
|
'.stackedit-trash',
|
||||||
[folder.id],
|
[folder.id],
|
||||||
{ workspaceId: properties.workspaceId },
|
{ folderId: folder.id },
|
||||||
undefined,
|
undefined,
|
||||||
'application/vnd.google-apps.folder',
|
'application/vnd.google-apps.folder',
|
||||||
)
|
)
|
||||||
@ -58,7 +59,7 @@ export default providerRegistry.register({
|
|||||||
})
|
})
|
||||||
.then((properties) => {
|
.then((properties) => {
|
||||||
// Update workspace if some properties are missing
|
// Update workspace if some properties are missing
|
||||||
if (properties.workspaceId === folder.appProperties.workspaceId
|
if (properties.folderId === folder.appProperties.folderId
|
||||||
&& properties.dataFolderId === folder.appProperties.dataFolderId
|
&& properties.dataFolderId === folder.appProperties.dataFolderId
|
||||||
&& properties.trashFolderId === folder.appProperties.trashFolderId
|
&& properties.trashFolderId === folder.appProperties.trashFolderId
|
||||||
) {
|
) {
|
||||||
@ -76,26 +77,48 @@ export default providerRegistry.register({
|
|||||||
.then(() => properties);
|
.then(() => properties);
|
||||||
})
|
})
|
||||||
.then((properties) => {
|
.then((properties) => {
|
||||||
|
// Fix the current url hash
|
||||||
|
const hash = `#providerId=${this.id}&folderId=${folder.id}`;
|
||||||
|
if (location.hash !== hash) {
|
||||||
|
location.hash = hash;
|
||||||
|
}
|
||||||
|
|
||||||
// Update workspace in the store
|
// Update workspace in the store
|
||||||
|
const workspaceId = makeWorkspaceId(folder.id);
|
||||||
store.dispatch('data/patchWorkspaces', {
|
store.dispatch('data/patchWorkspaces', {
|
||||||
[properties.workspaceId]: {
|
[workspaceId]: {
|
||||||
id: properties.workspaceId,
|
id: workspaceId,
|
||||||
sub: token.sub,
|
sub: token.sub,
|
||||||
name: folder.name,
|
name: folder.name,
|
||||||
providerId: this.id,
|
providerId: this.id,
|
||||||
|
url: utils.resolveUrl(hash),
|
||||||
folderId: folder.id,
|
folderId: folder.id,
|
||||||
dataFolderId: properties.dataFolderId,
|
dataFolderId: properties.dataFolderId,
|
||||||
trashFolderId: properties.trashFolderId,
|
trashFolderId: properties.trashFolderId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return store.getters['data/workspaces'][properties.workspaceId];
|
|
||||||
|
// Return the workspace
|
||||||
|
return getWorkspace(folder.id);
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.resolve(store.getters['data/googleTokens'][utils.queryParams.sub])
|
const workspace = getWorkspace(utils.queryParams.folderId);
|
||||||
.then(token => token || this.$store.dispatch('modal/workspaceGoogleRedirection', {
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
// See if we already have a token
|
||||||
|
const googleTokens = store.getters['data/googleTokens'];
|
||||||
|
// Token sub is in the workspace or in the url if workspace is about to be created
|
||||||
|
const token = workspace ? googleTokens[workspace.sub] : googleTokens[utils.queryParams.sub];
|
||||||
|
if (token && token.isDrive) {
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
// If no token has been found, popup an authorize window and get one
|
||||||
|
return store.dispatch('modal/workspaceGoogleRedirection', {
|
||||||
onResolve: () => googleHelper.addDriveAccount(),
|
onResolve: () => googleHelper.addDriveAccount(),
|
||||||
}))
|
});
|
||||||
|
})
|
||||||
.then(token => Promise.resolve()
|
.then(token => Promise.resolve()
|
||||||
|
// If no folderId is provided, create one
|
||||||
.then(() => utils.queryParams.folderId || googleHelper.uploadFile(
|
.then(() => utils.queryParams.folderId || googleHelper.uploadFile(
|
||||||
token,
|
token,
|
||||||
'StackEdit workspace',
|
'StackEdit workspace',
|
||||||
@ -103,25 +126,31 @@ export default providerRegistry.register({
|
|||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
'application/vnd.google-apps.folder',
|
'application/vnd.google-apps.folder',
|
||||||
).then(folder => initFolder(token, folder).then(() => folder.id)))
|
)
|
||||||
.then((folderId) => {
|
.then(folder => initFolder(token, {
|
||||||
const workspaceId = this.makeWorkspaceId(folderId);
|
...folder,
|
||||||
const workspace = store.getters['data/workspaces'][workspaceId];
|
appProperties: {},
|
||||||
return workspace || googleHelper.getFile(token, folderId)
|
})
|
||||||
|
.then(() => folder.id)))
|
||||||
|
// If workspace does not exist, initialize one
|
||||||
|
.then(folderId => getWorkspace(folderId) || googleHelper.getFile(token, folderId)
|
||||||
.then((folder) => {
|
.then((folder) => {
|
||||||
const folderWorkspaceId = folder.appProperties.workspaceId;
|
const folderIdProperty = folder.appProperties.folderId;
|
||||||
if (folderWorkspaceId && folderWorkspaceId !== workspaceId) {
|
if (folderIdProperty && folderIdProperty !== folderId) {
|
||||||
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
||||||
}
|
}
|
||||||
return initFolder(token, folder);
|
return initFolder(token, folder);
|
||||||
});
|
}, () => {
|
||||||
}));
|
throw new Error(`Folder ${folderId} is not accessible. Make sure it's a valid StackEdit workspace folder and you have the right permissions.`);
|
||||||
|
})));
|
||||||
},
|
},
|
||||||
getChanges(token) {
|
getChanges(token) {
|
||||||
return googleHelper.getChanges(token)
|
const startPageToken = store.getters['data/localSettings'].syncStartPageToken;
|
||||||
|
return googleHelper.getChanges(token, startPageToken, 'appDataFolder')
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
const changes = result.changes.filter((change) => {
|
const changes = result.changes.filter((change) => {
|
||||||
if (change.file) {
|
if (change.file) {
|
||||||
|
// Parse item from file name
|
||||||
try {
|
try {
|
||||||
change.item = JSON.parse(change.file.name);
|
change.item = JSON.parse(change.file.name);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -136,121 +165,11 @@ export default providerRegistry.register({
|
|||||||
};
|
};
|
||||||
change.file = undefined;
|
change.file = undefined;
|
||||||
}
|
}
|
||||||
|
change.syncDataId = change.fileId;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
changes.nextPageToken = result.nextPageToken;
|
changes.startPageToken = result.startPageToken;
|
||||||
return changes;
|
return changes;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
setAppliedChanges(token, changes) {
|
|
||||||
const lastToken = store.getters['data/googleTokens'][token.sub];
|
|
||||||
if (changes.nextPageToken !== lastToken.nextPageToken) {
|
|
||||||
store.dispatch('data/setGoogleToken', {
|
|
||||||
...lastToken,
|
|
||||||
nextPageToken: changes.nextPageToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
saveItem(token, item, syncData, ifNotTooLate) {
|
|
||||||
return googleHelper.uploadAppDataFile(
|
|
||||||
token,
|
|
||||||
JSON.stringify(item),
|
|
||||||
['appDataFolder'],
|
|
||||||
undefined,
|
|
||||||
undefined,
|
|
||||||
syncData && syncData.id,
|
|
||||||
ifNotTooLate,
|
|
||||||
)
|
|
||||||
.then(file => ({
|
|
||||||
// Build sync data
|
|
||||||
id: file.id,
|
|
||||||
itemId: item.id,
|
|
||||||
type: item.type,
|
|
||||||
hash: item.hash,
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
removeItem(token, syncData, ifNotTooLate) {
|
|
||||||
return googleHelper.removeAppDataFile(token, syncData.id, ifNotTooLate)
|
|
||||||
.then(() => syncData);
|
|
||||||
},
|
|
||||||
downloadContent(token, syncLocation) {
|
|
||||||
return this.downloadData(token, `${syncLocation.fileId}/content`);
|
|
||||||
},
|
|
||||||
downloadData(token, dataId) {
|
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
|
||||||
if (!syncData) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return googleHelper.downloadAppDataFile(token, syncData.id)
|
|
||||||
.then((content) => {
|
|
||||||
const item = JSON.parse(content);
|
|
||||||
if (item.hash !== syncData.hash) {
|
|
||||||
store.dispatch('data/patchSyncData', {
|
|
||||||
[syncData.id]: {
|
|
||||||
...syncData,
|
|
||||||
hash: item.hash,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return item;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
|
||||||
return this.uploadData(token, content, `${syncLocation.fileId}/content`, ifNotTooLate)
|
|
||||||
.then(() => syncLocation);
|
|
||||||
},
|
|
||||||
uploadData(token, item, dataId, ifNotTooLate) {
|
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
|
||||||
if (syncData && syncData.hash === item.hash) {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
return googleHelper.uploadAppDataFile(
|
|
||||||
token,
|
|
||||||
JSON.stringify({
|
|
||||||
id: item.id,
|
|
||||||
type: item.type,
|
|
||||||
hash: item.hash,
|
|
||||||
}),
|
|
||||||
['appDataFolder'],
|
|
||||||
undefined,
|
|
||||||
JSON.stringify(item),
|
|
||||||
syncData && syncData.id,
|
|
||||||
ifNotTooLate,
|
|
||||||
)
|
|
||||||
.then(file => store.dispatch('data/patchSyncData', {
|
|
||||||
[file.id]: {
|
|
||||||
// Build sync data
|
|
||||||
id: file.id,
|
|
||||||
itemId: item.id,
|
|
||||||
type: item.type,
|
|
||||||
hash: item.hash,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
listRevisions(token, fileId) {
|
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
|
||||||
if (!syncData) {
|
|
||||||
return Promise.reject(); // No need for a proper error message.
|
|
||||||
}
|
|
||||||
return googleHelper.getFileRevisions(token, syncData.id)
|
|
||||||
.then(revisions => revisions.map(revision => ({
|
|
||||||
id: revision.id,
|
|
||||||
sub: revision.lastModifyingUser && revision.lastModifyingUser.permissionId,
|
|
||||||
created: new Date(revision.modifiedTime).getTime(),
|
|
||||||
})));
|
|
||||||
},
|
|
||||||
getRevisionContent(token, fileId, revisionId) {
|
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
|
||||||
if (!syncData) {
|
|
||||||
return Promise.reject(); // No need for a proper error message.
|
|
||||||
}
|
|
||||||
return googleHelper.downloadFileRevision(token, syncData.id, revisionId)
|
|
||||||
.then(content => JSON.parse(content));
|
|
||||||
},
|
|
||||||
makeWorkspaceId(folderId) {
|
|
||||||
return Math.abs(utils.hash(utils.serializeObject({
|
|
||||||
providerId: this.id,
|
|
||||||
folderId: folderId,
|
|
||||||
}))).toString(36);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
@ -168,7 +168,7 @@ export default {
|
|||||||
expiresOn: Date.now() + (data.expiresIn * 1000),
|
expiresOn: Date.now() + (data.expiresIn * 1000),
|
||||||
idToken: data.idToken,
|
idToken: data.idToken,
|
||||||
sub: `${res.body.sub}`,
|
sub: `${res.body.sub}`,
|
||||||
isLogin: !store.getters['data/loginToken'] &&
|
isLogin: !store.getters['workspace/loginToken'] &&
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||||
isSponsor: false,
|
isSponsor: false,
|
||||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||||
@ -195,13 +195,14 @@ export default {
|
|||||||
// We probably retrieved a new token with restricted scopes.
|
// We probably retrieved a new token with restricted scopes.
|
||||||
// That's no problem, token will be refreshed later with merged scopes.
|
// That's no problem, token will be refreshed later with merged scopes.
|
||||||
// Restore flags
|
// Restore flags
|
||||||
token.isLogin = existingToken.isLogin || token.isLogin;
|
Object.assign(token, {
|
||||||
token.isSponsor = existingToken.isSponsor;
|
isLogin: existingToken.isLogin || token.isLogin,
|
||||||
token.isDrive = existingToken.isDrive || token.isDrive;
|
isSponsor: existingToken.isSponsor,
|
||||||
token.isBlogger = existingToken.isBlogger || token.isBlogger;
|
isDrive: existingToken.isDrive || token.isDrive,
|
||||||
token.isPhotos = existingToken.isPhotos || token.isPhotos;
|
isBlogger: existingToken.isBlogger || token.isBlogger,
|
||||||
token.driveFullAccess = existingToken.driveFullAccess || token.driveFullAccess;
|
isPhotos: existingToken.isPhotos || token.isPhotos,
|
||||||
token.nextPageToken = existingToken.nextPageToken;
|
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return token.isLogin && networkSvc.request({
|
return token.isLogin && networkSvc.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -296,7 +297,7 @@ export default {
|
|||||||
addPhotosAccount() {
|
addPhotosAccount() {
|
||||||
return this.startOauth2(photosScopes);
|
return this.startOauth2(photosScopes);
|
||||||
},
|
},
|
||||||
getChanges(token) {
|
getChanges(token, startPageToken, spaces) {
|
||||||
const result = {
|
const result = {
|
||||||
changes: [],
|
changes: [],
|
||||||
};
|
};
|
||||||
@ -307,20 +308,21 @@ export default {
|
|||||||
url: 'https://www.googleapis.com/drive/v3/changes',
|
url: 'https://www.googleapis.com/drive/v3/changes',
|
||||||
params: {
|
params: {
|
||||||
pageToken,
|
pageToken,
|
||||||
spaces: 'appDataFolder',
|
spaces,
|
||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/properties)',
|
fields: 'nextPageToken,newStartPageToken,changes(fileId,removed,file/name,file/appProperties)',
|
||||||
},
|
},
|
||||||
}).then((res) => {
|
})
|
||||||
|
.then((res) => {
|
||||||
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId));
|
result.changes = result.changes.concat(res.body.changes.filter(item => item.fileId));
|
||||||
if (res.body.nextPageToken) {
|
if (res.body.nextPageToken) {
|
||||||
return getPage(res.body.nextPageToken);
|
return getPage(res.body.nextPageToken);
|
||||||
}
|
}
|
||||||
result.nextPageToken = res.body.newStartPageToken;
|
result.startPageToken = res.body.newStartPageToken;
|
||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
return getPage(refreshedToken.nextPageToken);
|
return getPage(startPageToken);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
|
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
|
||||||
@ -338,6 +340,9 @@ export default {
|
|||||||
.then(refreshedToken => this.request(refreshedToken, {
|
.then(refreshedToken => this.request(refreshedToken, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||||
|
params: {
|
||||||
|
fields: 'id,name,mimeType,appProperties',
|
||||||
|
},
|
||||||
})
|
})
|
||||||
.then(res => res.body));
|
.then(res => res.body));
|
||||||
},
|
},
|
||||||
@ -369,7 +374,8 @@ export default {
|
|||||||
pageSize: 1000,
|
pageSize: 1000,
|
||||||
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
|
fields: 'nextPageToken,revisions(id,modifiedTime,lastModifyingUser/permissionId,lastModifyingUser/displayName,lastModifyingUser/photoLink)',
|
||||||
},
|
},
|
||||||
}).then((res) => {
|
})
|
||||||
|
.then((res) => {
|
||||||
res.body.revisions.forEach((revision) => {
|
res.body.revisions.forEach((revision) => {
|
||||||
store.commit('userInfo/addItem', {
|
store.commit('userInfo/addItem', {
|
||||||
id: revision.lastModifyingUser.permissionId,
|
id: revision.lastModifyingUser.permissionId,
|
||||||
|
@ -17,8 +17,8 @@ const getMonetize = () => Promise.resolve()
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isGoogleSponsor = () => {
|
const isGoogleSponsor = () => {
|
||||||
const loginToken = store.getters['data/loginToken'];
|
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||||
return loginToken && loginToken.isSponsor;
|
return sponsorToken && sponsorToken.isSponsor;
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkPayment = () => {
|
const checkPayment = () => {
|
||||||
|
@ -5,12 +5,13 @@ import diffUtils from './diffUtils';
|
|||||||
import networkSvc from './networkSvc';
|
import networkSvc from './networkSvc';
|
||||||
import providerRegistry from './providers/providerRegistry';
|
import providerRegistry from './providers/providerRegistry';
|
||||||
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||||
|
import './providers/googleDriveWorkspaceProvider';
|
||||||
|
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
const restartSyncAfter = 30 * 1000; // 30 sec
|
const restartSyncAfter = 30 * 1000; // 30 sec
|
||||||
const autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||||
|
|
||||||
let workspaceProvider;
|
let syncProvider;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use a lock in the local storage to prevent multiple windows concurrency.
|
* Use a lock in the local storage to prevent multiple windows concurrency.
|
||||||
@ -22,14 +23,7 @@ const getLastStoredSyncActivity = () =>
|
|||||||
/**
|
/**
|
||||||
* Return true if workspace sync is possible.
|
* Return true if workspace sync is possible.
|
||||||
*/
|
*/
|
||||||
const isWorkspaceSyncPossible = () => {
|
const isWorkspaceSyncPossible = () => !!store.getters['workspace/syncToken'];
|
||||||
const loginToken = store.getters['data/loginToken'];
|
|
||||||
if (!loginToken && Object.keys(store.getters['data/syncData']).length) {
|
|
||||||
// Reset sync data if token was removed
|
|
||||||
store.dispatch('data/setSyncData', {});
|
|
||||||
}
|
|
||||||
return !!loginToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if file has at least one explicit sync location.
|
* Return true if file has at least one explicit sync location.
|
||||||
@ -55,8 +49,11 @@ function isSyncWindow() {
|
|||||||
* Return true if auto sync can start, ie if lastSyncActivity is old enough.
|
* Return true if auto sync can start, ie if lastSyncActivity is old enough.
|
||||||
*/
|
*/
|
||||||
function isAutoSyncReady() {
|
function isAutoSyncReady() {
|
||||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
let autoSyncEvery = store.getters['data/computedSettings'].autoSyncEvery;
|
||||||
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
if (autoSyncEvery < minAutoSyncEvery) {
|
||||||
|
autoSyncEvery = minAutoSyncEvery;
|
||||||
|
}
|
||||||
|
return Date.now() > autoSyncEvery + getLastStoredSyncActivity();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -91,7 +88,6 @@ function cleanSyncedContent(syncedContent) {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply changes retrieved from the main provider. Update sync data accordingly.
|
* Apply changes retrieved from the main provider. Update sync data accordingly.
|
||||||
* @param {*} changes The changes to apply.
|
|
||||||
*/
|
*/
|
||||||
function applyChanges(changes) {
|
function applyChanges(changes) {
|
||||||
const storeItemMap = { ...store.getters.allItemMap };
|
const storeItemMap = { ...store.getters.allItemMap };
|
||||||
@ -99,7 +95,7 @@ function applyChanges(changes) {
|
|||||||
let syncDataChanged = false;
|
let syncDataChanged = false;
|
||||||
|
|
||||||
changes.forEach((change) => {
|
changes.forEach((change) => {
|
||||||
const existingSyncData = syncData[change.fileId];
|
const existingSyncData = syncData[change.syncDataId];
|
||||||
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
const existingItem = existingSyncData && storeItemMap[existingSyncData.itemId];
|
||||||
if (change.removed && existingSyncData) {
|
if (change.removed && existingSyncData) {
|
||||||
if (existingItem) {
|
if (existingItem) {
|
||||||
@ -107,19 +103,19 @@ function applyChanges(changes) {
|
|||||||
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
store.commit(`${existingItem.type}/deleteItem`, existingItem.id);
|
||||||
delete storeItemMap[existingItem.id];
|
delete storeItemMap[existingItem.id];
|
||||||
}
|
}
|
||||||
delete syncData[change.fileId];
|
delete syncData[change.syncDataId];
|
||||||
syncDataChanged = true;
|
syncDataChanged = true;
|
||||||
} else if (!change.removed && change.item && change.item.hash) {
|
} else if (!change.removed && change.item && change.item.hash) {
|
||||||
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
|
if (!existingSyncData || (existingSyncData.hash !== change.item.hash && (
|
||||||
!existingItem || existingItem.hash !== change.item.hash
|
!existingItem || existingItem.hash !== change.item.hash
|
||||||
))) {
|
))) {
|
||||||
// Put object in the store
|
// Put object in the store, except for content and data which will be merge later
|
||||||
if (change.item.type !== 'content') { // Merge contents later
|
if (change.item.type !== 'content' && change.item.type !== 'data') {
|
||||||
store.commit(`${change.item.type}/setItem`, change.item);
|
store.commit(`${change.item.type}/setItem`, change.item);
|
||||||
storeItemMap[change.item.id] = change.item;
|
storeItemMap[change.item.id] = change.item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncData[change.fileId] = change.syncData;
|
syncData[change.syncDataId] = change.syncData;
|
||||||
syncDataChanged = true;
|
syncDataChanged = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -221,7 +217,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
if (isWorkspaceSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
syncLocations.unshift({ id: 'main', providerId: syncProvider.id, fileId });
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
syncLocations.some((syncLocation) => {
|
syncLocations.some((syncLocation) => {
|
||||||
@ -355,7 +351,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If content was just created, restart sync to create the file as well
|
// If content was just created, restart sync to create the file as well
|
||||||
if (provider === mainProvider &&
|
if (provider === syncProvider &&
|
||||||
!store.getters['data/syncDataByItemId'][fileId]
|
!store.getters['data/syncDataByItemId'][fileId]
|
||||||
) {
|
) {
|
||||||
syncContext.restart = true;
|
syncContext.restart = true;
|
||||||
@ -396,21 +392,25 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sync a data item, typically settings and templates.
|
* Sync a data item, typically settings, workspaces and templates.
|
||||||
*/
|
*/
|
||||||
function syncDataItem(dataId) {
|
function syncDataItem(dataId) {
|
||||||
const item = store.state.data.itemMap[dataId];
|
const getItem = () => store.state.data.itemMap[dataId]
|
||||||
|
|| store.state.data.lsItemMap[dataId];
|
||||||
|
|
||||||
|
const item = getItem();
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||||
// Sync if item hash and syncData hash are inconsistent
|
// Sync if item hash and syncData hash are inconsistent
|
||||||
if (syncData && item && item.hash === syncData.hash) {
|
if (syncData && item && item.hash === syncData.hash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const token = mainProvider.getToken();
|
|
||||||
return token && mainProvider.downloadData(token, dataId)
|
const syncToken = store.getters['workspace/syncToken'];
|
||||||
|
return syncToken && syncProvider.downloadData(syncToken, dataId)
|
||||||
.then((serverItem = null) => {
|
.then((serverItem = null) => {
|
||||||
const dataSyncData = store.getters['data/dataSyncData'][dataId];
|
const dataSyncData = store.getters['data/dataSyncData'][dataId];
|
||||||
let mergedItem = (() => {
|
let mergedItem = (() => {
|
||||||
const clientItem = utils.deepCopy(store.state.data.itemMap[dataId]);
|
const clientItem = utils.deepCopy(getItem());
|
||||||
if (!clientItem) {
|
if (!clientItem) {
|
||||||
return serverItem;
|
return serverItem;
|
||||||
}
|
}
|
||||||
@ -445,15 +445,15 @@ function syncDataItem(dataId) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Retrieve item with new `hash` and freeze it
|
// Retrieve item with new `hash` and freeze it
|
||||||
mergedItem = utils.deepCopy(store.state.data.itemMap[dataId]);
|
mergedItem = utils.deepCopy(getItem());
|
||||||
|
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (serverItem && serverItem.hash === mergedItem.hash) {
|
if (serverItem && serverItem.hash === mergedItem.hash) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return mainProvider.uploadData(
|
return syncProvider.uploadData(
|
||||||
token,
|
syncToken,
|
||||||
mergedItem,
|
mergedItem,
|
||||||
dataId,
|
dataId,
|
||||||
);
|
);
|
||||||
@ -469,14 +469,26 @@ function syncDataItem(dataId) {
|
|||||||
/**
|
/**
|
||||||
* Sync the whole workspace with the main provider and the current file explicit locations.
|
* Sync the whole workspace with the main provider and the current file explicit locations.
|
||||||
*/
|
*/
|
||||||
function syncWorkspace() {
|
function syncWorkspace(workspace, syncToken) {
|
||||||
const syncContext = new SyncContext();
|
const syncContext = new SyncContext();
|
||||||
const mainToken = store.getters['data/loginToken'];
|
|
||||||
return mainProvider.getChanges(mainToken)
|
return Promise.resolve()
|
||||||
|
.then(() => {
|
||||||
|
// Store the sub in the DB since it's not safely stored in the token
|
||||||
|
const localSettings = store.getters['data/localSettings'];
|
||||||
|
if (!localSettings.syncSub) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
workspaceSyncSub: syncToken.sub,
|
||||||
|
});
|
||||||
|
} else if (localSettings.syncSub !== syncToken.sub) {
|
||||||
|
throw new Error('Synchronization failed due to token inconsistency.');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(() => syncProvider.getChanges(syncToken))
|
||||||
.then((changes) => {
|
.then((changes) => {
|
||||||
// Apply changes
|
// Apply changes
|
||||||
applyChanges(changes);
|
applyChanges(changes);
|
||||||
mainProvider.setAppliedChanges(mainToken, changes);
|
syncProvider.setAppliedChanges(changes);
|
||||||
|
|
||||||
// Prevent from sending items too long after changes have been retrieved
|
// Prevent from sending items too long after changes have been retrieved
|
||||||
const syncStartTime = Date.now();
|
const syncStartTime = Date.now();
|
||||||
@ -504,8 +516,8 @@ function syncWorkspace() {
|
|||||||
// Add file if content has been uploaded
|
// Add file if content has been uploaded
|
||||||
(item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
(item.type !== 'file' || syncDataByItemId[`${id}/content`])
|
||||||
) {
|
) {
|
||||||
result = mainProvider.saveItem(
|
result = syncProvider.saveItem(
|
||||||
mainToken,
|
syncToken,
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
utils.deepCopy(item),
|
utils.deepCopy(item),
|
||||||
utils.deepCopy(existingSyncData),
|
utils.deepCopy(existingSyncData),
|
||||||
@ -529,19 +541,20 @@ function syncWorkspace() {
|
|||||||
...store.state.syncLocation.itemMap,
|
...store.state.syncLocation.itemMap,
|
||||||
...store.state.publishLocation.itemMap,
|
...store.state.publishLocation.itemMap,
|
||||||
...store.state.content.itemMap,
|
...store.state.content.itemMap,
|
||||||
...store.state.data.itemMap,
|
|
||||||
};
|
};
|
||||||
const syncData = store.getters['data/syncData'];
|
const syncData = store.getters['data/syncData'];
|
||||||
let result;
|
let result;
|
||||||
Object.entries(syncData).some(([, existingSyncData]) => {
|
Object.entries(syncData).some(([, existingSyncData]) => {
|
||||||
if (!storeItemMap[existingSyncData.itemId] &&
|
if (!storeItemMap[existingSyncData.itemId] &&
|
||||||
|
// We don't want to delete data items, especially on first sync
|
||||||
|
existingSyncData.type !== 'data' &&
|
||||||
// Remove content only if file has been removed
|
// Remove content only if file has been removed
|
||||||
(existingSyncData.type !== 'content' || !storeItemMap[existingSyncData.itemId.split('/')[0]])
|
(existingSyncData.type !== 'content' || !storeItemMap[existingSyncData.itemId.split('/')[0]])
|
||||||
) {
|
) {
|
||||||
// Use deepCopy to freeze objects
|
// Use deepCopy to freeze objects
|
||||||
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
const syncDataToRemove = utils.deepCopy(existingSyncData);
|
||||||
result = mainProvider
|
result = syncProvider
|
||||||
.removeItem(mainToken, syncDataToRemove, ifNotTooLate)
|
.removeItem(syncToken, syncDataToRemove, ifNotTooLate)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const syncDataCopy = { ...store.getters['data/syncData'] };
|
const syncDataCopy = { ...store.getters['data/syncData'] };
|
||||||
delete syncDataCopy[syncDataToRemove.id];
|
delete syncDataCopy[syncDataToRemove.id];
|
||||||
@ -590,7 +603,10 @@ function syncWorkspace() {
|
|||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
.then(() => saveNextItem())
|
.then(() => saveNextItem())
|
||||||
.then(() => removeNextItem())
|
.then(() => removeNextItem())
|
||||||
.then(() => syncDataItem('settings'))
|
// Sync settings only in the main workspace
|
||||||
|
.then(() => workspace.id === 'main' && syncDataItem('settings'))
|
||||||
|
// Sync workspaces only in the main workspace
|
||||||
|
.then(() => workspace.id === 'main' && syncDataItem('workspaces'))
|
||||||
.then(() => syncDataItem('templates'))
|
.then(() => syncDataItem('templates'))
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const currentFileId = store.getters['file/current'].id;
|
const currentFileId = store.getters['file/current'].id;
|
||||||
@ -605,14 +621,14 @@ function syncWorkspace() {
|
|||||||
() => {
|
() => {
|
||||||
if (syncContext.restart) {
|
if (syncContext.restart) {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return syncWorkspace();
|
return syncWorkspace(workspace, syncToken);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return syncWorkspace();
|
return syncWorkspace(workspace, syncToken);
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
@ -660,12 +676,14 @@ function requestSync() {
|
|||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (isWorkspaceSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
return syncWorkspace();
|
return syncWorkspace(
|
||||||
|
store.getters['workspace/currentWorkspace'],
|
||||||
|
store.getters['workspace/syncToken']);
|
||||||
}
|
}
|
||||||
if (hasCurrentFileSyncLocations()) {
|
if (hasCurrentFileSyncLocations()) {
|
||||||
// Only sync current file if data sync is unavailable.
|
// Only sync current file if workspace sync is unavailable.
|
||||||
// We also could sync files that are out-of-sync but it would
|
// We could also sync files that are out-of-sync but it would
|
||||||
// require to load the syncedContent objects of all files.
|
// require to load all the syncedContent objects from the DB.
|
||||||
return syncFile(store.getters['file/current'].id);
|
return syncFile(store.getters['file/current'].id);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@ -692,14 +710,14 @@ export default {
|
|||||||
// Load workspaces and tokens from localStorage
|
// Load workspaces and tokens from localStorage
|
||||||
localDbSvc.syncLocalStorage();
|
localDbSvc.syncLocalStorage();
|
||||||
|
|
||||||
// Try to find a suitable workspace provider
|
// Try to find a suitable workspace sync provider
|
||||||
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
|
syncProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||||
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
if (!syncProvider || !syncProvider.initWorkspace) {
|
||||||
workspaceProvider = googleDriveAppDataProvider;
|
syncProvider = googleDriveAppDataProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceProvider.initWorkspace()
|
return syncProvider.initWorkspace()
|
||||||
.then(workspace => store.commit('workspace/setCurrentWorkspaceId', workspace.id))
|
.then(workspace => store.dispatch('workspace/setCurrentWorkspaceId', workspace.id))
|
||||||
.then(() => localDbSvc.init())
|
.then(() => localDbSvc.init())
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Sync periodically
|
// Sync periodically
|
||||||
|
@ -4,24 +4,26 @@ import defaultProperties from '../data/defaultFileProperties.yml';
|
|||||||
|
|
||||||
const origin = `${location.protocol}//${location.host}`;
|
const origin = `${location.protocol}//${location.host}`;
|
||||||
|
|
||||||
// For uid()
|
// For utils.uid()
|
||||||
const uidLength = 16;
|
const uidLength = 16;
|
||||||
const crypto = window.crypto || window.msCrypto;
|
const crypto = window.crypto || window.msCrypto;
|
||||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||||
const radix = alphabet.length;
|
const radix = alphabet.length;
|
||||||
const array = new Uint32Array(uidLength);
|
const array = new Uint32Array(uidLength);
|
||||||
|
|
||||||
// For parseQueryParams()
|
// For utils.parseQueryParams()
|
||||||
const parseQueryParams = (params) => {
|
const parseQueryParams = (params) => {
|
||||||
const result = {};
|
const result = {};
|
||||||
params.split('&').forEach((param) => {
|
params.split('&').forEach((param) => {
|
||||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||||
|
if (key) {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// For addQueryParams()
|
// For utils.addQueryParams()
|
||||||
const urlParser = window.document.createElement('a');
|
const urlParser = window.document.createElement('a');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -121,20 +123,43 @@ export default {
|
|||||||
return setInterval(() => func(), this.randomize(interval));
|
return setInterval(() => func(), this.randomize(interval));
|
||||||
},
|
},
|
||||||
parseQueryParams,
|
parseQueryParams,
|
||||||
addQueryParams(url = '', params = {}) {
|
addQueryParams(url = '', params = {}, hash = false) {
|
||||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||||
urlParser.href = url;
|
urlParser.href = url;
|
||||||
if (!keys.length) {
|
if (!keys.length) {
|
||||||
return urlParser.href;
|
return urlParser.href;
|
||||||
}
|
}
|
||||||
|
const serializedParams = keys.map(key =>
|
||||||
|
`${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
||||||
|
if (hash) {
|
||||||
|
if (urlParser.hash) {
|
||||||
|
urlParser.hash += '&';
|
||||||
|
} else {
|
||||||
|
urlParser.hash = '#';
|
||||||
|
}
|
||||||
|
urlParser.hash += serializedParams;
|
||||||
|
} else {
|
||||||
if (urlParser.search) {
|
if (urlParser.search) {
|
||||||
urlParser.search += '&';
|
urlParser.search += '&';
|
||||||
} else {
|
} else {
|
||||||
urlParser.search = '?';
|
urlParser.search = '?';
|
||||||
}
|
}
|
||||||
urlParser.search += keys.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`).join('&');
|
urlParser.search += serializedParams;
|
||||||
|
}
|
||||||
return urlParser.href;
|
return urlParser.href;
|
||||||
},
|
},
|
||||||
|
resolveUrl(url) {
|
||||||
|
return this.addQueryParams(url);
|
||||||
|
},
|
||||||
|
createHiddenIframe(url) {
|
||||||
|
const iframeElt = document.createElement('iframe');
|
||||||
|
iframeElt.style.position = 'absolute';
|
||||||
|
iframeElt.style.left = '-9999px';
|
||||||
|
iframeElt.style.width = '1px';
|
||||||
|
iframeElt.style.height = '1px';
|
||||||
|
iframeElt.src = url;
|
||||||
|
return iframeElt;
|
||||||
|
},
|
||||||
wrapRange(range, eltProperties) {
|
wrapRange(range, eltProperties) {
|
||||||
const rangeLength = `${range}`.length;
|
const rangeLength = `${range}`.length;
|
||||||
let wrappedLength = 0;
|
let wrappedLength = 0;
|
||||||
|
@ -87,6 +87,9 @@ const tokenSetter = providerId => ({ getters, dispatch }, token) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// For workspaces
|
||||||
|
const urlParser = window.document.createElement('a');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
@ -118,18 +121,24 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
workspaces: (state) => {
|
workspaces: (state, getters, rootState, rootGetters) => {
|
||||||
const workspaces = (state.lsItemMap.workspaces || empty('workspaces')).data;
|
const workspaces = (state.lsItemMap.workspaces || empty('workspaces')).data;
|
||||||
const result = {};
|
const sanitizedWorkspaces = {};
|
||||||
|
const mainWorkspaceToken = rootGetters['workspace/mainWorkspaceToken'];
|
||||||
Object.entries(workspaces).forEach(([id, workspace]) => {
|
Object.entries(workspaces).forEach(([id, workspace]) => {
|
||||||
result[id] = {
|
const sanitizedWorkspace = {
|
||||||
...workspace,
|
|
||||||
id,
|
id,
|
||||||
providerId: workspace.providerId || 'googleDriveWorkspace',
|
providerId: mainWorkspaceToken && 'googleDriveAppData',
|
||||||
url: utils.addQueryParams('app'),
|
sub: mainWorkspaceToken && mainWorkspaceToken.sub,
|
||||||
|
...workspace,
|
||||||
};
|
};
|
||||||
|
// Rebuild the url with current hostname
|
||||||
|
urlParser.href = workspace.url || 'app';
|
||||||
|
const params = utils.parseQueryParams(urlParser.hash.slice(1));
|
||||||
|
sanitizedWorkspace.url = utils.addQueryParams('app', params, true);
|
||||||
|
sanitizedWorkspaces[id] = sanitizedWorkspace;
|
||||||
});
|
});
|
||||||
return result;
|
return sanitizedWorkspaces;
|
||||||
},
|
},
|
||||||
settings: getter('settings'),
|
settings: getter('settings'),
|
||||||
computedSettings: (state, getters) => {
|
computedSettings: (state, getters) => {
|
||||||
@ -202,13 +211,6 @@ export default {
|
|||||||
githubTokens: (state, getters) => getters.tokens.github || {},
|
githubTokens: (state, getters) => getters.tokens.github || {},
|
||||||
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
||||||
zendeskTokens: (state, getters) => getters.tokens.zendesk || {},
|
zendeskTokens: (state, getters) => getters.tokens.zendesk || {},
|
||||||
loginToken: (state, getters) => {
|
|
||||||
// Return the first google token that has the isLogin flag
|
|
||||||
const googleTokens = getters.googleTokens;
|
|
||||||
const loginSubs = Object.keys(googleTokens)
|
|
||||||
.filter(sub => googleTokens[sub].isLogin);
|
|
||||||
return googleTokens[loginSubs[0]];
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setWorkspaces: setter('workspaces'),
|
setWorkspaces: setter('workspaces'),
|
||||||
|
@ -123,7 +123,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
|
createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
|
||||||
const loginToken = rootGetters['data/loginToken'];
|
const loginToken = rootGetters['workspace/loginToken'];
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
dispatch('modal/signInForComment', {
|
dispatch('modal/signInForComment', {
|
||||||
onResolve: () => googleHelper.signin()
|
onResolve: () => googleHelper.signin()
|
||||||
|
@ -18,6 +18,7 @@ import modal from './modal';
|
|||||||
import notification from './notification';
|
import notification from './notification';
|
||||||
import queue from './queue';
|
import queue from './queue';
|
||||||
import userInfo from './userInfo';
|
import userInfo from './userInfo';
|
||||||
|
import workspace from './workspace';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ const store = new Vuex.Store({
|
|||||||
notification,
|
notification,
|
||||||
queue,
|
queue,
|
||||||
userInfo,
|
userInfo,
|
||||||
|
workspace,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
offline: false,
|
offline: false,
|
||||||
@ -55,8 +57,8 @@ const store = new Vuex.Store({
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
isSponsor: (state, getters) => {
|
isSponsor: (state, getters) => {
|
||||||
const loginToken = getters['data/loginToken'];
|
const sponsorToken = getters['workspace/sponsorToken'];
|
||||||
return state.monetizeSponsor || (loginToken && loginToken.isSponsor);
|
return state.monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
|
@ -75,8 +75,13 @@ export default {
|
|||||||
resolveText: 'Yes, revert',
|
resolveText: 'Yes, revert',
|
||||||
rejectText: 'No',
|
rejectText: 'No',
|
||||||
}),
|
}),
|
||||||
|
removeWorkspace: ({ dispatch }) => dispatch('open', {
|
||||||
|
content: '<p>This will clean your workspace locally. Are you sure?</p>',
|
||||||
|
resolveText: 'Yes, clean',
|
||||||
|
rejectText: 'No',
|
||||||
|
}),
|
||||||
reset: ({ dispatch }) => dispatch('open', {
|
reset: ({ dispatch }) => dispatch('open', {
|
||||||
content: '<p>This will clean your local files and settings. Are you sure?</p>',
|
content: '<p>This will clean all your workspaces locally. Are you sure?</p>',
|
||||||
resolveText: 'Yes, clean',
|
resolveText: 'Yes, clean',
|
||||||
rejectText: 'No',
|
rejectText: 'No',
|
||||||
}),
|
}),
|
||||||
|
@ -1,38 +1,50 @@
|
|||||||
import utils from '../services/utils';
|
|
||||||
import googleHelper from '../services/providers/helpers/googleHelper';
|
|
||||||
import syncSvc from '../services/syncSvc';
|
|
||||||
|
|
||||||
const idShifter = offset => (state, getters) => {
|
|
||||||
const ids = Object.keys(getters.currentFileDiscussions);
|
|
||||||
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
|
|
||||||
return ids[idx % ids.length];
|
|
||||||
};
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
currentWorkspaceId: null,
|
currentWorkspaceId: null,
|
||||||
|
lastFocus: 0,
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setCurrentWorkspaceId: (state, value) => {
|
setCurrentWorkspaceId: (state, value) => {
|
||||||
state.currentWorkspaceId = value;
|
state.currentWorkspaceId = value;
|
||||||
},
|
},
|
||||||
|
setLastFocus: (state, value) => {
|
||||||
|
state.lastFocus = value;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
|
mainWorkspace: (state, getters, rootState, rootGetters) => {
|
||||||
|
const workspaces = rootGetters['data/workspaces'];
|
||||||
|
return workspaces.main;
|
||||||
|
},
|
||||||
currentWorkspace: (state, getters, rootState, rootGetters) => {
|
currentWorkspace: (state, getters, rootState, rootGetters) => {
|
||||||
const workspaces = rootGetters['data/workspaces'];
|
const workspaces = rootGetters['data/workspaces'];
|
||||||
return workspaces[state.currentWorkspaceId] || workspaces.main;
|
return workspaces[state.currentWorkspaceId] || getters.mainWorkspace;
|
||||||
},
|
},
|
||||||
lastSyncActivityKey: (state, getters) => {
|
lastSyncActivityKey: (state, getters) => `${getters.currentWorkspace.id}/lastSyncActivity`,
|
||||||
const workspaceId = getters.currentWorkspace.id;
|
lastFocusKey: (state, getters) => `${getters.currentWorkspace.id}/lastWindowFocus`,
|
||||||
return `${workspaceId}/lastSyncActivity`;
|
mainWorkspaceToken: (state, getters, rootState, rootGetters) => {
|
||||||
|
const googleTokens = rootGetters['data/googleTokens'];
|
||||||
|
const loginSubs = Object.keys(googleTokens)
|
||||||
|
.filter(sub => googleTokens[sub].isLogin);
|
||||||
|
return googleTokens[loginSubs[0]];
|
||||||
},
|
},
|
||||||
lastFocusKey: (state, getters) => {
|
syncToken: (state, getters, rootState, rootGetters) => {
|
||||||
const workspaceId = getters.currentWorkspace.id;
|
const workspace = getters.currentWorkspace;
|
||||||
return `${workspaceId}/lastWindowFocus`;
|
if (workspace.providerId === 'googleDriveWorkspace') {
|
||||||
|
const googleTokens = rootGetters['data/googleTokens'];
|
||||||
|
return googleTokens[workspace.sub];
|
||||||
|
}
|
||||||
|
return getters.mainWorkspaceToken;
|
||||||
},
|
},
|
||||||
lastFocus: (state, getters) => parseInt(localStorage.getItem(getters.lastFocusKey), 10) || 0,
|
loginToken: (state, getters) => getters.syncToken,
|
||||||
|
sponsorToken: (state, getters) => getters.mainWorkspaceToken,
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
|
setCurrentWorkspaceId: ({ commit, getters }, value) => {
|
||||||
|
commit('setCurrentWorkspaceId', value);
|
||||||
|
const lastFocus = parseInt(localStorage.getItem(getters.lastFocusKey), 10) || 0;
|
||||||
|
commit('setLastFocus', lastFocus);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user