Workspaces (part 1)
This commit is contained in:
parent
3a08bc617e
commit
9596339684
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<splash-screen v-if="!ready"></splash-screen>
|
<div class="app">
|
||||||
<div v-else class="app">
|
<splash-screen v-if="!ready"></splash-screen>
|
||||||
<layout></layout>
|
<layout v-else></layout>
|
||||||
<modal v-if="showModal"></modal>
|
<modal v-if="showModal"></modal>
|
||||||
<notification></notification>
|
<notification></notification>
|
||||||
</div>
|
</div>
|
||||||
@ -9,11 +9,11 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { mapState } from 'vuex';
|
|
||||||
import Layout from './Layout';
|
import Layout from './Layout';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import Notification from './Notification';
|
import Notification from './Notification';
|
||||||
import SplashScreen from './SplashScreen';
|
import SplashScreen from './SplashScreen';
|
||||||
|
import syncSvc from '../services/syncSvc';
|
||||||
import timeSvc from '../services/timeSvc';
|
import timeSvc from '../services/timeSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
|
||||||
@ -66,14 +66,20 @@ export default {
|
|||||||
Notification,
|
Notification,
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
},
|
},
|
||||||
|
data: () => ({
|
||||||
|
ready: false,
|
||||||
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapState([
|
|
||||||
'ready',
|
|
||||||
]),
|
|
||||||
showModal() {
|
showModal() {
|
||||||
return !!this.$store.getters['modal/config'];
|
return !!this.$store.getters['modal/config'];
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
syncSvc.init()
|
||||||
|
.then(() => {
|
||||||
|
this.ready = true;
|
||||||
|
});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="button-bar">
|
<div class="button-bar">
|
||||||
<div class="button-bar__inner button-bar__inner--top">
|
<div class="button-bar__inner button-bar__inner--top">
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showNavigationBar }" @click="toggleNavigationBar()" v-title="'Toggle navigation bar'">
|
||||||
<icon-navigation-bar></icon-navigation-bar>
|
<icon-navigation-bar></icon-navigation-bar>
|
||||||
</button>
|
</button>
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showSidePreview }" @click="toggleSidePreview()" v-title="'Toggle side preview'">
|
||||||
<icon-side-preview></icon-side-preview>
|
<icon-side-preview></icon-side-preview>
|
||||||
</button>
|
</button>
|
||||||
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
<button class="button-bar__button button" @click="toggleEditor(false)" v-title="'Reader mode'">
|
||||||
@ -12,13 +12,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-bar__inner button-bar__inner--bottom">
|
<div class="button-bar__inner button-bar__inner--bottom">
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.focusMode }" @click="toggleFocusMode()" v-title="'Toggle focus mode'">
|
||||||
<icon-target></icon-target>
|
<icon-target></icon-target>
|
||||||
</button>
|
</button>
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.scrollSync }" @click="toggleScrollSync()" v-title="'Toggle scroll sync'">
|
||||||
<icon-scroll-sync></icon-scroll-sync>
|
<icon-scroll-sync></icon-scroll-sync>
|
||||||
</button>
|
</button>
|
||||||
<button class="button-bar__button button" :class="{ 'button-bar__button--on': localSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
<button class="button-bar__button button" :class="{ 'button-bar__button--on': layoutSettings.showStatusBar }" @click="toggleStatusBar()" v-title="'Toggle status bar'">
|
||||||
<icon-status-bar></icon-status-bar>
|
<icon-status-bar></icon-status-bar>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -30,7 +30,7 @@ import { mapGetters, mapActions } from 'vuex';
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
computed: mapGetters('data', [
|
computed: mapGetters('data', [
|
||||||
'localSettings',
|
'layoutSettings',
|
||||||
]),
|
]),
|
||||||
methods: mapActions('data', [
|
methods: mapActions('data', [
|
||||||
'toggleNavigationBar',
|
'toggleNavigationBar',
|
||||||
|
@ -84,7 +84,10 @@ export default {
|
|||||||
if (node.isFolder) {
|
if (node.isFolder) {
|
||||||
this.$store.commit('explorer/toggleOpenNode', id);
|
this.$store.commit('explorer/toggleOpenNode', id);
|
||||||
} else {
|
} else {
|
||||||
this.$store.commit('file/setCurrentId', id);
|
// Prevent from freezing the UI while loading the file
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$store.commit('file/setCurrentId', id);
|
||||||
|
}, 10);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -47,12 +47,12 @@ const accessor = (fieldName, setterName) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const computedLocalSetting = key => ({
|
const computedLayoutSetting = key => ({
|
||||||
get() {
|
get() {
|
||||||
return store.getters['data/localSettings'][key];
|
return store.getters['data/layoutSettings'][key];
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
store.dispatch('data/patchLocalSettings', {
|
store.dispatch('data/patchLayoutSettings', {
|
||||||
[key]: value,
|
[key]: value,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -95,14 +95,13 @@ export default {
|
|||||||
]),
|
]),
|
||||||
findText: accessor('findText', 'setFindText'),
|
findText: accessor('findText', 'setFindText'),
|
||||||
replaceText: accessor('replaceText', 'setReplaceText'),
|
replaceText: accessor('replaceText', 'setReplaceText'),
|
||||||
findCaseSensitive: computedLocalSetting('findCaseSensitive'),
|
findCaseSensitive: computedLayoutSetting('findCaseSensitive'),
|
||||||
findUseRegexp: computedLocalSetting('findUseRegexp'),
|
findUseRegexp: computedLayoutSetting('findUseRegexp'),
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
highlightOccurrences() {
|
highlightOccurrences() {
|
||||||
const oldClassAppliers = {};
|
const oldClassAppliers = {};
|
||||||
Object.keys(this.classAppliers).forEach((key) => {
|
Object.entries(this.classAppliers).forEach(([, classApplier]) => {
|
||||||
const classApplier = this.classAppliers[key];
|
|
||||||
const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
|
const newKey = `${classApplier.startMarker.offset}:${classApplier.endMarker.offset}`;
|
||||||
oldClassAppliers[newKey] = classApplier;
|
oldClassAppliers[newKey] = classApplier;
|
||||||
});
|
});
|
||||||
@ -137,8 +136,7 @@ export default {
|
|||||||
this.state = 'created';
|
this.state = 'created';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.keys(oldClassAppliers).forEach((key) => {
|
Object.entries(oldClassAppliers).forEach(([key, classApplier]) => {
|
||||||
const classApplier = oldClassAppliers[key];
|
|
||||||
if (!this.classAppliers[key]) {
|
if (!this.classAppliers[key]) {
|
||||||
classApplier.clean();
|
classApplier.clean();
|
||||||
if (classApplier === this.selectedClassApplier) {
|
if (classApplier === this.selectedClassApplier) {
|
||||||
|
@ -11,10 +11,12 @@
|
|||||||
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
<image-modal v-else-if="config.type === 'image'"></image-modal>
|
||||||
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
<sync-management-modal v-else-if="config.type === 'syncManagement'"></sync-management-modal>
|
||||||
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
<publish-management-modal v-else-if="config.type === 'publishManagement'"></publish-management-modal>
|
||||||
|
<workspace-management-modal v-else-if="config.type === 'workspaceManagement'"></workspace-management-modal>
|
||||||
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
<sponsor-modal v-else-if="config.type === 'sponsor'"></sponsor-modal>
|
||||||
<!-- Providers -->
|
<!-- Providers -->
|
||||||
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
<google-photo-modal v-else-if="config.type === 'googlePhoto'"></google-photo-modal>
|
||||||
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
|
<google-drive-save-modal v-else-if="config.type === 'googleDriveSave'"></google-drive-save-modal>
|
||||||
|
<google-drive-workspace-modal v-else-if="config.type === 'googleDriveWorkspace'"></google-drive-workspace-modal>
|
||||||
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
<google-drive-publish-modal v-else-if="config.type === 'googleDrivePublish'"></google-drive-publish-modal>
|
||||||
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
<dropbox-account-modal v-else-if="config.type === 'dropboxAccount'"></dropbox-account-modal>
|
||||||
<dropbox-save-modal v-else-if="config.type === 'dropboxSave'"></dropbox-save-modal>
|
<dropbox-save-modal v-else-if="config.type === 'dropboxSave'"></dropbox-save-modal>
|
||||||
@ -55,11 +57,13 @@ import LinkModal from './modals/LinkModal';
|
|||||||
import ImageModal from './modals/ImageModal';
|
import ImageModal from './modals/ImageModal';
|
||||||
import SyncManagementModal from './modals/SyncManagementModal';
|
import SyncManagementModal from './modals/SyncManagementModal';
|
||||||
import PublishManagementModal from './modals/PublishManagementModal';
|
import PublishManagementModal from './modals/PublishManagementModal';
|
||||||
|
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
||||||
import SponsorModal from './modals/SponsorModal';
|
import SponsorModal from './modals/SponsorModal';
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
import GooglePhotoModal from './modals/providers/GooglePhotoModal';
|
||||||
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
import GoogleDriveSaveModal from './modals/providers/GoogleDriveSaveModal';
|
||||||
|
import GoogleDriveWorkspaceModal from './modals/providers/GoogleDriveWorkspaceModal';
|
||||||
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
import GoogleDrivePublishModal from './modals/providers/GoogleDrivePublishModal';
|
||||||
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
|
import DropboxAccountModal from './modals/providers/DropboxAccountModal';
|
||||||
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
|
import DropboxSaveModal from './modals/providers/DropboxSaveModal';
|
||||||
@ -94,10 +98,12 @@ export default {
|
|||||||
ImageModal,
|
ImageModal,
|
||||||
SyncManagementModal,
|
SyncManagementModal,
|
||||||
PublishManagementModal,
|
PublishManagementModal,
|
||||||
|
WorkspaceManagementModal,
|
||||||
SponsorModal,
|
SponsorModal,
|
||||||
// Providers
|
// Providers
|
||||||
GooglePhotoModal,
|
GooglePhotoModal,
|
||||||
GoogleDriveSaveModal,
|
GoogleDriveSaveModal,
|
||||||
|
GoogleDriveWorkspaceModal,
|
||||||
GoogleDrivePublishModal,
|
GoogleDrivePublishModal,
|
||||||
DropboxAccountModal,
|
DropboxAccountModal,
|
||||||
DropboxSaveModal,
|
DropboxSaveModal,
|
||||||
@ -178,6 +184,10 @@ export default {
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background-color: rgba(160, 160, 160, 0.5);
|
background-color: rgba(160, 160, 160, 0.5);
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
|
|
||||||
|
hr {
|
||||||
|
margin: 0.5em 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal__inner-1 {
|
.modal__inner-1 {
|
||||||
|
@ -219,6 +219,9 @@ export default {
|
|||||||
|
|
||||||
.navigation-bar__inner--right {
|
.navigation-bar__inner--right {
|
||||||
float: right;
|
float: right;
|
||||||
|
|
||||||
|
/* prevent from seeing wrapped buttons */
|
||||||
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar__inner--button {
|
.navigation-bar__inner--button {
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="side-bar__inner">
|
<div class="side-bar__inner">
|
||||||
<main-menu v-if="panel === 'menu'"></main-menu>
|
<main-menu v-if="panel === 'menu'"></main-menu>
|
||||||
|
<workspaces-menu v-if="panel === 'workspaces'"></workspaces-menu>
|
||||||
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
<sync-menu v-else-if="panel === 'sync'"></sync-menu>
|
||||||
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
|
||||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||||
@ -33,6 +34,7 @@
|
|||||||
import { mapActions } from 'vuex';
|
import { mapActions } from 'vuex';
|
||||||
import Toc from './Toc';
|
import Toc from './Toc';
|
||||||
import MainMenu from './menus/MainMenu';
|
import MainMenu from './menus/MainMenu';
|
||||||
|
import WorkspacesMenu from './menus/WorkspacesMenu';
|
||||||
import SyncMenu from './menus/SyncMenu';
|
import SyncMenu from './menus/SyncMenu';
|
||||||
import PublishMenu from './menus/PublishMenu';
|
import PublishMenu from './menus/PublishMenu';
|
||||||
import HistoryMenu from './menus/HistoryMenu';
|
import HistoryMenu from './menus/HistoryMenu';
|
||||||
@ -43,6 +45,7 @@ import markdownConversionSvc from '../services/markdownConversionSvc';
|
|||||||
|
|
||||||
const panelNames = {
|
const panelNames = {
|
||||||
menu: 'Menu',
|
menu: 'Menu',
|
||||||
|
workspaces: 'Workspaces',
|
||||||
help: 'Markdown cheat sheet',
|
help: 'Markdown cheat sheet',
|
||||||
toc: 'Table of contents',
|
toc: 'Table of contents',
|
||||||
sync: 'Synchronize',
|
sync: 'Synchronize',
|
||||||
@ -56,6 +59,7 @@ export default {
|
|||||||
components: {
|
components: {
|
||||||
Toc,
|
Toc,
|
||||||
MainMenu,
|
MainMenu,
|
||||||
|
WorkspacesMenu,
|
||||||
SyncMenu,
|
SyncMenu,
|
||||||
PublishMenu,
|
PublishMenu,
|
||||||
HistoryMenu,
|
HistoryMenu,
|
||||||
@ -67,7 +71,7 @@ export default {
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
panel() {
|
panel() {
|
||||||
return this.$store.getters['data/localSettings'].sideBarPanel;
|
return this.$store.getters['data/layoutSettings'].sideBarPanel;
|
||||||
},
|
},
|
||||||
panelName() {
|
panelName() {
|
||||||
return panelNames[this.panel];
|
return panelNames[this.panel];
|
||||||
|
@ -63,13 +63,13 @@ export default {
|
|||||||
'setCurrentDiscussionId',
|
'setCurrentDiscussionId',
|
||||||
]),
|
]),
|
||||||
updateTops() {
|
updateTops() {
|
||||||
const localSettings = this.$store.getters['data/localSettings'];
|
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
||||||
const minTop = -2;
|
const minTop = -2;
|
||||||
let minCommentTop = minTop;
|
let minCommentTop = minTop;
|
||||||
const getTop = (discussion, commentElt1, commentElt2, isCurrent) => {
|
const getTop = (discussion, commentElt1, commentElt2, isCurrent) => {
|
||||||
const firstElt = commentElt1 || commentElt2;
|
const firstElt = commentElt1 || commentElt2;
|
||||||
const secondElt = commentElt1 && commentElt2;
|
const secondElt = commentElt1 && commentElt2;
|
||||||
const coordinates = localSettings.showEditor
|
const coordinates = layoutSettings.showEditor
|
||||||
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||||
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||||
let commentTop = minTop;
|
let commentTop = minTop;
|
||||||
@ -98,10 +98,9 @@ export default {
|
|||||||
// Get the discussion top coordinates
|
// Get the discussion top coordinates
|
||||||
const tops = {};
|
const tops = {};
|
||||||
const discussions = this.currentFileDiscussions;
|
const discussions = this.currentFileDiscussions;
|
||||||
Object.keys(discussions)
|
Object.entries(discussions)
|
||||||
.sort((id1, id2) => discussions[id1].end - discussions[id2].end)
|
.sort(([, discussion1], [, discussion2]) => discussion1.end - discussion2.end)
|
||||||
.forEach((discussionId) => {
|
.forEach(([discussionId, discussion]) => {
|
||||||
const discussion = this.currentFileDiscussions[discussionId];
|
|
||||||
if (discussion === this.currentDiscussion || discussion === this.newDiscussion) {
|
if (discussion === this.currentDiscussion || discussion === this.newDiscussion) {
|
||||||
tops.current = getTop(
|
tops.current = getTop(
|
||||||
discussion,
|
discussion,
|
||||||
@ -123,8 +122,8 @@ export default {
|
|||||||
() => this.updateTops(),
|
() => this.updateTops(),
|
||||||
{ immediate: true });
|
{ immediate: true });
|
||||||
|
|
||||||
const localSettings = this.$store.getters['data/localSettings'];
|
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
||||||
this.scrollerElt = localSettings.showEditor
|
this.scrollerElt = layoutSettings.showEditor
|
||||||
? editorSvc.editorElt.parentNode
|
? editorSvc.editorElt.parentNode
|
||||||
: editorSvc.previewElt.parentNode;
|
: editorSvc.previewElt.parentNode;
|
||||||
|
|
||||||
|
@ -68,15 +68,15 @@ export default {
|
|||||||
]),
|
]),
|
||||||
goToDiscussion(discussionId = this.currentDiscussionId) {
|
goToDiscussion(discussionId = this.currentDiscussionId) {
|
||||||
this.setCurrentDiscussionId(discussionId);
|
this.setCurrentDiscussionId(discussionId);
|
||||||
const localSettings = this.$store.getters['data/localSettings'];
|
const layoutSettings = this.$store.getters['data/layoutSettings'];
|
||||||
const discussion = this.currentFileDiscussions[discussionId];
|
const discussion = this.currentFileDiscussions[discussionId];
|
||||||
const coordinates = localSettings.showEditor
|
const coordinates = layoutSettings.showEditor
|
||||||
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
? editorSvc.clEditor.selectionMgr.getCoordinates(discussion.end)
|
||||||
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
: editorSvc.getPreviewOffsetCoordinates(editorSvc.getPreviewOffset(discussion.end));
|
||||||
if (!coordinates) {
|
if (!coordinates) {
|
||||||
this.$store.dispatch('notification/info', "Discussion can't be located in the file.");
|
this.$store.dispatch('notification/info', "Discussion can't be located in the file.");
|
||||||
} else {
|
} else {
|
||||||
const scrollerElt = localSettings.showEditor
|
const scrollerElt = layoutSettings.showEditor
|
||||||
? editorSvc.editorElt.parentNode
|
? editorSvc.editorElt.parentNode
|
||||||
: editorSvc.previewElt.parentNode;
|
: editorSvc.previewElt.parentNode;
|
||||||
let scrollTop = coordinates.top - (scrollerElt.offsetHeight / 2);
|
let scrollTop = coordinates.top - (scrollerElt.offsetHeight / 2);
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--history">
|
<div class="history side-bar__panel">
|
||||||
<a class="revision button flex flex--row" href="javascript:void(0)" v-for="revision in revisions" :key="revision.id" @click="open(revision)">
|
<div class="revision" v-for="revision in revisions" :key="revision.id">
|
||||||
<div class="revision__icon">
|
<div class="history__spacer" v-if="revision.spacer"></div>
|
||||||
<user-image :user-id="revision.sub"></user-image>
|
<a class="revision__button button flex flex--row" href="javascript:void(0)" @click="open(revision)">
|
||||||
</div>
|
<div class="revision__icon">
|
||||||
<div class="revision__header flex flex--column">
|
<user-image :user-id="revision.sub"></user-image>
|
||||||
<user-name :user-id="revision.sub"></user-name>
|
</div>
|
||||||
<div class="revision__created">{{revision.created | formatTime}}</div>
|
<div class="revision__header flex flex--column">
|
||||||
</div>
|
<user-name :user-id="revision.sub"></user-name>
|
||||||
</a>
|
<div class="revision__created">{{revision.created | formatTime}}</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="history__spacer history__spacer--last"></div>
|
||||||
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
<div class="flex flex--row flex--end" v-if="showMoreButton">
|
||||||
<button class="revision__button button" @click="showMore">More</button>
|
<button class="history__button button" @click="showMore">More</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -33,6 +37,7 @@ let cachedFileId;
|
|||||||
let revisionsPromise;
|
let revisionsPromise;
|
||||||
let revisionContentPromises;
|
let revisionContentPromises;
|
||||||
const pageSize = 50;
|
const pageSize = 50;
|
||||||
|
const spacerThreshold = 12 * 60 * 60 * 1000; // 12h
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -46,7 +51,15 @@ export default {
|
|||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
revisions() {
|
revisions() {
|
||||||
return this.allRevisions.slice(0, this.showCount);
|
let previousCreated = 0;
|
||||||
|
return this.allRevisions.slice(0, this.showCount).map((revision) => {
|
||||||
|
const revisionWithSpacer = {
|
||||||
|
...revision,
|
||||||
|
spacer: revision.created + spacerThreshold < previousCreated,
|
||||||
|
};
|
||||||
|
previousCreated = revision.created;
|
||||||
|
return revisionWithSpacer;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
showMoreButton() {
|
showMoreButton() {
|
||||||
return this.showCount < this.allRevisions.length;
|
return this.showCount < this.allRevisions.length;
|
||||||
@ -171,11 +184,34 @@ export default {
|
|||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@import '../common/variables.scss';
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
.side-bar__panel--history {
|
.history {
|
||||||
padding: 5px 5px 50px;
|
padding: 5px 5px 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.revision {
|
.history__button {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.history__spacer {
|
||||||
|
position: relative;
|
||||||
|
height: 40px;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 24px;
|
||||||
|
border-left: 2px dotted $hr-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.history__spacer--last {
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.revision__button {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
height: auto;
|
height: auto;
|
||||||
@ -199,7 +235,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child::before {
|
.revision:first-child &::before {
|
||||||
height: 67%;
|
height: 67%;
|
||||||
top: 33%;
|
top: 33%;
|
||||||
}
|
}
|
||||||
@ -225,11 +261,6 @@ export default {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.revision__button {
|
|
||||||
font-size: 14px;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout--revision {
|
.layout--revision {
|
||||||
.cledit-section *,
|
.cledit-section *,
|
||||||
.cl-preview-section * {
|
.cl-preview-section * {
|
||||||
|
@ -11,6 +11,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
<span>Signed in as <b>{{loginToken.name}}</b>.</span>
|
||||||
</div>
|
</div>
|
||||||
|
<menu-entry @click.native="setPanel('workspaces')">
|
||||||
|
<icon-database slot="icon"></icon-database>
|
||||||
|
<div>Workspaces</div>
|
||||||
|
<span>Switch to another workspace.</span>
|
||||||
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('sync')">
|
<menu-entry @click.native="setPanel('sync')">
|
||||||
<icon-sync slot="icon"></icon-sync>
|
<icon-sync slot="icon"></icon-sync>
|
||||||
@ -22,17 +27,16 @@
|
|||||||
<div>Publish</div>
|
<div>Publish</div>
|
||||||
<span>Export your file to the web.</span>
|
<span>Export your file to the web.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="fileProperties">
|
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
|
||||||
<div>File properties</div>
|
|
||||||
<span>Add metadata and configure extensions.</span>
|
|
||||||
</menu-entry>
|
|
||||||
<menu-entry @click.native="history">
|
<menu-entry @click.native="history">
|
||||||
<icon-history slot="icon"></icon-history>
|
<icon-history slot="icon"></icon-history>
|
||||||
<div>File history</div>
|
<div>File history</div>
|
||||||
<span>Track and restore file revisions.</span>
|
<span>Track and restore file revisions.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="fileProperties">
|
||||||
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
|
<div>File properties</div>
|
||||||
|
<span>Add metadata and configure extensions.</span>
|
||||||
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('toc')">
|
<menu-entry @click.native="setPanel('toc')">
|
||||||
<icon-toc slot="icon"></icon-toc>
|
<icon-toc slot="icon"></icon-toc>
|
||||||
@ -42,7 +46,6 @@
|
|||||||
<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>
|
||||||
<hr>
|
|
||||||
<menu-entry @click.native="print">
|
<menu-entry @click.native="print">
|
||||||
<icon-printer slot="icon"></icon-printer>
|
<icon-printer slot="icon"></icon-printer>
|
||||||
Print
|
Print
|
||||||
@ -122,9 +125,10 @@ export default {
|
|||||||
history() {
|
history() {
|
||||||
const loginToken = this.$store.getters['data/loginToken'];
|
const loginToken = this.$store.getters['data/loginToken'];
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
this.$store.dispatch('modal/signInForHistory')
|
this.$store.dispatch('modal/signInForHistory', {
|
||||||
.then(() => googleHelper.signin())
|
onResolve: () => googleHelper.signin()
|
||||||
.then(() => syncSvc.requestSync())
|
.then(() => syncSvc.requestSync()),
|
||||||
|
})
|
||||||
.catch(() => { }); // Cancel
|
.catch(() => { }); // Cancel
|
||||||
} else {
|
} else {
|
||||||
this.setPanel('history');
|
this.setPanel('history');
|
||||||
|
60
src/components/menus/WorkspacesMenu.vue
Normal file
60
src/components/menus/WorkspacesMenu.vue
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<template>
|
||||||
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
|
<div class="workspace" v-for="(workspace, id) in workspaces" :key="id">
|
||||||
|
<menu-entry :href="workspace.url" target="_blank">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<div class="workspace__name">{{workspace.name}}</div>
|
||||||
|
<span>{{workspace.url}}</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="addGoogleDriveWorkspace">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||||
|
<span>Add Google Drive workspace</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="manageWorkspaces">
|
||||||
|
<icon-database slot="icon"></icon-database>
|
||||||
|
<span>Manage workspaces</span>
|
||||||
|
</menu-entry>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import MenuEntry from './common/MenuEntry';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('data', [
|
||||||
|
'workspaces',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addGoogleDriveWorkspace() {
|
||||||
|
return googleHelper.addDriveAccount()
|
||||||
|
.then(token => this.$store.dispatch('modal/open', {
|
||||||
|
type: 'googleDriveWorkspace',
|
||||||
|
token,
|
||||||
|
}))
|
||||||
|
.catch(() => {}); // Cancel
|
||||||
|
},
|
||||||
|
manageWorkspaces() {
|
||||||
|
return this.$store.dispatch('modal/open', 'workspaceManagement');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.workspace__name {
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
.menu-entry div & {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -24,7 +24,7 @@
|
|||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
text-decoration-skip: ink;
|
text-decoration-skip: ink;
|
||||||
|
|
||||||
&.menu-entry__sponsor {
|
.menu-entry__sponsor {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,10 +37,9 @@ export default modalTemplate({
|
|||||||
computed: {
|
computed: {
|
||||||
googlePhotosTokens() {
|
googlePhotosTokens() {
|
||||||
const googleToken = this.$store.getters['data/googleTokens'];
|
const googleToken = this.$store.getters['data/googleTokens'];
|
||||||
return Object.keys(googleToken)
|
return Object.entries(googleToken)
|
||||||
.map(sub => googleToken[sub])
|
.filter(([, token]) => token.isPhotos)
|
||||||
.filter(token => token.isPhotos)
|
.sort(([, token1], [, token2]) => token1.name.localeCompare(token2.name));
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
<p v-if="publishLocations.length"><b>{{currentFileName}}</b> is published to the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not published yet.</p>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="location in publishLocations" :key="location.id" class="publish-entry flex flex--row flex--align-center">
|
<div class="publish-entry flex flex--row flex--align-center" v-for="location in publishLocations" :key="location.id">
|
||||||
<div class="publish-entry__icon flex flex--column flex--center">
|
<div class="publish-entry__icon flex flex--column flex--center">
|
||||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -26,8 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.resolve()">Close</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
<p v-if="syncLocations.length"><b>{{currentFileName}}</b> is synchronized with the following location(s):</p>
|
||||||
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
<p v-else><b>{{currentFileName}}</b> is not synchronized yet.</p>
|
||||||
<div>
|
<div>
|
||||||
<div v-for="location in syncLocations" :key="location.id" class="sync-entry flex flex--row flex--align-center">
|
<div class="sync-entry flex flex--row flex--align-center" v-for="location in syncLocations" :key="location.id">
|
||||||
<div class="sync-entry__icon flex flex--column flex--center">
|
<div class="sync-entry__icon flex flex--column flex--center">
|
||||||
<icon-provider :provider-id="location.providerId"></icon-provider>
|
<icon-provider :provider-id="location.providerId"></icon-provider>
|
||||||
</div>
|
</div>
|
||||||
@ -26,8 +26,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<button class="button" @click="config.resolve()">Close</button>
|
||||||
<button class="button" @click="config.resolve()">Ok</button>
|
|
||||||
</div>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
|
@ -95,12 +95,12 @@ export default {
|
|||||||
(allTemplates) => {
|
(allTemplates) => {
|
||||||
const templates = {};
|
const templates = {};
|
||||||
// Sort templates by name
|
// Sort templates by name
|
||||||
Object.keys(allTemplates)
|
Object.entries(allTemplates)
|
||||||
.sort((id1, id2) => collator.compare(allTemplates[id1].name, allTemplates[id2].name))
|
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
|
||||||
.forEach((id) => {
|
.forEach(([id, template]) => {
|
||||||
const template = utils.deepCopy(allTemplates[id]);
|
const templateClone = utils.deepCopy(template);
|
||||||
fillEmptyFields(template);
|
fillEmptyFields(templateClone);
|
||||||
templates[id] = template;
|
templates[id] = templateClone;
|
||||||
});
|
});
|
||||||
this.templates = templates;
|
this.templates = templates;
|
||||||
this.selectedId = this.config.selectedId;
|
this.selectedId = this.config.selectedId;
|
||||||
|
133
src/components/modals/WorkspaceManagementModal.vue
Normal file
133
src/components/modals/WorkspaceManagementModal.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner class="modal__inner-1--workspace-management" aria-label="Manage workspaces">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="workspace-entry flex flex--row flex--align-center" v-for="(workspace, id) in workspaces" :key="id">
|
||||||
|
<div class="workspace-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-provider :provider-id="workspace.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<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>
|
||||||
|
{{workspace.name}}
|
||||||
|
</div>
|
||||||
|
<div class="workspace-entry__buttons flex flex--row flex--center">
|
||||||
|
<button class="workspace-entry__button button" @click="edit(id)">
|
||||||
|
<icon-pen></icon-pen>
|
||||||
|
</button>
|
||||||
|
<a class="workspace-entry__button button" :href="workspace.url" target="_blank">
|
||||||
|
<icon-open-in-new></icon-open-in-new>
|
||||||
|
</a>
|
||||||
|
<button class="workspace-entry__button button" @click="remove(id)">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.resolve()">Close</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import ModalInner from './common/ModalInner';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
},
|
||||||
|
data: () => ({
|
||||||
|
editedId: null,
|
||||||
|
editingName: '',
|
||||||
|
}),
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
...mapGetters('data', [
|
||||||
|
'workspaces',
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
edit(id) {
|
||||||
|
this.editedId = id;
|
||||||
|
this.editingName = this.workspaces[id].name;
|
||||||
|
},
|
||||||
|
submitEdit(cancel) {
|
||||||
|
const workspace = this.workspaces[this.editedId];
|
||||||
|
if (workspace && !cancel && this.editingName) {
|
||||||
|
this.$store.dispatch('data/patchWorkspaces', {
|
||||||
|
[this.editedId]: {
|
||||||
|
...workspace,
|
||||||
|
name: this.editingName,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.editingName = workspace.name;
|
||||||
|
}
|
||||||
|
this.editedId = null;
|
||||||
|
},
|
||||||
|
remove(id) {
|
||||||
|
const workspaces = {
|
||||||
|
...this.workspaces,
|
||||||
|
};
|
||||||
|
delete workspaces[id];
|
||||||
|
this.$store.dispatch('data/setWorkspaces', workspaces);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../common/variables.scss';
|
||||||
|
|
||||||
|
.modal__inner-1--workspace-management {
|
||||||
|
max-width: 560px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry {
|
||||||
|
text-align: left;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin: 15px 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.5;
|
||||||
|
text-transform: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry__icon {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
margin-right: 12px;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry__name {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry__buttons {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.workspace-entry__button {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 6px;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -32,9 +32,11 @@ export default {
|
|||||||
sponsor() {
|
sponsor() {
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => !this.$store.getters['data/loginToken'] &&
|
.then(() => !this.$store.getters['data/loginToken'] &&
|
||||||
this.$store.dispatch('modal/signInForSponsorship') // If user has to sign in
|
// If user has to sign in
|
||||||
.then(() => googleHelper.signin())
|
this.$store.dispatch('modal/signInForSponsorship', {
|
||||||
.then(() => syncSvc.requestSync()))
|
onResolve: () => googleHelper.signin()
|
||||||
|
.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');
|
||||||
|
@ -40,8 +40,7 @@ export default (desc) => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
Object.keys(desc.computedLocalSettings || {}).forEach((key) => {
|
Object.entries(desc.computedLocalSettings || {}).forEach(([key, id]) => {
|
||||||
const id = desc.computedLocalSettings[key];
|
|
||||||
component.computed[key] = {
|
component.computed[key] = {
|
||||||
get() {
|
get() {
|
||||||
return store.getters['data/localSettings'][id];
|
return store.getters['data/localSettings'][id];
|
||||||
@ -56,10 +55,10 @@ export default (desc) => {
|
|||||||
component.computed.allTemplates = () => {
|
component.computed.allTemplates = () => {
|
||||||
const allTemplates = store.getters['data/allTemplates'];
|
const allTemplates = store.getters['data/allTemplates'];
|
||||||
const sortedTemplates = {};
|
const sortedTemplates = {};
|
||||||
Object.keys(allTemplates)
|
Object.entries(allTemplates)
|
||||||
.sort((id1, id2) => collator.compare(allTemplates[id1].name, allTemplates[id2].name))
|
.sort(([, template1], [, template2]) => collator.compare(template1.name, template2.name))
|
||||||
.forEach((templateId) => {
|
.forEach(([templateId, template]) => {
|
||||||
sortedTemplates[templateId] = allTemplates[templateId];
|
sortedTemplates[templateId] = template;
|
||||||
});
|
});
|
||||||
return sortedTemplates;
|
return sortedTemplates;
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,59 @@
|
|||||||
|
<template>
|
||||||
|
<modal-inner aria-label="Add Google Drive workspace">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-provider provider-id="googleDrive"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<p>This will create a workspace synchronized with a <b>Google Drive</b> folder.</p>
|
||||||
|
<form-entry label="Folder ID (optional)">
|
||||||
|
<input slot="field" class="textfield" type="text" v-model.trim="folderId" @keyup.enter="resolve()">
|
||||||
|
<div class="form-entry__info">
|
||||||
|
If no folder ID is supplied, a new workspace folder will be created in your root folder.
|
||||||
|
</div>
|
||||||
|
<div class="form-entry__actions">
|
||||||
|
<a href="javascript:void(0)" @click="openFolder">Choose folder</a>
|
||||||
|
</div>
|
||||||
|
</form-entry>
|
||||||
|
</div>
|
||||||
|
<div class="modal__button-bar">
|
||||||
|
<button class="button" @click="config.reject()">Cancel</button>
|
||||||
|
<button class="button" @click="resolve()">Ok</button>
|
||||||
|
</div>
|
||||||
|
</modal-inner>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import googleHelper from '../../../services/providers/helpers/googleHelper';
|
||||||
|
import modalTemplate from '../common/modalTemplate';
|
||||||
|
import utils from '../../../services/utils';
|
||||||
|
|
||||||
|
export default modalTemplate({
|
||||||
|
data: () => ({
|
||||||
|
fileId: '',
|
||||||
|
}),
|
||||||
|
computedLocalSettings: {
|
||||||
|
folderId: 'googleDriveFolderId',
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openFolder() {
|
||||||
|
return this.$store.dispatch(
|
||||||
|
'modal/hideUntil',
|
||||||
|
googleHelper.openPicker(this.config.token, 'folder')
|
||||||
|
.then((folders) => {
|
||||||
|
this.$store.dispatch('data/patchLocalSettings', {
|
||||||
|
googleDriveFolderId: folders[0].id,
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
resolve() {
|
||||||
|
const url = utils.addQueryParams('app', {
|
||||||
|
providerId: 'googleDriveWorkspace',
|
||||||
|
folderId: this.folderId,
|
||||||
|
sub: this.config.token.sub,
|
||||||
|
});
|
||||||
|
this.config.resolve();
|
||||||
|
window.open(url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
13
src/data/defaultLayoutSettings.js
Normal file
13
src/data/defaultLayoutSettings.js
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export default () => ({
|
||||||
|
showNavigationBar: true,
|
||||||
|
showEditor: true,
|
||||||
|
showSidePreview: true,
|
||||||
|
showStatusBar: true,
|
||||||
|
showSideBar: false,
|
||||||
|
showExplorer: false,
|
||||||
|
scrollSync: true,
|
||||||
|
focusMode: false,
|
||||||
|
findCaseSensitive: false,
|
||||||
|
findUseRegexp: false,
|
||||||
|
sideBarPanel: 'menu',
|
||||||
|
});
|
@ -1,16 +1,5 @@
|
|||||||
export default () => ({
|
export default () => ({
|
||||||
welcomeFileHashes: {},
|
welcomeFileHashes: {},
|
||||||
showNavigationBar: true,
|
|
||||||
showEditor: true,
|
|
||||||
showSidePreview: true,
|
|
||||||
showStatusBar: true,
|
|
||||||
showSideBar: false,
|
|
||||||
showExplorer: false,
|
|
||||||
scrollSync: true,
|
|
||||||
focusMode: false,
|
|
||||||
findCaseSensitive: false,
|
|
||||||
findUseRegexp: false,
|
|
||||||
sideBarPanel: 'menu',
|
|
||||||
htmlExportTemplate: 'styledHtml',
|
htmlExportTemplate: 'styledHtml',
|
||||||
pdfExportTemplate: 'styledHtml',
|
pdfExportTemplate: 'styledHtml',
|
||||||
pandocExportFormat: 'pdf',
|
pandocExportFormat: 'pdf',
|
||||||
|
5
src/data/defaultWorkspaces.js
Normal file
5
src/data/defaultWorkspaces.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export default () => ({
|
||||||
|
main: {
|
||||||
|
name: 'Main workspace',
|
||||||
|
},
|
||||||
|
});
|
5
src/icons/Database.vue
Normal file
5
src/icons/Database.vue
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M12,3C7.58,3 4,4.79 4,7C4,9.21 7.58,11 12,11C16.42,11 20,9.21 20,7C20,4.79 16.42,3 12,3M4,9V12C4,14.21 7.58,16 12,16C16.42,16 20,14.21 20,12V9C20,11.21 16.42,13 12,13C7.58,13 4,11.21 4,9M4,14V17C4,19.21 7.58,21 12,21C16.42,21 20,19.21 20,17V14C20,16.21 16.42,18 12,18C7.58,18 4,16.21 4,14Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -1,11 +1,30 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="icon-provider" :class="['icon-provider--' + providerId]">
|
<div class="icon-provider" :class="'icon-provider--' + classState">
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ['providerId'],
|
props: ['providerId'],
|
||||||
|
computed: {
|
||||||
|
classState() {
|
||||||
|
switch (this.providerId) {
|
||||||
|
case 'googleDrive':
|
||||||
|
case 'googleDriveWorkspace':
|
||||||
|
return 'google-drive';
|
||||||
|
case 'googlePhotos':
|
||||||
|
return 'google-photos';
|
||||||
|
case 'dropboxRestricted':
|
||||||
|
return 'dropbox';
|
||||||
|
case 'gist':
|
||||||
|
return 'github';
|
||||||
|
case 'bloggerPage':
|
||||||
|
return 'blogger';
|
||||||
|
default:
|
||||||
|
return this.providerId;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -22,21 +41,19 @@ export default {
|
|||||||
background-image: url(../assets/iconStackedit.svg);
|
background-image: url(../assets/iconStackedit.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-provider--googleDrive {
|
.icon-provider--google-drive {
|
||||||
background-image: url(../assets/iconGoogleDrive.svg);
|
background-image: url(../assets/iconGoogleDrive.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-provider--googlePhotos {
|
.icon-provider--google-photos {
|
||||||
background-image: url(../assets/iconGooglePhotos.svg);
|
background-image: url(../assets/iconGooglePhotos.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-provider--github,
|
.icon-provider--github {
|
||||||
.icon-provider--gist {
|
|
||||||
background-image: url(../assets/iconGithub.svg);
|
background-image: url(../assets/iconGithub.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-provider--dropbox,
|
.icon-provider--dropbox {
|
||||||
.icon-provider--dropboxRestricted {
|
|
||||||
background-image: url(../assets/iconDropbox.svg);
|
background-image: url(../assets/iconDropbox.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,8 +61,7 @@ export default {
|
|||||||
background-image: url(../assets/iconWordpress.svg);
|
background-image: url(../assets/iconWordpress.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-provider--blogger,
|
.icon-provider--blogger {
|
||||||
.icon-provider--bloggerPage {
|
|
||||||
background-image: url(../assets/iconBlogger.svg);
|
background-image: url(../assets/iconBlogger.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -48,6 +48,7 @@ import Redo from './Redo';
|
|||||||
import ContentSave from './ContentSave';
|
import ContentSave from './ContentSave';
|
||||||
import Message from './Message';
|
import Message from './Message';
|
||||||
import History from './History';
|
import History from './History';
|
||||||
|
import Database from './Database';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -98,3 +99,4 @@ Vue.component('iconRedo', Redo);
|
|||||||
Vue.component('iconContentSave', ContentSave);
|
Vue.component('iconContentSave', ContentSave);
|
||||||
Vue.component('iconMessage', Message);
|
Vue.component('iconMessage', Message);
|
||||||
Vue.component('iconHistory', History);
|
Vue.component('iconHistory', History);
|
||||||
|
Vue.component('iconDatabase', Database);
|
||||||
|
@ -15,8 +15,7 @@ export default {
|
|||||||
|
|
||||||
// Parse JSON value
|
// Parse JSON value
|
||||||
const parsedValue = JSON.parse(jsonValue);
|
const parsedValue = JSON.parse(jsonValue);
|
||||||
Object.keys(parsedValue).forEach((id) => {
|
Object.entries(parsedValue).forEach(([id, value]) => {
|
||||||
const value = parsedValue[id];
|
|
||||||
if (value) {
|
if (value) {
|
||||||
const v4Match = id.match(/^file\.([^.]+)\.([^.]+)$/);
|
const v4Match = id.match(/^file\.([^.]+)\.([^.]+)$/);
|
||||||
if (v4Match) {
|
if (v4Match) {
|
||||||
@ -56,8 +55,8 @@ export default {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Go through the maps
|
// Go through the maps
|
||||||
Object.keys(nameMap).forEach(externalId => store.dispatch('createFile', {
|
Object.entries(nameMap).forEach(([externalId, name]) => store.dispatch('createFile', {
|
||||||
name: nameMap[externalId],
|
name,
|
||||||
parentId: folderIdMap[parentIdMap[externalId]],
|
parentId: folderIdMap[parentIdMap[externalId]],
|
||||||
text: textMap[externalId],
|
text: textMap[externalId],
|
||||||
properties: propertiesMap[externalId],
|
properties: propertiesMap[externalId],
|
||||||
|
@ -32,6 +32,16 @@ const diffMatchPatch = new DiffMatchPatch();
|
|||||||
let instantPreview = true;
|
let instantPreview = true;
|
||||||
let tokens;
|
let tokens;
|
||||||
|
|
||||||
|
class SectionDesc {
|
||||||
|
constructor(section, previewElt, tocElt, html) {
|
||||||
|
this.section = section;
|
||||||
|
this.editorElt = section.elt;
|
||||||
|
this.previewElt = previewElt;
|
||||||
|
this.tocElt = tocElt;
|
||||||
|
this.html = html;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use a vue instance as an event bus
|
// Use a vue instance as an event bus
|
||||||
const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, {
|
const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils, {
|
||||||
// Elements
|
// Elements
|
||||||
@ -88,7 +98,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
return this.parsingCtx.sections;
|
return this.parsingCtx.sections;
|
||||||
},
|
},
|
||||||
getCursorFocusRatio: () => {
|
getCursorFocusRatio: () => {
|
||||||
if (store.getters['data/localSettings'].focusMode) {
|
if (store.getters['data/layoutSettings'].focusMode) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
return 0.15;
|
return 0.15;
|
||||||
@ -128,12 +138,8 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
sectionDescIdx += 1;
|
sectionDescIdx += 1;
|
||||||
if (sectionDesc.editorElt !== section.elt) {
|
if (sectionDesc.editorElt !== section.elt) {
|
||||||
// Force textToPreviewDiffs computation
|
// Force textToPreviewDiffs computation
|
||||||
sectionDesc = {
|
sectionDesc = new SectionDesc(
|
||||||
...sectionDesc,
|
section, sectionDesc.previewElt, sectionDesc.tocElt, sectionDesc.html);
|
||||||
section,
|
|
||||||
editorElt: section.elt,
|
|
||||||
textToPreviewDiffs: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
newSectionDescList.push(sectionDesc);
|
newSectionDescList.push(sectionDesc);
|
||||||
previewHtml += sectionDesc.html;
|
previewHtml += sectionDesc.html;
|
||||||
@ -183,16 +189,11 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
}
|
}
|
||||||
|
|
||||||
previewHtml += html;
|
previewHtml += html;
|
||||||
newSectionDescList.push({
|
newSectionDescList.push(new SectionDesc(section, sectionPreviewElt, sectionTocElt, html));
|
||||||
section,
|
|
||||||
editorElt: section.elt,
|
|
||||||
previewElt: sectionPreviewElt,
|
|
||||||
tocElt: sectionTocElt,
|
|
||||||
html,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.sectionDescList = newSectionDescList;
|
this.sectionDescList = newSectionDescList;
|
||||||
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
this.previewHtml = previewHtml.replace(/^\s+|\s+$/g, '');
|
||||||
this.$emit('previewHtml', this.previewHtml);
|
this.$emit('previewHtml', this.previewHtml);
|
||||||
@ -275,7 +276,7 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save editor selection/scroll state into the current file content.
|
* Save editor selection/scroll state into the store.
|
||||||
*/
|
*/
|
||||||
saveContentState: allowDebounce(() => {
|
saveContentState: allowDebounce(() => {
|
||||||
const scrollPosition = editorSvc.getScrollPosition() ||
|
const scrollPosition = editorSvc.getScrollPosition() ||
|
||||||
@ -342,12 +343,12 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
this.tocElt = tocElt;
|
this.tocElt = tocElt;
|
||||||
|
|
||||||
this.createClEditor(editorElt);
|
this.createClEditor(editorElt);
|
||||||
|
|
||||||
this.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
this.clEditor.on('contentChanged', (content, diffs, sectionList) => {
|
||||||
const parsingCtx = {
|
this.parsingCtx = {
|
||||||
...this.parsingCtx,
|
...this.parsingCtx,
|
||||||
sectionList,
|
sectionList,
|
||||||
};
|
};
|
||||||
this.parsingCtx = parsingCtx;
|
|
||||||
});
|
});
|
||||||
this.clEditor.undoMgr.on('undoStateChange', () => {
|
this.clEditor.undoMgr.on('undoStateChange', () => {
|
||||||
const canUndo = this.clEditor.undoMgr.canUndo();
|
const canUndo = this.clEditor.undoMgr.canUndo();
|
||||||
@ -447,11 +448,11 @@ const editorSvc = Object.assign(new Vue(), editorSvcDiscussions, editorSvcUtils,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const triggerImgCacheGc = debounce(() => {
|
const triggerImgCacheGc = debounce(() => {
|
||||||
Object.keys(imgCache).forEach((src) => {
|
Object.entries(imgCache).forEach(([src, entries]) => {
|
||||||
const entries = imgCache[src]
|
// Filter entries that are not attached to the DOM
|
||||||
.filter(imgElt => this.editorElt.contains(imgElt));
|
const filteredEntries = entries.filter(imgElt => this.editorElt.contains(imgElt));
|
||||||
if (entries.length) {
|
if (filteredEntries.length) {
|
||||||
imgCache[src] = entries;
|
imgCache[src] = filteredEntries;
|
||||||
} else {
|
} else {
|
||||||
delete imgCache[src];
|
delete imgCache[src];
|
||||||
}
|
}
|
||||||
|
@ -45,8 +45,7 @@ function syncDiscussionMarkers(content, writeOffsets) {
|
|||||||
...newDiscussion,
|
...newDiscussion,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Object.keys(discussionMarkers).forEach((markerKey) => {
|
Object.entries(discussionMarkers).forEach(([markerKey, marker]) => {
|
||||||
const marker = discussionMarkers[markerKey];
|
|
||||||
// Remove marker if discussion was removed
|
// Remove marker if discussion was removed
|
||||||
const discussion = discussions[marker.discussionId];
|
const discussion = discussions[marker.discussionId];
|
||||||
if (!discussion) {
|
if (!discussion) {
|
||||||
@ -55,8 +54,7 @@ function syncDiscussionMarkers(content, writeOffsets) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
Object.keys(discussions).forEach((discussionId) => {
|
Object.entries(discussions).forEach(([discussionId, discussion]) => {
|
||||||
const discussion = discussions[discussionId];
|
|
||||||
getDiscussionMarkers(discussion, discussionId, writeOffsets
|
getDiscussionMarkers(discussion, discussionId, writeOffsets
|
||||||
? (marker) => {
|
? (marker) => {
|
||||||
discussion[marker.offsetName] = marker.offset;
|
discussion[marker.offsetName] = marker.offset;
|
||||||
@ -73,8 +71,8 @@ function syncDiscussionMarkers(content, writeOffsets) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeDiscussionMarkers() {
|
function removeDiscussionMarkers() {
|
||||||
Object.keys(discussionMarkers).forEach((markerKey) => {
|
Object.entries(discussionMarkers).forEach(([, marker]) => {
|
||||||
clEditor.removeMarker(discussionMarkers[markerKey]);
|
clEditor.removeMarker(marker);
|
||||||
});
|
});
|
||||||
discussionMarkers = {};
|
discussionMarkers = {};
|
||||||
markerKeys = [];
|
markerKeys = [];
|
||||||
@ -138,25 +136,6 @@ export default {
|
|||||||
isChangePatch = false;
|
isChangePatch = false;
|
||||||
});
|
});
|
||||||
clEditor.on('focus', () => store.commit('discussion/setNewCommentFocus', false));
|
clEditor.on('focus', () => store.commit('discussion/setNewCommentFocus', false));
|
||||||
|
|
||||||
// Track new discussions (not sure it's a good idea)
|
|
||||||
// store.watch(
|
|
||||||
// () => store.getters['content/current'].discussions,
|
|
||||||
// (discussions) => {
|
|
||||||
// const oldDiscussionIds = discussionIds;
|
|
||||||
// discussionIds = {};
|
|
||||||
// let hasNewDiscussion = false;
|
|
||||||
// Object.keys(discussions).forEach((discussionId) => {
|
|
||||||
// discussionIds[discussionId] = true;
|
|
||||||
// if (!oldDiscussionIds[discussionId]) {
|
|
||||||
// hasNewDiscussion = true;
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// if (hasNewDiscussion) {
|
|
||||||
// const content = store.getters['content/current'];
|
|
||||||
// currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
},
|
},
|
||||||
initClEditorInternal(opts) {
|
initClEditorInternal(opts) {
|
||||||
const content = store.getters['content/current'];
|
const content = store.getters['content/current'];
|
||||||
@ -241,9 +220,10 @@ export default {
|
|||||||
classGetter('editor', discussionId), offsetGetter(discussionId), { discussionId });
|
classGetter('editor', discussionId), offsetGetter(discussionId), { discussionId });
|
||||||
editorClassAppliers[discussionId] = classApplier;
|
editorClassAppliers[discussionId] = classApplier;
|
||||||
});
|
});
|
||||||
Object.keys(oldEditorClassAppliers).forEach((discussionId) => {
|
// Clean unused class appliers
|
||||||
|
Object.entries(oldEditorClassAppliers).forEach(([discussionId, classApplier]) => {
|
||||||
if (!editorClassAppliers[discussionId]) {
|
if (!editorClassAppliers[discussionId]) {
|
||||||
oldEditorClassAppliers[discussionId].stop();
|
classApplier.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -255,9 +235,10 @@ export default {
|
|||||||
classGetter('preview', discussionId), offsetGetter(discussionId), { discussionId });
|
classGetter('preview', discussionId), offsetGetter(discussionId), { discussionId });
|
||||||
previewClassAppliers[discussionId] = classApplier;
|
previewClassAppliers[discussionId] = classApplier;
|
||||||
});
|
});
|
||||||
Object.keys(oldPreviewClassAppliers).forEach((discussionId) => {
|
// Clean unused class appliers
|
||||||
|
Object.entries(oldPreviewClassAppliers).forEach(([discussionId, classApplier]) => {
|
||||||
if (!previewClassAppliers[discussionId]) {
|
if (!previewClassAppliers[discussionId]) {
|
||||||
oldPreviewClassAppliers[discussionId].stop();
|
classApplier.stop();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -6,36 +6,25 @@ import store from '../store';
|
|||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
|
||||||
* Get element and dimension that handles scrolling.
|
|
||||||
*/
|
|
||||||
getObjectToScroll() {
|
|
||||||
let elt = this.editorElt.parentNode;
|
|
||||||
let dimensionKey = 'editorDimension';
|
|
||||||
if (!store.getters['layout/styles'].showEditor) {
|
|
||||||
elt = this.previewElt.parentNode;
|
|
||||||
dimensionKey = 'previewDimension';
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
elt,
|
|
||||||
dimensionKey,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an object describing the position of the scroll bar in the file.
|
* Get an object describing the position of the scroll bar in the file.
|
||||||
*/
|
*/
|
||||||
getScrollPosition() {
|
getScrollPosition(elt = store.getters['layout/styles'].showEditor
|
||||||
const objToScroll = this.getObjectToScroll();
|
? this.editorElt
|
||||||
const scrollTop = objToScroll.elt.scrollTop;
|
: this.previewElt,
|
||||||
|
) {
|
||||||
|
const dimensionKey = elt === this.editorElt
|
||||||
|
? 'editorDimension'
|
||||||
|
: 'previewDimension';
|
||||||
|
const scrollTop = elt.parentNode.scrollTop;
|
||||||
let result;
|
let result;
|
||||||
if (this.sectionDescMeasuredList) {
|
if (this.sectionDescMeasuredList) {
|
||||||
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
this.sectionDescMeasuredList.some((sectionDesc, sectionIdx) => {
|
||||||
if (scrollTop >= sectionDesc[objToScroll.dimensionKey].endOffset) {
|
if (scrollTop >= sectionDesc[dimensionKey].endOffset) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const posInSection = (scrollTop - sectionDesc[objToScroll.dimensionKey].startOffset) /
|
const posInSection = (scrollTop - sectionDesc[dimensionKey].startOffset) /
|
||||||
(sectionDesc[objToScroll.dimensionKey].height || 1);
|
(sectionDesc[dimensionKey].height || 1);
|
||||||
result = {
|
result = {
|
||||||
sectionIdx,
|
sectionIdx,
|
||||||
posInSection,
|
posInSection,
|
||||||
|
@ -15,11 +15,11 @@ const deleteMarkerMaxAge = 1000;
|
|||||||
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
const checkSponsorshipAfter = (5 * 60 * 1000) + (30 * 1000); // tokenExpirationMargin + 30 sec
|
||||||
|
|
||||||
class Connection {
|
class Connection {
|
||||||
constructor() {
|
constructor(dbName) {
|
||||||
this.getTxCbs = [];
|
this.getTxCbs = [];
|
||||||
|
|
||||||
// Init connexion
|
// Init connection
|
||||||
const request = indexedDB.open('stackedit-db', dbVersion);
|
const request = indexedDB.open(dbName, dbVersion);
|
||||||
|
|
||||||
request.onerror = () => {
|
request.onerror = () => {
|
||||||
throw new Error("Can't connect to IndexedDB.");
|
throw new Error("Can't connect to IndexedDB.");
|
||||||
@ -39,7 +39,7 @@ class Connection {
|
|||||||
const oldVersion = event.oldVersion || 0;
|
const oldVersion = event.oldVersion || 0;
|
||||||
|
|
||||||
// We don't use 'break' in this switch statement,
|
// We don't use 'break' in this switch statement,
|
||||||
// the fall-through behaviour is what we want.
|
// the fall-through behavior is what we want.
|
||||||
/* eslint-disable no-fallthrough */
|
/* eslint-disable no-fallthrough */
|
||||||
switch (oldVersion) {
|
switch (oldVersion) {
|
||||||
case 0:
|
case 0:
|
||||||
@ -80,21 +80,174 @@ class Connection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashMap = {};
|
|
||||||
utils.types.forEach((type) => {
|
|
||||||
hashMap[type] = Object.create(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
content: true,
|
content: true,
|
||||||
contentState: true,
|
contentState: true,
|
||||||
syncedContent: true,
|
syncedContent: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hashMap = {};
|
||||||
|
utils.types.forEach((type) => {
|
||||||
|
hashMap[type] = Object.create(null);
|
||||||
|
});
|
||||||
|
const lsHashMap = Object.create(null);
|
||||||
|
|
||||||
const localDbSvc = {
|
const localDbSvc = {
|
||||||
lastTx: 0,
|
lastTx: 0,
|
||||||
hashMap,
|
hashMap,
|
||||||
connection: new Connection(),
|
connection: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the connection and start syncing.
|
||||||
|
*/
|
||||||
|
init() {
|
||||||
|
// Create the connection
|
||||||
|
this.connection = new Connection(store.getters['data/dbName']);
|
||||||
|
|
||||||
|
// Load the DB
|
||||||
|
return localDbSvc.sync()
|
||||||
|
.then(() => {
|
||||||
|
// If exportBackup parameter was provided
|
||||||
|
if (exportBackup) {
|
||||||
|
const backup = JSON.stringify(store.getters.allItemMap);
|
||||||
|
const blob = new Blob([backup], {
|
||||||
|
type: 'text/plain;charset=utf-8',
|
||||||
|
});
|
||||||
|
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save welcome file content hash if not done already
|
||||||
|
const hash = utils.hash(welcomeFile);
|
||||||
|
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
||||||
|
if (!welcomeFileHashes[hash]) {
|
||||||
|
store.dispatch('data/patchLocalSettings', {
|
||||||
|
welcomeFileHashes: {
|
||||||
|
...welcomeFileHashes,
|
||||||
|
[hash]: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
|
if (!store.getters['data/loginToken'] &&
|
||||||
|
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
||||||
|
) {
|
||||||
|
// Clean files
|
||||||
|
store.getters['file/items']
|
||||||
|
.filter(file => file.parentId === 'trash') // If file is in the trash
|
||||||
|
.forEach(file => store.dispatch('deleteFile', file.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enable sponsorship
|
||||||
|
if (utils.queryParams.paymentSuccess) {
|
||||||
|
location.hash = '';
|
||||||
|
store.dispatch('modal/paymentSuccess');
|
||||||
|
const loginToken = store.getters['data/loginToken'];
|
||||||
|
// Force check sponsorship after a few seconds
|
||||||
|
const currentDate = Date.now();
|
||||||
|
if (loginToken && loginToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
||||||
|
store.dispatch('data/setGoogleToken', {
|
||||||
|
...loginToken,
|
||||||
|
expiresOn: currentDate - checkSponsorshipAfter,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync local DB periodically
|
||||||
|
utils.setInterval(() => localDbSvc.sync(), 1000);
|
||||||
|
|
||||||
|
const ifNoId = cb => (obj) => {
|
||||||
|
if (obj.id) {
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
return cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
// watch current file changing
|
||||||
|
store.watch(
|
||||||
|
() => store.getters['file/current'].id,
|
||||||
|
() => Promise.resolve(store.getters['file/current'])
|
||||||
|
// If current file has no ID, get the most recent file
|
||||||
|
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||||
|
// If still no ID, create a new file
|
||||||
|
.then(ifNoId(() => store.dispatch('createFile', {
|
||||||
|
name: 'Welcome file',
|
||||||
|
text: welcomeFile,
|
||||||
|
})))
|
||||||
|
.then((currentFile) => {
|
||||||
|
// Fix current file ID
|
||||||
|
if (store.getters['file/current'].id !== currentFile.id) {
|
||||||
|
store.commit('file/setCurrentId', currentFile.id);
|
||||||
|
// Wait for the next watch tick
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve()
|
||||||
|
// Load contentState from DB
|
||||||
|
.then(() => localDbSvc.loadContentState(currentFile.id))
|
||||||
|
// Load syncedContent from DB
|
||||||
|
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
||||||
|
// Load content from DB
|
||||||
|
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
||||||
|
.then(
|
||||||
|
() => {
|
||||||
|
// Set last opened file
|
||||||
|
store.dispatch('data/setLastOpenedId', currentFile.id);
|
||||||
|
// Cancel new discussion
|
||||||
|
store.commit('discussion/setCurrentDiscussionId');
|
||||||
|
// Open the gutter if file contains discussions
|
||||||
|
store.commit('discussion/setCurrentDiscussionId',
|
||||||
|
store.getters['discussion/nextDiscussionId']);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
// Failure (content is not available), go back to previous file
|
||||||
|
const lastOpenedFile = store.getters['file/lastOpened'];
|
||||||
|
store.commit('file/setCurrentId', lastOpenedFile.id);
|
||||||
|
throw err;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(err); // eslint-disable-line no-console
|
||||||
|
store.dispatch('notification/error', err);
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync data items stored in the localStorage.
|
||||||
|
*/
|
||||||
|
syncLocalStorage() {
|
||||||
|
utils.localStorageDataIds.forEach((id) => {
|
||||||
|
const key = `data/${id}`;
|
||||||
|
|
||||||
|
// Skip reloading the layoutSettings
|
||||||
|
if (id !== 'layoutSettings' || !lsHashMap[id]) {
|
||||||
|
try {
|
||||||
|
// Try to parse the item from the localStorage
|
||||||
|
const storedItem = JSON.parse(localStorage.getItem(key));
|
||||||
|
if (storedItem.hash && lsHashMap[id] !== storedItem.hash) {
|
||||||
|
// Item has changed, replace it in the store
|
||||||
|
store.commit('data/setItem', storedItem);
|
||||||
|
lsHashMap[id] = storedItem.hash;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore parsing issue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write item if different from stored one
|
||||||
|
const item = store.state.data.lsItemMap[id];
|
||||||
|
if (item && item.hash !== lsHashMap[id]) {
|
||||||
|
localStorage.setItem(key, JSON.stringify(item));
|
||||||
|
lsHashMap[id] = item.hash;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a promise that will be resolved once the synchronization between the store and the
|
* Return a promise that will be resolved once the synchronization between the store and the
|
||||||
@ -103,9 +256,15 @@ const localDbSvc = {
|
|||||||
*/
|
*/
|
||||||
sync() {
|
sync() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
|
// Create the DB transaction
|
||||||
this.connection.createTx((tx) => {
|
this.connection.createTx((tx) => {
|
||||||
|
// Look for DB changes and apply them to the store
|
||||||
this.readAll(tx, (storeItemMap) => {
|
this.readAll(tx, (storeItemMap) => {
|
||||||
|
// Persist all the store changes into the DB
|
||||||
this.writeAll(storeItemMap, tx);
|
this.writeAll(storeItemMap, tx);
|
||||||
|
// Sync localStorage
|
||||||
|
this.syncLocalStorage();
|
||||||
|
// Done
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}, () => reject(new Error('Local DB access error.')));
|
}, () => reject(new Error('Local DB access error.')));
|
||||||
@ -186,8 +345,7 @@ const localDbSvc = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Put changes
|
// Put changes
|
||||||
Object.keys(storeItemMap).forEach((id) => {
|
Object.entries(storeItemMap).forEach(([, storeItem]) => {
|
||||||
const storeItem = storeItemMap[id];
|
|
||||||
// Store object has changed
|
// Store object has changed
|
||||||
if (this.hashMap[storeItem.type][storeItem.id] !== storeItem.hash) {
|
if (this.hashMap[storeItem.type][storeItem.id] !== storeItem.hash) {
|
||||||
const item = {
|
const item = {
|
||||||
@ -266,11 +424,11 @@ const localDbSvc = {
|
|||||||
return this.sync()
|
return this.sync()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Keep only last opened files in memory
|
// Keep only last opened files in memory
|
||||||
const lastOpenedFileIds = new Set(store.getters['data/lastOpenedIds']);
|
const lastOpenedFileIdSet = new Set(store.getters['data/lastOpenedIds']);
|
||||||
Object.keys(contentTypes).forEach((type) => {
|
Object.keys(contentTypes).forEach((type) => {
|
||||||
store.getters[`${type}/items`].forEach((item) => {
|
store.getters[`${type}/items`].forEach((item) => {
|
||||||
const [fileId] = item.id.split('/');
|
const [fileId] = item.id.split('/');
|
||||||
if (!lastOpenedFileIds.has(fileId)) {
|
if (!lastOpenedFileIdSet.has(fileId)) {
|
||||||
// Remove item from the store
|
// Remove item from the store
|
||||||
store.commit(`${type}/deleteItem`, item.id);
|
store.commit(`${type}/deleteItem`, item.id);
|
||||||
}
|
}
|
||||||
@ -303,119 +461,4 @@ const loader = type => fileId => localDbSvc.loadItem(`${fileId}/${type}`)
|
|||||||
localDbSvc.loadSyncedContent = loader('syncedContent');
|
localDbSvc.loadSyncedContent = loader('syncedContent');
|
||||||
localDbSvc.loadContentState = loader('contentState');
|
localDbSvc.loadContentState = loader('contentState');
|
||||||
|
|
||||||
const ifNoId = cb => (obj) => {
|
|
||||||
if (obj.id) {
|
|
||||||
return obj;
|
|
||||||
}
|
|
||||||
return cb();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load the DB on boot
|
|
||||||
localDbSvc.sync()
|
|
||||||
.then(() => {
|
|
||||||
if (exportBackup) {
|
|
||||||
const backup = JSON.stringify(store.getters.allItemMap);
|
|
||||||
const blob = new Blob([backup], {
|
|
||||||
type: 'text/plain;charset=utf-8',
|
|
||||||
});
|
|
||||||
FileSaver.saveAs(blob, 'StackEdit workspace.json');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the ready flag
|
|
||||||
store.commit('setReady');
|
|
||||||
|
|
||||||
// Save welcome file content hash if not done already
|
|
||||||
const hash = utils.hash(welcomeFile);
|
|
||||||
const welcomeFileHashes = store.getters['data/localSettings'].welcomeFileHashes;
|
|
||||||
if (!welcomeFileHashes[hash]) {
|
|
||||||
store.dispatch('data/patchLocalSettings', {
|
|
||||||
welcomeFileHashes: {
|
|
||||||
...welcomeFileHashes,
|
|
||||||
[hash]: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If app was last opened 7 days ago and synchronization is off
|
|
||||||
if (!store.getters['data/loginToken'] &&
|
|
||||||
(utils.lastOpened + utils.cleanTrashAfter < Date.now())
|
|
||||||
) {
|
|
||||||
// Clean files
|
|
||||||
store.getters['file/items']
|
|
||||||
.filter(file => file.parentId === 'trash') // If file is in the trash
|
|
||||||
.forEach(file => store.dispatch('deleteFile', file.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enable sponsorship
|
|
||||||
if (utils.queryParams.paymentSuccess) {
|
|
||||||
location.hash = '';
|
|
||||||
store.dispatch('modal/paymentSuccess');
|
|
||||||
const loginToken = store.getters['data/loginToken'];
|
|
||||||
// Force check sponsorship after a few seconds
|
|
||||||
const currentDate = Date.now();
|
|
||||||
if (loginToken && loginToken.expiresOn > currentDate - checkSponsorshipAfter) {
|
|
||||||
store.dispatch('data/setGoogleToken', {
|
|
||||||
...loginToken,
|
|
||||||
expiresOn: currentDate - checkSponsorshipAfter,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// watch file changing
|
|
||||||
store.watch(
|
|
||||||
() => store.getters['file/current'].id,
|
|
||||||
() => Promise.resolve(store.getters['file/current'])
|
|
||||||
// If current file has no ID, get the most recent file
|
|
||||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
|
||||||
// If still no ID, create a new file
|
|
||||||
.then(ifNoId(() => store.dispatch('createFile', {
|
|
||||||
name: 'Welcome file',
|
|
||||||
text: welcomeFile,
|
|
||||||
})))
|
|
||||||
.then((currentFile) => {
|
|
||||||
// Fix current file ID
|
|
||||||
if (store.getters['file/current'].id !== currentFile.id) {
|
|
||||||
store.commit('file/setCurrentId', currentFile.id);
|
|
||||||
// Wait for the next watch tick
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve()
|
|
||||||
// Load contentState from DB
|
|
||||||
.then(() => localDbSvc.loadContentState(currentFile.id))
|
|
||||||
// Load syncedContent from DB
|
|
||||||
.then(() => localDbSvc.loadSyncedContent(currentFile.id))
|
|
||||||
// Load content from DB
|
|
||||||
.then(() => localDbSvc.loadItem(`${currentFile.id}/content`))
|
|
||||||
.then(
|
|
||||||
() => {
|
|
||||||
// Set last opened file
|
|
||||||
store.dispatch('data/setLastOpenedId', currentFile.id);
|
|
||||||
// Cancel new discussion
|
|
||||||
store.commit('discussion/setCurrentDiscussionId');
|
|
||||||
// Open the gutter if file contains discussions
|
|
||||||
store.commit('discussion/setCurrentDiscussionId',
|
|
||||||
store.getters['discussion/nextDiscussionId']);
|
|
||||||
},
|
|
||||||
(err) => {
|
|
||||||
// Failure (content is not available), go back to previous file
|
|
||||||
const lastOpenedFile = store.getters['file/lastOpened'];
|
|
||||||
store.commit('file/setCurrentId', lastOpenedFile.id);
|
|
||||||
throw err;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error(err); // eslint-disable-line no-console
|
|
||||||
store.dispatch('notification/error', err);
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
immediate: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Sync local DB periodically
|
|
||||||
utils.setInterval(() => localDbSvc.sync(), 1000);
|
|
||||||
|
|
||||||
export default localDbSvc;
|
export default localDbSvc;
|
||||||
|
@ -20,15 +20,13 @@ const languageAliases = ({
|
|||||||
ps1: 'powershell',
|
ps1: 'powershell',
|
||||||
psm1: 'powershell',
|
psm1: 'powershell',
|
||||||
});
|
});
|
||||||
Object.keys(languageAliases).forEach((alias) => {
|
Object.entries(languageAliases).forEach(([alias, language]) => {
|
||||||
const language = languageAliases[alias];
|
|
||||||
Prism.languages[alias] = Prism.languages[language];
|
Prism.languages[alias] = Prism.languages[language];
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add programming language parsing capability to markdown fences
|
// Add programming language parsing capability to markdown fences
|
||||||
const insideFences = {};
|
const insideFences = {};
|
||||||
Object.keys(Prism.languages).forEach((name) => {
|
Object.entries(Prism.languages).forEach(([name, language]) => {
|
||||||
const language = Prism.languages[name];
|
|
||||||
if (Prism.util.type(language) === 'Object') {
|
if (Prism.util.type(language) === 'Object') {
|
||||||
insideFences[`language-${name}`] = {
|
insideFences[`language-${name}`] = {
|
||||||
pattern: new RegExp(`(\`\`\`|~~~)${name}\\W[\\s\\S]*`),
|
pattern: new RegExp(`(\`\`\`|~~~)${name}\\W[\\s\\S]*`),
|
||||||
|
@ -190,8 +190,7 @@ export default {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(defs).forEach((name) => {
|
Object.entries(defs).forEach(([name, def]) => {
|
||||||
const def = defs[name];
|
|
||||||
grammars.main[name] = def;
|
grammars.main[name] = def;
|
||||||
grammars.list[name] = def;
|
grammars.list[name] = def;
|
||||||
grammars.blockquote[name] = def;
|
grammars.blockquote[name] = def;
|
||||||
@ -396,8 +395,7 @@ export default {
|
|||||||
rest.linkref.inside['cl cl-underlined-text'].inside = inside;
|
rest.linkref.inside['cl cl-underlined-text'].inside = inside;
|
||||||
|
|
||||||
// Wrap any other characters to allow paragraph folding
|
// Wrap any other characters to allow paragraph folding
|
||||||
Object.keys(grammars).forEach((key) => {
|
Object.entries(grammars).forEach(([, grammar]) => {
|
||||||
const grammar = grammars[key];
|
|
||||||
grammar.rest = grammar.rest || {};
|
grammar.rest = grammar.rest || {};
|
||||||
grammar.rest.p = /.+/;
|
grammar.rest.p = /.+/;
|
||||||
});
|
});
|
||||||
|
@ -193,12 +193,8 @@ export default {
|
|||||||
|
|
||||||
const url = utils.addQueryParams(config.url, config.params);
|
const url = utils.addQueryParams(config.url, config.params);
|
||||||
xhr.open(config.method || 'GET', url);
|
xhr.open(config.method || 'GET', url);
|
||||||
Object.keys(config.headers).forEach((key) => {
|
Object.entries(config.headers).forEach(([key, value]) =>
|
||||||
const value = config.headers[key];
|
value && xhr.setRequestHeader(key, `${value}`));
|
||||||
if (value) {
|
|
||||||
xhr.setRequestHeader(key, `${value}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (config.blob) {
|
if (config.blob) {
|
||||||
xhr.responseType = 'blob';
|
xhr.responseType = 'blob';
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ function throttle(func, wait) {
|
|||||||
const doScrollSync = () => {
|
const doScrollSync = () => {
|
||||||
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
const localSkipAnimation = skipAnimation || !store.getters['layout/styles'].showSidePreview;
|
||||||
skipAnimation = false;
|
skipAnimation = false;
|
||||||
if (!store.getters['data/localSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
if (!store.getters['data/layoutSettings'].scrollSync || !sectionDescList || sectionDescList.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let editorScrollTop = editorScrollerElt.scrollTop;
|
let editorScrollTop = editorScrollerElt.scrollTop;
|
||||||
@ -116,7 +116,7 @@ const forceScrollSync = () => {
|
|||||||
doScrollSync();
|
doScrollSync();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
store.watch(() => store.getters['data/localSettings'].scrollSync, forceScrollSync);
|
store.watch(() => store.getters['data/layoutSettings'].scrollSync, forceScrollSync);
|
||||||
|
|
||||||
editorSvc.$on('inited', () => {
|
editorSvc.$on('inited', () => {
|
||||||
editorScrollerElt = editorSvc.editorElt.parentNode;
|
editorScrollerElt = editorSvc.editorElt.parentNode;
|
||||||
|
@ -67,8 +67,7 @@ store.watch(
|
|||||||
Mousetrap.reset();
|
Mousetrap.reset();
|
||||||
|
|
||||||
const shortcuts = computedSettings.shortcuts;
|
const shortcuts = computedSettings.shortcuts;
|
||||||
Object.keys(shortcuts).forEach((key) => {
|
Object.entries(shortcuts).forEach(([key, shortcut]) => {
|
||||||
const shortcut = shortcuts[key];
|
|
||||||
if (shortcut) {
|
if (shortcut) {
|
||||||
const method = `${shortcut.method || shortcut}`;
|
const method = `${shortcut.method || shortcut}`;
|
||||||
let params = shortcut.params || [];
|
let params = shortcut.params || [];
|
||||||
|
@ -7,6 +7,10 @@ export default providerRegistry.register({
|
|||||||
getToken() {
|
getToken() {
|
||||||
return store.getters['data/loginToken'];
|
return store.getters['data/loginToken'];
|
||||||
},
|
},
|
||||||
|
initWorkspace() {
|
||||||
|
// Nothing to do since the main workspace isn't necessarily synchronized
|
||||||
|
return Promise.resolve();
|
||||||
|
},
|
||||||
getChanges(token) {
|
getChanges(token) {
|
||||||
return googleHelper.getChanges(token)
|
return googleHelper.getChanges(token)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
@ -44,8 +48,10 @@ export default providerRegistry.register({
|
|||||||
saveItem(token, item, syncData, ifNotTooLate) {
|
saveItem(token, item, syncData, ifNotTooLate) {
|
||||||
return googleHelper.uploadAppDataFile(
|
return googleHelper.uploadAppDataFile(
|
||||||
token,
|
token,
|
||||||
JSON.stringify(item), ['appDataFolder'],
|
JSON.stringify(item),
|
||||||
null,
|
['appDataFolder'],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
syncData && syncData.id,
|
syncData && syncData.id,
|
||||||
ifNotTooLate,
|
ifNotTooLate,
|
||||||
)
|
)
|
||||||
@ -100,6 +106,7 @@ export default providerRegistry.register({
|
|||||||
hash: item.hash,
|
hash: item.hash,
|
||||||
}),
|
}),
|
||||||
['appDataFolder'],
|
['appDataFolder'],
|
||||||
|
undefined,
|
||||||
JSON.stringify(item),
|
JSON.stringify(item),
|
||||||
syncData && syncData.id,
|
syncData && syncData.id,
|
||||||
ifNotTooLate,
|
ifNotTooLate,
|
||||||
@ -116,6 +123,9 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
listRevisions(token, fileId) {
|
listRevisions(token, fileId) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
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)
|
return googleHelper.getFileRevisions(token, syncData.id)
|
||||||
.then(revisions => revisions.map(revision => ({
|
.then(revisions => revisions.map(revision => ({
|
||||||
id: revision.id,
|
id: revision.id,
|
||||||
@ -125,6 +135,9 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
getRevisionContent(token, fileId, revisionId) {
|
getRevisionContent(token, fileId, revisionId) {
|
||||||
const syncData = store.getters['data/syncDataByItemId'][`${fileId}/content`];
|
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)
|
return googleHelper.downloadFileRevision(token, syncData.id, revisionId)
|
||||||
.then(content => JSON.parse(content));
|
.then(content => JSON.parse(content));
|
||||||
},
|
},
|
||||||
|
@ -32,6 +32,7 @@ export default providerRegistry.register({
|
|||||||
token,
|
token,
|
||||||
name,
|
name,
|
||||||
parents,
|
parents,
|
||||||
|
undefined,
|
||||||
providerUtils.serializeContent(content),
|
providerUtils.serializeContent(content),
|
||||||
undefined,
|
undefined,
|
||||||
syncLocation.driveFileId,
|
syncLocation.driveFileId,
|
||||||
@ -47,6 +48,7 @@ export default providerRegistry.register({
|
|||||||
token,
|
token,
|
||||||
metadata.title,
|
metadata.title,
|
||||||
[],
|
[],
|
||||||
|
undefined,
|
||||||
html,
|
html,
|
||||||
publishLocation.templateId ? 'text/html' : undefined,
|
publishLocation.templateId ? 'text/html' : undefined,
|
||||||
publishLocation.driveFileId,
|
publishLocation.driveFileId,
|
||||||
|
256
src/services/providers/googleDriveWorkspaceProvider.js
Normal file
256
src/services/providers/googleDriveWorkspaceProvider.js
Normal file
@ -0,0 +1,256 @@
|
|||||||
|
import store from '../../store';
|
||||||
|
import googleHelper from './helpers/googleHelper';
|
||||||
|
import providerRegistry from './providerRegistry';
|
||||||
|
import utils from '../utils';
|
||||||
|
|
||||||
|
let workspaceFolderId;
|
||||||
|
|
||||||
|
const makeWorkspaceId = () => {
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
export default providerRegistry.register({
|
||||||
|
id: 'googleDriveWorkspace',
|
||||||
|
getToken() {
|
||||||
|
return store.getters['data/loginToken'];
|
||||||
|
},
|
||||||
|
initWorkspace() {
|
||||||
|
const initFolder = (token, folder) => Promise.resolve({
|
||||||
|
workspaceId: this.makeWorkspaceId(folder.id),
|
||||||
|
dataFolderId: folder.appProperties.dataFolderId,
|
||||||
|
trashFolderId: folder.appProperties.trashFolderId,
|
||||||
|
})
|
||||||
|
.then((properties) => {
|
||||||
|
// Make sure data folder exists
|
||||||
|
if (properties.dataFolderId) {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
return googleHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
'.stackedit-data',
|
||||||
|
[folder.id],
|
||||||
|
{ workspaceId: properties.workspaceId },
|
||||||
|
undefined,
|
||||||
|
'application/vnd.google-apps.folder',
|
||||||
|
)
|
||||||
|
.then(dataFolder => ({
|
||||||
|
...properties,
|
||||||
|
dataFolderId: dataFolder.id,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.then((properties) => {
|
||||||
|
// Make sure trash folder exists
|
||||||
|
if (properties.trashFolderId) {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
return googleHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
'.stackedit-trash',
|
||||||
|
[folder.id],
|
||||||
|
{ workspaceId: properties.workspaceId },
|
||||||
|
undefined,
|
||||||
|
'application/vnd.google-apps.folder',
|
||||||
|
)
|
||||||
|
.then(trashFolder => ({
|
||||||
|
...properties,
|
||||||
|
trashFolderId: trashFolder.id,
|
||||||
|
}));
|
||||||
|
})
|
||||||
|
.then((properties) => {
|
||||||
|
// Update workspace if some properties are missing
|
||||||
|
if (properties.workspaceId === folder.appProperties.workspaceId
|
||||||
|
&& properties.dataFolderId === folder.appProperties.dataFolderId
|
||||||
|
&& properties.trashFolderId === folder.appProperties.trashFolderId
|
||||||
|
) {
|
||||||
|
return properties;
|
||||||
|
}
|
||||||
|
return googleHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
properties,
|
||||||
|
undefined,
|
||||||
|
'application/vnd.google-apps.folder',
|
||||||
|
folder.id,
|
||||||
|
)
|
||||||
|
.then(() => properties);
|
||||||
|
})
|
||||||
|
.then((properties) => {
|
||||||
|
// Update workspace in the store
|
||||||
|
store.dispatch('data/patchWorkspaces', {
|
||||||
|
[properties.workspaceId]: {
|
||||||
|
id: properties.workspaceId,
|
||||||
|
sub: token.sub,
|
||||||
|
name: folder.name,
|
||||||
|
providerId: this.id,
|
||||||
|
folderId: folder.id,
|
||||||
|
dataFolderId: properties.dataFolderId,
|
||||||
|
trashFolderId: properties.trashFolderId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return store.getters['data/workspaces'][properties.workspaceId];
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.resolve(store.getters['data/googleTokens'][utils.queryParams.sub])
|
||||||
|
.then(token => token || this.$store.dispatch('modal/workspaceGoogleRedirection', {
|
||||||
|
onResolve: () => googleHelper.addDriveAccount(),
|
||||||
|
}))
|
||||||
|
.then(token => Promise.resolve()
|
||||||
|
.then(() => utils.queryParams.folderId || googleHelper.uploadFile(
|
||||||
|
token,
|
||||||
|
'StackEdit workspace',
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
'application/vnd.google-apps.folder',
|
||||||
|
).then(folder => initFolder(token, folder).then(() => folder.id)))
|
||||||
|
.then((folderId) => {
|
||||||
|
const workspaceId = this.makeWorkspaceId(folderId);
|
||||||
|
const workspace = store.getters['data/workspaces'][workspaceId];
|
||||||
|
return workspace || googleHelper.getFile(token, folderId)
|
||||||
|
.then((folder) => {
|
||||||
|
const folderWorkspaceId = folder.appProperties.workspaceId;
|
||||||
|
if (folderWorkspaceId && folderWorkspaceId !== workspaceId) {
|
||||||
|
throw new Error(`Google Drive folder ${folderId} is part of another workspace.`);
|
||||||
|
}
|
||||||
|
return initFolder(token, folder);
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
getChanges(token) {
|
||||||
|
return googleHelper.getChanges(token)
|
||||||
|
.then((result) => {
|
||||||
|
const changes = result.changes.filter((change) => {
|
||||||
|
if (change.file) {
|
||||||
|
try {
|
||||||
|
change.item = JSON.parse(change.file.name);
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Build sync data
|
||||||
|
change.syncData = {
|
||||||
|
id: change.fileId,
|
||||||
|
itemId: change.item.id,
|
||||||
|
type: change.item.type,
|
||||||
|
hash: change.item.hash,
|
||||||
|
};
|
||||||
|
change.file = undefined;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
changes.nextPageToken = result.nextPageToken;
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
});
|
@ -53,7 +53,16 @@ export default {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadFileInternal(refreshedToken, name, parents, media = null, mediaType = 'text/plain', fileId = null, ifNotTooLate = cb => res => cb(res)) {
|
uploadFileInternal(
|
||||||
|
refreshedToken,
|
||||||
|
name,
|
||||||
|
parents,
|
||||||
|
appProperties,
|
||||||
|
media = null,
|
||||||
|
mediaType = null,
|
||||||
|
fileId = null,
|
||||||
|
ifNotTooLate = cb => res => cb(res),
|
||||||
|
) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
// Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
|
// Refreshing a token can take a while if an oauth window pops up, make sure it's not too late
|
||||||
.then(ifNotTooLate(() => {
|
.then(ifNotTooLate(() => {
|
||||||
@ -61,7 +70,7 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://www.googleapis.com/drive/v3/files',
|
url: 'https://www.googleapis.com/drive/v3/files',
|
||||||
};
|
};
|
||||||
const metadata = { name };
|
const metadata = { name, appProperties };
|
||||||
if (fileId) {
|
if (fileId) {
|
||||||
options.method = 'PATCH';
|
options.method = 'PATCH';
|
||||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||||
@ -78,7 +87,7 @@ export default {
|
|||||||
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
multipartRequestBody += 'Content-Type: application/json; charset=UTF-8\r\n\r\n';
|
||||||
multipartRequestBody += JSON.stringify(metadata);
|
multipartRequestBody += JSON.stringify(metadata);
|
||||||
multipartRequestBody += delimiter;
|
multipartRequestBody += delimiter;
|
||||||
multipartRequestBody += `Content-Type: ${mediaType}; charset=UTF-8\r\n\r\n`;
|
multipartRequestBody += `Content-Type: ${mediaType || 'text/plain'}; charset=UTF-8\r\n\r\n`;
|
||||||
multipartRequestBody += media;
|
multipartRequestBody += media;
|
||||||
multipartRequestBody += closeDelimiter;
|
multipartRequestBody += closeDelimiter;
|
||||||
options.url = options.url.replace(
|
options.url = options.url.replace(
|
||||||
@ -95,6 +104,9 @@ export default {
|
|||||||
body: multipartRequestBody,
|
body: multipartRequestBody,
|
||||||
}).then(res => res.body);
|
}).then(res => res.body);
|
||||||
}
|
}
|
||||||
|
if (mediaType) {
|
||||||
|
metadata.mimeType = mediaType;
|
||||||
|
}
|
||||||
return this.request(refreshedToken, {
|
return this.request(refreshedToken, {
|
||||||
...options,
|
...options,
|
||||||
body: metadata,
|
body: metadata,
|
||||||
@ -170,7 +182,7 @@ export default {
|
|||||||
.then(token => this.getUser(token.sub)
|
.then(token => this.getUser(token.sub)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.status === 404) {
|
if (err.status === 404) {
|
||||||
store.dispatch('notification/info', 'Please activate Google Plus to change your account name!');
|
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo!');
|
||||||
} else {
|
} else {
|
||||||
throw err;
|
throw err;
|
||||||
}
|
}
|
||||||
@ -311,15 +323,23 @@ export default {
|
|||||||
return getPage(refreshedToken.nextPageToken);
|
return getPage(refreshedToken.nextPageToken);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadFile(token, name, parents, media, mediaType, fileId, ifNotTooLate) {
|
uploadFile(token, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(token, getDriveScopes(token))
|
return this.refreshToken(token, getDriveScopes(token))
|
||||||
.then(refreshedToken => this.uploadFileInternal(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, mediaType, fileId, ifNotTooLate));
|
refreshedToken, name, parents, appProperties, media, mediaType, fileId, ifNotTooLate));
|
||||||
},
|
},
|
||||||
uploadAppDataFile(token, name, parents, media, fileId, ifNotTooLate) {
|
uploadAppDataFile(token, name, parents, appProperties, media, fileId, ifNotTooLate) {
|
||||||
return this.refreshToken(token, driveAppDataScopes)
|
return this.refreshToken(token, driveAppDataScopes)
|
||||||
.then(refreshedToken => this.uploadFileInternal(
|
.then(refreshedToken => this.uploadFileInternal(
|
||||||
refreshedToken, name, parents, media, undefined, fileId, ifNotTooLate));
|
refreshedToken, name, parents, appProperties, media, undefined, fileId, ifNotTooLate));
|
||||||
|
},
|
||||||
|
getFile(token, id) {
|
||||||
|
return this.refreshToken(token, getDriveScopes(token))
|
||||||
|
.then(refreshedToken => this.request(refreshedToken, {
|
||||||
|
method: 'GET',
|
||||||
|
url: `https://www.googleapis.com/drive/v3/files/${id}`,
|
||||||
|
})
|
||||||
|
.then(res => res.body));
|
||||||
},
|
},
|
||||||
downloadFile(token, id) {
|
downloadFile(token, id) {
|
||||||
return this.refreshToken(token, getDriveScopes(token))
|
return this.refreshToken(token, getDriveScopes(token))
|
||||||
|
@ -62,8 +62,8 @@ export default {
|
|||||||
*/
|
*/
|
||||||
openFileWithLocation(allLocations, criteria) {
|
openFileWithLocation(allLocations, criteria) {
|
||||||
return allLocations.some((location) => {
|
return allLocations.some((location) => {
|
||||||
// If every field fits the criteria
|
// If every field fits the criteria
|
||||||
if (Object.keys(criteria).every(key => criteria[key] === location[key])) {
|
if (Object.entries(criteria).every(([key, value]) => value === location[key])) {
|
||||||
// Found one location that fits, open it if it exists
|
// Found one location that fits, open it if it exists
|
||||||
const file = store.state.file.itemMap[location.fileId];
|
const file = store.state.file.itemMap[location.fileId];
|
||||||
if (file) {
|
if (file) {
|
||||||
|
@ -3,38 +3,73 @@ import store from '../store';
|
|||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import diffUtils from './diffUtils';
|
import diffUtils from './diffUtils';
|
||||||
import providerRegistry from './providers/providerRegistry';
|
import providerRegistry from './providers/providerRegistry';
|
||||||
import mainProvider from './providers/googleDriveAppDataProvider';
|
import googleDriveAppDataProvider from './providers/googleDriveAppDataProvider';
|
||||||
|
|
||||||
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
|
||||||
let lastSyncActivity;
|
|
||||||
const getLastStoredSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
|
||||||
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 autoSyncAfter = utils.randomize(60 * 1000); // 60 sec
|
||||||
|
|
||||||
const isDataSyncPossible = () => !!store.getters['data/loginToken'];
|
let workspaceProvider;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use a lock in the local storage to prevent multiple windows concurrency.
|
||||||
|
*/
|
||||||
|
const lastSyncActivityKey = `${utils.workspaceId}/lastSyncActivity`;
|
||||||
|
let lastSyncActivity;
|
||||||
|
const getLastStoredSyncActivity = () => parseInt(localStorage[lastSyncActivityKey], 10) || 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if workspace sync is possible.
|
||||||
|
*/
|
||||||
|
const isWorkspaceSyncPossible = () => {
|
||||||
|
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.
|
||||||
|
*/
|
||||||
const hasCurrentFileSyncLocations = () => !!store.getters['syncLocation/current'].length;
|
const hasCurrentFileSyncLocations = () => !!store.getters['syncLocation/current'].length;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if we are online and we have something to sync.
|
||||||
|
*/
|
||||||
const isSyncPossible = () => !store.state.offline &&
|
const isSyncPossible = () => !store.state.offline &&
|
||||||
(isDataSyncPossible() || hasCurrentFileSyncLocations());
|
(isWorkspaceSyncPossible() || hasCurrentFileSyncLocations());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if we are the many window, ie we have the lastSyncActivity lock.
|
||||||
|
*/
|
||||||
function isSyncWindow() {
|
function isSyncWindow() {
|
||||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||||
return lastSyncActivity === storedLastSyncActivity ||
|
return lastSyncActivity === storedLastSyncActivity ||
|
||||||
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
Date.now() > inactivityThreshold + storedLastSyncActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if auto sync can start, ie that lastSyncActivity is old enough.
|
||||||
|
*/
|
||||||
function isAutoSyncReady() {
|
function isAutoSyncReady() {
|
||||||
const storedLastSyncActivity = getLastStoredSyncActivity();
|
const storedLastSyncActivity = getLastStoredSyncActivity();
|
||||||
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
return Date.now() > autoSyncAfter + storedLastSyncActivity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the lastSyncActivity, assuming we have the lock.
|
||||||
|
*/
|
||||||
function setLastSyncActivity() {
|
function setLastSyncActivity() {
|
||||||
const currentDate = Date.now();
|
const currentDate = Date.now();
|
||||||
lastSyncActivity = currentDate;
|
lastSyncActivity = currentDate;
|
||||||
localStorage[lastSyncActivityKey] = currentDate;
|
localStorage[lastSyncActivityKey] = currentDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean a syncedContent.
|
||||||
|
*/
|
||||||
function cleanSyncedContent(syncedContent) {
|
function cleanSyncedContent(syncedContent) {
|
||||||
// Clean syncHistory from removed syncLocations
|
// Clean syncHistory from removed syncLocations
|
||||||
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
|
Object.keys(syncedContent.syncHistory).forEach((syncLocationId) => {
|
||||||
@ -42,17 +77,21 @@ function cleanSyncedContent(syncedContent) {
|
|||||||
delete syncedContent.syncHistory[syncLocationId];
|
delete syncedContent.syncHistory[syncLocationId];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const allSyncLocationHashes = new Set([].concat(
|
const allSyncLocationHashSet = new Set([].concat(
|
||||||
...Object.keys(syncedContent.syncHistory).map(
|
...Object.keys(syncedContent.syncHistory).map(
|
||||||
id => syncedContent.syncHistory[id])));
|
id => syncedContent.syncHistory[id])));
|
||||||
// Clean historyData from unused contents
|
// Clean historyData from unused contents
|
||||||
Object.keys(syncedContent.historyData).map(hash => parseInt(hash, 10)).forEach((hash) => {
|
Object.keys(syncedContent.historyData).map(hash => parseInt(hash, 10)).forEach((hash) => {
|
||||||
if (!allSyncLocationHashes.has(hash)) {
|
if (!allSyncLocationHashSet.has(hash)) {
|
||||||
delete syncedContent.historyData[hash];
|
delete syncedContent.historyData[hash];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 };
|
||||||
const syncData = { ...store.getters['data/syncData'] };
|
const syncData = { ...store.getters['data/syncData'] };
|
||||||
@ -92,6 +131,9 @@ function applyChanges(changes) {
|
|||||||
const LAST_SENT = 0;
|
const LAST_SENT = 0;
|
||||||
const LAST_MERGED = 1;
|
const LAST_MERGED = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a sync location by uploading the current file content.
|
||||||
|
*/
|
||||||
function createSyncLocation(syncLocation) {
|
function createSyncLocation(syncLocation) {
|
||||||
syncLocation.id = utils.uid();
|
syncLocation.id = utils.uid();
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
@ -137,6 +179,9 @@ class FileSyncContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync one file with all its locations.
|
||||||
|
*/
|
||||||
function syncFile(fileId, syncContext = new SyncContext()) {
|
function syncFile(fileId, syncContext = new SyncContext()) {
|
||||||
const fileSyncContext = new FileSyncContext();
|
const fileSyncContext = new FileSyncContext();
|
||||||
syncContext.synced[`${fileId}/content`] = true;
|
syncContext.synced[`${fileId}/content`] = true;
|
||||||
@ -174,7 +219,7 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
const syncLocations = [
|
const syncLocations = [
|
||||||
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
...store.getters['syncLocation/groupedByFileId'][fileId] || [],
|
||||||
];
|
];
|
||||||
if (isDataSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
syncLocations.unshift({ id: 'main', providerId: mainProvider.id, fileId });
|
||||||
}
|
}
|
||||||
let result;
|
let result;
|
||||||
@ -349,7 +394,9 @@ function syncFile(fileId, syncContext = new SyncContext()) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a data item, typically settings and templates.
|
||||||
|
*/
|
||||||
function syncDataItem(dataId) {
|
function syncDataItem(dataId) {
|
||||||
const item = store.state.data.itemMap[dataId];
|
const item = store.state.data.itemMap[dataId];
|
||||||
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
const syncData = store.getters['data/syncDataByItemId'][dataId];
|
||||||
@ -418,7 +465,10 @@ function syncDataItem(dataId) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function sync() {
|
/**
|
||||||
|
* Sync the whole workspace with the main provider and the current file explicit locations.
|
||||||
|
*/
|
||||||
|
function syncWorkspace() {
|
||||||
const syncContext = new SyncContext();
|
const syncContext = new SyncContext();
|
||||||
const mainToken = store.getters['data/loginToken'];
|
const mainToken = store.getters['data/loginToken'];
|
||||||
return mainProvider.getChanges(mainToken)
|
return mainProvider.getChanges(mainToken)
|
||||||
@ -447,8 +497,7 @@ function sync() {
|
|||||||
};
|
};
|
||||||
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
const syncDataByItemId = store.getters['data/syncDataByItemId'];
|
||||||
let result;
|
let result;
|
||||||
Object.keys(storeItemMap).some((id) => {
|
Object.entries(storeItemMap).some(([id, item]) => {
|
||||||
const item = storeItemMap[id];
|
|
||||||
const existingSyncData = syncDataByItemId[id];
|
const existingSyncData = syncDataByItemId[id];
|
||||||
if ((!existingSyncData || existingSyncData.hash !== item.hash) &&
|
if ((!existingSyncData || existingSyncData.hash !== item.hash) &&
|
||||||
// Add file if content has been uploaded
|
// Add file if content has been uploaded
|
||||||
@ -483,8 +532,7 @@ function sync() {
|
|||||||
};
|
};
|
||||||
const syncData = store.getters['data/syncData'];
|
const syncData = store.getters['data/syncData'];
|
||||||
let result;
|
let result;
|
||||||
Object.keys(syncData).some((id) => {
|
Object.entries(syncData).some(([, existingSyncData]) => {
|
||||||
const existingSyncData = syncData[id];
|
|
||||||
if (!storeItemMap[existingSyncData.itemId] &&
|
if (!storeItemMap[existingSyncData.itemId] &&
|
||||||
// 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]])
|
||||||
@ -556,20 +604,23 @@ function sync() {
|
|||||||
() => {
|
() => {
|
||||||
if (syncContext.restart) {
|
if (syncContext.restart) {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return sync();
|
return syncWorkspace();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
(err) => {
|
(err) => {
|
||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
return sync();
|
return syncWorkspace();
|
||||||
}
|
}
|
||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enqueue a sync task, if possible.
|
||||||
|
*/
|
||||||
function requestSync() {
|
function requestSync() {
|
||||||
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
store.dispatch('queue/enqueueSyncRequest', () => new Promise((resolve, reject) => {
|
||||||
let intervalId;
|
let intervalId;
|
||||||
@ -607,8 +658,8 @@ function requestSync() {
|
|||||||
|
|
||||||
Promise.resolve()
|
Promise.resolve()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (isDataSyncPossible()) {
|
if (isWorkspaceSyncPossible()) {
|
||||||
return sync();
|
return syncWorkspace();
|
||||||
}
|
}
|
||||||
if (hasCurrentFileSyncLocations()) {
|
if (hasCurrentFileSyncLocations()) {
|
||||||
// Only sync current file if data sync is unavailable.
|
// Only sync current file if data sync is unavailable.
|
||||||
@ -620,9 +671,9 @@ function requestSync() {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
// Clean files
|
// Clean files
|
||||||
Object.keys(fileHashesToClean).forEach((fileId) => {
|
Object.entries(fileHashesToClean).forEach(([fileId, fileHash]) => {
|
||||||
const file = store.state.file.itemMap[fileId];
|
const file = store.state.file.itemMap[fileId];
|
||||||
if (file && file.hash === fileHashesToClean[fileId]) {
|
if (file && file.hash === fileHash) {
|
||||||
store.dispatch('deleteFile', fileId);
|
store.dispatch('deleteFile', fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -635,26 +686,41 @@ function requestSync() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync periodically
|
|
||||||
utils.setInterval(() => {
|
|
||||||
if (isSyncPossible() &&
|
|
||||||
utils.isUserActive() &&
|
|
||||||
isSyncWindow() &&
|
|
||||||
isAutoSyncReady()
|
|
||||||
) {
|
|
||||||
requestSync();
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
// Unload contents from memory periodically
|
|
||||||
utils.setInterval(() => {
|
|
||||||
// Wait for sync and publish to finish
|
|
||||||
if (store.state.queue.isEmpty) {
|
|
||||||
localDbSvc.unloadContents();
|
|
||||||
}
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
init() {
|
||||||
|
// Load workspaces and tokens from localStorage
|
||||||
|
localDbSvc.syncLocalStorage();
|
||||||
|
|
||||||
|
// Try to find a suitable workspace provider
|
||||||
|
workspaceProvider = providerRegistry.providers[utils.queryParams.providerId];
|
||||||
|
if (!workspaceProvider || !workspaceProvider.initWorkspace) {
|
||||||
|
workspaceProvider = googleDriveAppDataProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
return workspaceProvider.initWorkspace()
|
||||||
|
.then(workspace => store.commit('workspace/setCurrentWorkspaceId', workspace.id))
|
||||||
|
.then(() => localDbSvc.init())
|
||||||
|
.then(() => {
|
||||||
|
// Sync periodically
|
||||||
|
utils.setInterval(() => {
|
||||||
|
if (isSyncPossible() &&
|
||||||
|
utils.isUserActive() &&
|
||||||
|
isSyncWindow() &&
|
||||||
|
isAutoSyncReady()
|
||||||
|
) {
|
||||||
|
requestSync();
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Unload contents from memory periodically
|
||||||
|
utils.setInterval(() => {
|
||||||
|
// Wait for sync and publish to finish
|
||||||
|
if (store.state.queue.isEmpty) {
|
||||||
|
localDbSvc.unloadContents();
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
},
|
||||||
isSyncPossible,
|
isSyncPossible,
|
||||||
requestSync,
|
requestSync,
|
||||||
createSyncLocation,
|
createSyncLocation,
|
||||||
|
@ -31,7 +31,9 @@ const setLastFocus = () => {
|
|||||||
localStorage[lastFocusKey] = lastFocus;
|
localStorage[lastFocusKey] = lastFocus;
|
||||||
setLastActivity();
|
setLastActivity();
|
||||||
};
|
};
|
||||||
setLastFocus();
|
if (document.hasFocus()) {
|
||||||
|
setLastFocus();
|
||||||
|
}
|
||||||
window.addEventListener('focus', setLastFocus);
|
window.addEventListener('focus', setLastFocus);
|
||||||
|
|
||||||
// For parseQueryParams()
|
// For parseQueryParams()
|
||||||
@ -64,6 +66,12 @@ export default {
|
|||||||
'publishLocation',
|
'publishLocation',
|
||||||
'data',
|
'data',
|
||||||
],
|
],
|
||||||
|
localStorageDataIds: [
|
||||||
|
'workspaces',
|
||||||
|
'settings',
|
||||||
|
'layoutSettings',
|
||||||
|
'tokens',
|
||||||
|
],
|
||||||
textMaxLength: 150000,
|
textMaxLength: 150000,
|
||||||
sanitizeText(text) {
|
sanitizeText(text) {
|
||||||
const result = `${text || ''}`.slice(0, this.textMaxLength);
|
const result = `${text || ''}`.slice(0, this.textMaxLength);
|
||||||
@ -148,10 +156,10 @@ export default {
|
|||||||
parseQueryParams,
|
parseQueryParams,
|
||||||
addQueryParams(url = '', params = {}) {
|
addQueryParams(url = '', params = {}) {
|
||||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||||
if (!keys.length) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
urlParser.href = url;
|
urlParser.href = url;
|
||||||
|
if (!keys.length) {
|
||||||
|
return urlParser.href;
|
||||||
|
}
|
||||||
if (urlParser.search) {
|
if (urlParser.search) {
|
||||||
urlParser.search += '&';
|
urlParser.search += '&';
|
||||||
} else {
|
} else {
|
||||||
@ -180,8 +188,8 @@ export default {
|
|||||||
startOffset = 0;
|
startOffset = 0;
|
||||||
}
|
}
|
||||||
const elt = document.createElement('span');
|
const elt = document.createElement('span');
|
||||||
Object.keys(eltProperties).forEach((key) => {
|
Object.entries(eltProperties).forEach(([key, value]) => {
|
||||||
elt[key] = eltProperties[key];
|
elt[key] = value;
|
||||||
});
|
});
|
||||||
treeWalker.currentNode.parentNode.insertBefore(elt, treeWalker.currentNode);
|
treeWalker.currentNode.parentNode.insertBefore(elt, treeWalker.currentNode);
|
||||||
elt.appendChild(treeWalker.currentNode);
|
elt.appendChild(treeWalker.currentNode);
|
||||||
|
@ -81,8 +81,7 @@ module.actions = {
|
|||||||
const diffs = diffMatchPatch.diff_main(
|
const diffs = diffMatchPatch.diff_main(
|
||||||
currentContent.text, revisionContent.originalText);
|
currentContent.text, revisionContent.originalText);
|
||||||
diffMatchPatch.diff_cleanupSemantic(diffs);
|
diffMatchPatch.diff_cleanupSemantic(diffs);
|
||||||
Object.keys(currentContent.discussions).forEach((discussionId) => {
|
Object.entries(currentContent.discussions).forEach(([, discussion]) => {
|
||||||
const discussion = currentContent.discussions[discussionId];
|
|
||||||
const adjustOffset = (offsetName) => {
|
const adjustOffset = (offsetName) => {
|
||||||
const marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
|
const marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
|
||||||
marker.adjustOffset(diffs);
|
marker.adjustOffset(diffs);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import moduleTemplate from './moduleTemplate';
|
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
|
import defaultWorkspaces from '../data/defaultWorkspaces';
|
||||||
import defaultSettings from '../data/defaultSettings.yml';
|
import defaultSettings from '../data/defaultSettings.yml';
|
||||||
import defaultLocalSettings from '../data/defaultLocalSettings';
|
import defaultLocalSettings from '../data/defaultLocalSettings';
|
||||||
|
import defaultLayoutSettings from '../data/defaultLayoutSettings';
|
||||||
import plainHtmlTemplate from '../data/plainHtmlTemplate.html';
|
import plainHtmlTemplate from '../data/plainHtmlTemplate.html';
|
||||||
import styledHtmlTemplate from '../data/styledHtmlTemplate.html';
|
import styledHtmlTemplate from '../data/styledHtmlTemplate.html';
|
||||||
import styledHtmlWithTocTemplate from '../data/styledHtmlWithTocTemplate.html';
|
import styledHtmlWithTocTemplate from '../data/styledHtmlWithTocTemplate.html';
|
||||||
@ -13,36 +14,31 @@ const itemTemplate = (id, data = {}) => ({ id, type: 'data', data, hash: 0 });
|
|||||||
|
|
||||||
const empty = (id) => {
|
const empty = (id) => {
|
||||||
switch (id) {
|
switch (id) {
|
||||||
|
case 'workspaces':
|
||||||
|
return itemTemplate(id, defaultWorkspaces());
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return itemTemplate(id, '\n');
|
return itemTemplate(id, '\n');
|
||||||
case 'localSettings':
|
case 'localSettings':
|
||||||
return itemTemplate(id, defaultLocalSettings());
|
return itemTemplate(id, defaultLocalSettings());
|
||||||
|
case 'layoutSettings':
|
||||||
|
return itemTemplate(id, defaultLayoutSettings());
|
||||||
default:
|
default:
|
||||||
return itemTemplate(id);
|
return itemTemplate(id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const module = moduleTemplate(empty, true);
|
|
||||||
|
|
||||||
module.mutations.setItem = (state, value) => {
|
// Item IDs that will be stored in the localStorage
|
||||||
const emptyItem = empty(value.id);
|
const lsItemIdSet = new Set(utils.localStorageDataIds);
|
||||||
const data = typeof value.data === 'object'
|
|
||||||
? Object.assign(emptyItem.data, value.data)
|
|
||||||
: value.data;
|
|
||||||
const item = {
|
|
||||||
...emptyItem,
|
|
||||||
data,
|
|
||||||
};
|
|
||||||
item.hash = utils.hash(utils.serializeObject({
|
|
||||||
...item,
|
|
||||||
hash: undefined,
|
|
||||||
}));
|
|
||||||
Vue.set(state.itemMap, item.id, item);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getter = id => state => (state.itemMap[id] || empty(id)).data;
|
// Getter/setter/patcher factories
|
||||||
|
const getter = id => state => ((lsItemIdSet.has(id)
|
||||||
|
? state.lsItemMap
|
||||||
|
: state.itemMap)[id] || empty(id)).data;
|
||||||
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
|
||||||
const patcher = id => ({ state, commit }, data) => {
|
const patcher = id => ({ state, commit }, data) => {
|
||||||
const item = Object.assign(empty(id), state.itemMap[id]);
|
const item = Object.assign(empty(id), (lsItemIdSet.has(id)
|
||||||
|
? state.lsItemMap
|
||||||
|
: state.itemMap)[id]);
|
||||||
commit('setItem', {
|
commit('setItem', {
|
||||||
...empty(id),
|
...empty(id),
|
||||||
data: typeof data === 'object' ? {
|
data: typeof data === 'object' ? {
|
||||||
@ -52,18 +48,10 @@ const patcher = id => ({ state, commit }, data) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Local settings
|
// For layoutSettings
|
||||||
module.getters.localSettings = getter('localSettings');
|
const layoutSettingsToggler = propertyName => ({ getters, dispatch }, value) => dispatch('patchLayoutSettings', {
|
||||||
module.actions.patchLocalSettings = patcher('localSettings');
|
[propertyName]: value === undefined ? !getters.layoutSettings[propertyName] : value,
|
||||||
const localSettingsToggler = propertyName => ({ getters, dispatch }, value) => dispatch('patchLocalSettings', {
|
|
||||||
[propertyName]: value === undefined ? !getters.localSettings[propertyName] : value,
|
|
||||||
});
|
});
|
||||||
module.actions.toggleNavigationBar = localSettingsToggler('showNavigationBar');
|
|
||||||
module.actions.toggleEditor = localSettingsToggler('showEditor');
|
|
||||||
module.actions.toggleSidePreview = localSettingsToggler('showSidePreview');
|
|
||||||
module.actions.toggleStatusBar = localSettingsToggler('showStatusBar');
|
|
||||||
module.actions.toggleScrollSync = localSettingsToggler('scrollSync');
|
|
||||||
module.actions.toggleFocusMode = localSettingsToggler('focusMode');
|
|
||||||
const notEnoughSpace = (getters) => {
|
const notEnoughSpace = (getters) => {
|
||||||
const constants = getters['layout/constants'];
|
const constants = getters['layout/constants'];
|
||||||
const showGutter = getters['discussion/currentDiscussion'];
|
const showGutter = getters['discussion/currentDiscussion'];
|
||||||
@ -73,60 +61,8 @@ const notEnoughSpace = (getters) => {
|
|||||||
constants.buttonBarWidth +
|
constants.buttonBarWidth +
|
||||||
(showGutter ? constants.gutterWidth : 0);
|
(showGutter ? constants.gutterWidth : 0);
|
||||||
};
|
};
|
||||||
module.actions.toggleSideBar = ({ commit, getters, dispatch, rootGetters }, value) => {
|
|
||||||
// Reset side bar
|
|
||||||
dispatch('setSideBarPanel');
|
|
||||||
// Close explorer if not enough space
|
|
||||||
const patch = {
|
|
||||||
showSideBar: value === undefined ? !getters.localSettings.showSideBar : value,
|
|
||||||
};
|
|
||||||
if (patch.showSideBar && notEnoughSpace(rootGetters)) {
|
|
||||||
patch.showExplorer = false;
|
|
||||||
}
|
|
||||||
dispatch('patchLocalSettings', patch);
|
|
||||||
};
|
|
||||||
module.actions.toggleExplorer = ({ commit, getters, dispatch, rootGetters }, value) => {
|
|
||||||
// Close side bar if not enough space
|
|
||||||
const patch = {
|
|
||||||
showExplorer: value === undefined ? !getters.localSettings.showExplorer : value,
|
|
||||||
};
|
|
||||||
if (patch.showExplorer && notEnoughSpace(rootGetters)) {
|
|
||||||
patch.showSideBar = false;
|
|
||||||
}
|
|
||||||
dispatch('patchLocalSettings', patch);
|
|
||||||
};
|
|
||||||
module.actions.setSideBarPanel = ({ dispatch }, value) => dispatch('patchLocalSettings', {
|
|
||||||
sideBarPanel: value === undefined ? 'menu' : value,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Settings
|
// For templates
|
||||||
module.getters.settings = getter('settings');
|
|
||||||
module.getters.computedSettings = (state, getters) => {
|
|
||||||
const customSettings = yaml.safeLoad(getters.settings);
|
|
||||||
const settings = yaml.safeLoad(defaultSettings);
|
|
||||||
const override = (obj, opt) => {
|
|
||||||
const objType = Object.prototype.toString.call(obj);
|
|
||||||
const optType = Object.prototype.toString.call(opt);
|
|
||||||
if (objType !== optType) {
|
|
||||||
return obj;
|
|
||||||
} else if (objType !== '[object Object]') {
|
|
||||||
return opt;
|
|
||||||
}
|
|
||||||
Object.keys(obj).forEach((key) => {
|
|
||||||
if (key === 'shortcuts') {
|
|
||||||
obj[key] = Object.assign(obj[key], opt[key]);
|
|
||||||
} else {
|
|
||||||
obj[key] = override(obj[key], opt[key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return obj;
|
|
||||||
};
|
|
||||||
return override(settings, customSettings);
|
|
||||||
};
|
|
||||||
module.actions.setSettings = setter('settings');
|
|
||||||
|
|
||||||
// Templates
|
|
||||||
module.getters.templates = getter('templates');
|
|
||||||
const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
||||||
name,
|
name,
|
||||||
value,
|
value,
|
||||||
@ -140,100 +76,8 @@ const additionalTemplates = {
|
|||||||
styledHtmlWithToc: makeAdditionalTemplate('Styled HTML with TOC', styledHtmlWithTocTemplate),
|
styledHtmlWithToc: makeAdditionalTemplate('Styled HTML with TOC', styledHtmlWithTocTemplate),
|
||||||
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
jekyllSite: makeAdditionalTemplate('Jekyll site', jekyllSiteTemplate),
|
||||||
};
|
};
|
||||||
module.getters.allTemplates = (state, getters) => ({
|
|
||||||
...getters.templates,
|
|
||||||
...additionalTemplates,
|
|
||||||
});
|
|
||||||
module.actions.setTemplates = ({ commit }, data) => {
|
|
||||||
const dataToCommit = {
|
|
||||||
...data,
|
|
||||||
};
|
|
||||||
// We don't store additional templates
|
|
||||||
Object.keys(additionalTemplates).forEach((id) => {
|
|
||||||
delete dataToCommit[id];
|
|
||||||
});
|
|
||||||
commit('setItem', itemTemplate('templates', dataToCommit));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Last opened
|
// For tokens
|
||||||
module.getters.lastOpened = getter('lastOpened');
|
|
||||||
module.getters.lastOpenedIds = (state, getters, rootState) => {
|
|
||||||
const lastOpened = {
|
|
||||||
...getters.lastOpened,
|
|
||||||
};
|
|
||||||
const currentFileId = rootState.file.currentId;
|
|
||||||
if (currentFileId && !lastOpened[currentFileId]) {
|
|
||||||
lastOpened[currentFileId] = Date.now();
|
|
||||||
}
|
|
||||||
return Object.keys(lastOpened)
|
|
||||||
.filter(id => rootState.file.itemMap[id])
|
|
||||||
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
|
||||||
.slice(0, 20);
|
|
||||||
};
|
|
||||||
module.actions.setLastOpenedId = ({ getters, commit, dispatch, rootState }, fileId) => {
|
|
||||||
const lastOpened = { ...getters.lastOpened };
|
|
||||||
lastOpened[fileId] = Date.now();
|
|
||||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
|
||||||
dispatch('cleanLastOpenedId');
|
|
||||||
};
|
|
||||||
module.actions.cleanLastOpenedId = ({ getters, commit, rootState }) => {
|
|
||||||
const lastOpened = {};
|
|
||||||
const oldLastOpened = getters.lastOpened;
|
|
||||||
Object.keys(oldLastOpened).forEach((fileId) => {
|
|
||||||
if (rootState.file.itemMap[fileId]) {
|
|
||||||
lastOpened[fileId] = oldLastOpened[fileId];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Sync data
|
|
||||||
module.getters.syncData = getter('syncData');
|
|
||||||
module.getters.syncDataByItemId = (state, getters) => {
|
|
||||||
const result = {};
|
|
||||||
const syncData = getters.syncData;
|
|
||||||
Object.keys(syncData).forEach((id) => {
|
|
||||||
const value = syncData[id];
|
|
||||||
result[value.itemId] = value;
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
module.getters.syncDataByType = (state, getters) => {
|
|
||||||
const result = {};
|
|
||||||
utils.types.forEach((type) => {
|
|
||||||
result[type] = {};
|
|
||||||
});
|
|
||||||
const syncData = getters.syncData;
|
|
||||||
Object.keys(syncData).forEach((id) => {
|
|
||||||
const item = syncData[id];
|
|
||||||
if (result[item.type]) {
|
|
||||||
result[item.type][item.itemId] = item;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
module.actions.patchSyncData = patcher('syncData');
|
|
||||||
module.actions.setSyncData = setter('syncData');
|
|
||||||
|
|
||||||
// Data sync data (used to sync settings and settings)
|
|
||||||
module.getters.dataSyncData = getter('dataSyncData');
|
|
||||||
module.actions.patchDataSyncData = patcher('dataSyncData');
|
|
||||||
|
|
||||||
// Tokens
|
|
||||||
module.getters.tokens = getter('tokens');
|
|
||||||
module.getters.googleTokens = (state, getters) => getters.tokens.google || {};
|
|
||||||
module.getters.dropboxTokens = (state, getters) => getters.tokens.dropbox || {};
|
|
||||||
module.getters.githubTokens = (state, getters) => getters.tokens.github || {};
|
|
||||||
module.getters.wordpressTokens = (state, getters) => getters.tokens.wordpress || {};
|
|
||||||
module.getters.zendeskTokens = (state, getters) => getters.tokens.zendesk || {};
|
|
||||||
module.getters.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]];
|
|
||||||
};
|
|
||||||
module.actions.patchTokens = patcher('tokens');
|
|
||||||
const tokenSetter = providerId => ({ getters, dispatch }, token) => {
|
const tokenSetter = providerId => ({ getters, dispatch }, token) => {
|
||||||
dispatch('patchTokens', {
|
dispatch('patchTokens', {
|
||||||
[providerId]: {
|
[providerId]: {
|
||||||
@ -242,10 +86,213 @@ const tokenSetter = providerId => ({ getters, dispatch }, token) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
module.actions.setGoogleToken = tokenSetter('google');
|
|
||||||
module.actions.setDropboxToken = tokenSetter('dropbox');
|
|
||||||
module.actions.setGithubToken = tokenSetter('github');
|
|
||||||
module.actions.setWordpressToken = tokenSetter('wordpress');
|
|
||||||
module.actions.setZendeskToken = tokenSetter('zendesk');
|
|
||||||
|
|
||||||
export default module;
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
// Data items stored in the DB
|
||||||
|
itemMap: {},
|
||||||
|
// Data items stored in the localStorage
|
||||||
|
lsItemMap: {},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setItem: (state, value) => {
|
||||||
|
// Create an empty item and override its data field
|
||||||
|
const emptyItem = empty(value.id);
|
||||||
|
const data = typeof value.data === 'object'
|
||||||
|
? Object.assign(emptyItem.data, value.data)
|
||||||
|
: value.data;
|
||||||
|
const item = {
|
||||||
|
...emptyItem,
|
||||||
|
data,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate item hash
|
||||||
|
item.hash = utils.hash(utils.serializeObject({
|
||||||
|
...item,
|
||||||
|
hash: undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Store item in itemMap or lsItemMap if its stored in the localStorage
|
||||||
|
Vue.set(lsItemIdSet.has(item.id) ? state.lsItemMap : state.itemMap, item.id, item);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
workspaces: (state) => {
|
||||||
|
const workspaces = (state.lsItemMap.workspaces || empty('workspaces')).data;
|
||||||
|
const result = {};
|
||||||
|
Object.entries(workspaces).forEach(([id, workspace]) => {
|
||||||
|
result[id] = {
|
||||||
|
...workspace,
|
||||||
|
id,
|
||||||
|
providerId: workspace.providerId || 'googleDriveWorkspace',
|
||||||
|
url: utils.addQueryParams('app'),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
dbName: (state, getters) => {
|
||||||
|
let dbName;
|
||||||
|
Object.keys(getters.workspaces).some((id) => {
|
||||||
|
dbName = 'stackedit-db';
|
||||||
|
if (id !== 'main') {
|
||||||
|
dbName += `-${id}`;
|
||||||
|
}
|
||||||
|
return dbName;
|
||||||
|
});
|
||||||
|
return dbName;
|
||||||
|
},
|
||||||
|
settings: getter('settings'),
|
||||||
|
computedSettings: (state, getters) => {
|
||||||
|
const customSettings = yaml.safeLoad(getters.settings);
|
||||||
|
const settings = yaml.safeLoad(defaultSettings);
|
||||||
|
const override = (obj, opt) => {
|
||||||
|
const objType = Object.prototype.toString.call(obj);
|
||||||
|
const optType = Object.prototype.toString.call(opt);
|
||||||
|
if (objType !== optType) {
|
||||||
|
return obj;
|
||||||
|
} else if (objType !== '[object Object]') {
|
||||||
|
return opt;
|
||||||
|
}
|
||||||
|
Object.keys(obj).forEach((key) => {
|
||||||
|
if (key === 'shortcuts') {
|
||||||
|
obj[key] = Object.assign(obj[key], opt[key]);
|
||||||
|
} else {
|
||||||
|
obj[key] = override(obj[key], opt[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return obj;
|
||||||
|
};
|
||||||
|
return override(settings, customSettings);
|
||||||
|
},
|
||||||
|
localSettings: getter('localSettings'),
|
||||||
|
layoutSettings: getter('layoutSettings'),
|
||||||
|
templates: getter('templates'),
|
||||||
|
allTemplates: (state, getters) => ({
|
||||||
|
...getters.templates,
|
||||||
|
...additionalTemplates,
|
||||||
|
}),
|
||||||
|
lastOpened: getter('lastOpened'),
|
||||||
|
lastOpenedIds: (state, getters, rootState) => {
|
||||||
|
const lastOpened = {
|
||||||
|
...getters.lastOpened,
|
||||||
|
};
|
||||||
|
const currentFileId = rootState.file.currentId;
|
||||||
|
if (currentFileId && !lastOpened[currentFileId]) {
|
||||||
|
lastOpened[currentFileId] = Date.now();
|
||||||
|
}
|
||||||
|
return Object.keys(lastOpened)
|
||||||
|
.filter(id => rootState.file.itemMap[id])
|
||||||
|
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
||||||
|
.slice(0, 20);
|
||||||
|
},
|
||||||
|
syncData: getter('syncData'),
|
||||||
|
syncDataByItemId: (state, getters) => {
|
||||||
|
const result = {};
|
||||||
|
Object.entries(getters.syncData).forEach(([, value]) => {
|
||||||
|
result[value.itemId] = value;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
syncDataByType: (state, getters) => {
|
||||||
|
const result = {};
|
||||||
|
utils.types.forEach((type) => {
|
||||||
|
result[type] = {};
|
||||||
|
});
|
||||||
|
Object.entries(getters.syncData).forEach(([, item]) => {
|
||||||
|
if (result[item.type]) {
|
||||||
|
result[item.type][item.itemId] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
dataSyncData: getter('dataSyncData'),
|
||||||
|
tokens: getter('tokens'),
|
||||||
|
googleTokens: (state, getters) => getters.tokens.google || {},
|
||||||
|
dropboxTokens: (state, getters) => getters.tokens.dropbox || {},
|
||||||
|
githubTokens: (state, getters) => getters.tokens.github || {},
|
||||||
|
wordpressTokens: (state, getters) => getters.tokens.wordpress || {},
|
||||||
|
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: {
|
||||||
|
setWorkspaces: setter('workspaces'),
|
||||||
|
patchWorkspaces: patcher('workspaces'),
|
||||||
|
setSettings: setter('settings'),
|
||||||
|
patchLocalSettings: patcher('localSettings'),
|
||||||
|
patchLayoutSettings: patcher('layoutSettings'),
|
||||||
|
toggleNavigationBar: layoutSettingsToggler('showNavigationBar'),
|
||||||
|
toggleEditor: layoutSettingsToggler('showEditor'),
|
||||||
|
toggleSidePreview: layoutSettingsToggler('showSidePreview'),
|
||||||
|
toggleStatusBar: layoutSettingsToggler('showStatusBar'),
|
||||||
|
toggleScrollSync: layoutSettingsToggler('scrollSync'),
|
||||||
|
toggleFocusMode: layoutSettingsToggler('focusMode'),
|
||||||
|
toggleSideBar: ({ commit, getters, dispatch, rootGetters }, value) => {
|
||||||
|
// Reset side bar
|
||||||
|
dispatch('setSideBarPanel');
|
||||||
|
|
||||||
|
// Close explorer if not enough space
|
||||||
|
const patch = {
|
||||||
|
showSideBar: value === undefined ? !getters.layoutSettings.showSideBar : value,
|
||||||
|
};
|
||||||
|
if (patch.showSideBar && notEnoughSpace(rootGetters)) {
|
||||||
|
patch.showExplorer = false;
|
||||||
|
}
|
||||||
|
dispatch('patchLayoutSettings', patch);
|
||||||
|
},
|
||||||
|
toggleExplorer: ({ commit, getters, dispatch, rootGetters }, value) => {
|
||||||
|
// Close side bar if not enough space
|
||||||
|
const patch = {
|
||||||
|
showExplorer: value === undefined ? !getters.layoutSettings.showExplorer : value,
|
||||||
|
};
|
||||||
|
if (patch.showExplorer && notEnoughSpace(rootGetters)) {
|
||||||
|
patch.showSideBar = false;
|
||||||
|
}
|
||||||
|
dispatch('patchLayoutSettings', patch);
|
||||||
|
},
|
||||||
|
setSideBarPanel: ({ dispatch }, value) => dispatch('patchLayoutSettings', {
|
||||||
|
sideBarPanel: value === undefined ? 'menu' : value,
|
||||||
|
}),
|
||||||
|
setTemplates: ({ commit }, data) => {
|
||||||
|
const dataToCommit = {
|
||||||
|
...data,
|
||||||
|
};
|
||||||
|
// We don't store additional templates
|
||||||
|
Object.keys(additionalTemplates).forEach((id) => {
|
||||||
|
delete dataToCommit[id];
|
||||||
|
});
|
||||||
|
commit('setItem', itemTemplate('templates', dataToCommit));
|
||||||
|
},
|
||||||
|
setLastOpenedId: ({ getters, commit, dispatch, rootState }, fileId) => {
|
||||||
|
const lastOpened = { ...getters.lastOpened };
|
||||||
|
lastOpened[fileId] = Date.now();
|
||||||
|
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||||
|
dispatch('cleanLastOpenedId');
|
||||||
|
},
|
||||||
|
cleanLastOpenedId: ({ getters, commit, rootState }) => {
|
||||||
|
const lastOpened = {};
|
||||||
|
const oldLastOpened = getters.lastOpened;
|
||||||
|
Object.entries(oldLastOpened).forEach(([fileId, date]) => {
|
||||||
|
if (rootState.file.itemMap[fileId]) {
|
||||||
|
lastOpened[fileId] = date;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||||
|
},
|
||||||
|
setSyncData: setter('syncData'),
|
||||||
|
patchSyncData: patcher('syncData'),
|
||||||
|
patchDataSyncData: patcher('dataSyncData'),
|
||||||
|
patchTokens: patcher('tokens'),
|
||||||
|
setGoogleToken: tokenSetter('google'),
|
||||||
|
setDropboxToken: tokenSetter('dropbox'),
|
||||||
|
setGithubToken: tokenSetter('github'),
|
||||||
|
setWordpressToken: tokenSetter('wordpress'),
|
||||||
|
setZendeskToken: tokenSetter('zendesk'),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
@ -65,8 +65,7 @@ export default {
|
|||||||
const discussions = rootGetters['content/current'].discussions;
|
const discussions = rootGetters['content/current'].discussions;
|
||||||
const comments = rootGetters['content/current'].comments;
|
const comments = rootGetters['content/current'].comments;
|
||||||
const discussionLastComments = {};
|
const discussionLastComments = {};
|
||||||
Object.keys(comments).forEach((commentId) => {
|
Object.entries(comments).forEach(([, comment]) => {
|
||||||
const comment = comments[commentId];
|
|
||||||
if (discussions[comment.discussionId]) {
|
if (discussions[comment.discussionId]) {
|
||||||
const lastComment = discussionLastComments[comment.discussionId];
|
const lastComment = discussionLastComments[comment.discussionId];
|
||||||
if (!lastComment || lastComment.created < comment.created) {
|
if (!lastComment || lastComment.created < comment.created) {
|
||||||
@ -84,10 +83,10 @@ export default {
|
|||||||
}
|
}
|
||||||
const discussions = rootGetters['content/current'].discussions;
|
const discussions = rootGetters['content/current'].discussions;
|
||||||
const discussionLastComments = getters.currentFileDiscussionLastComments;
|
const discussionLastComments = getters.currentFileDiscussionLastComments;
|
||||||
Object.keys(discussionLastComments)
|
Object.entries(discussionLastComments)
|
||||||
.sort((id1, id2) =>
|
.sort(([, lastComment1], [, lastComment2]) =>
|
||||||
discussionLastComments[id2].created - discussionLastComments[id1].created)
|
lastComment1.created - lastComment2.created)
|
||||||
.forEach((discussionId) => {
|
.forEach(([discussionId]) => {
|
||||||
currentFileDiscussions[discussionId] = discussions[discussionId];
|
currentFileDiscussions[discussionId] = discussions[discussionId];
|
||||||
});
|
});
|
||||||
return currentFileDiscussions;
|
return currentFileDiscussions;
|
||||||
@ -100,13 +99,13 @@ export default {
|
|||||||
const comments = {};
|
const comments = {};
|
||||||
if (getters.currentDiscussion) {
|
if (getters.currentDiscussion) {
|
||||||
const contentComments = rootGetters['content/current'].comments;
|
const contentComments = rootGetters['content/current'].comments;
|
||||||
Object.keys(contentComments)
|
Object.entries(contentComments)
|
||||||
.filter(commentId =>
|
.filter(([, comment]) =>
|
||||||
contentComments[commentId].discussionId === state.currentDiscussionId)
|
comment.discussionId === state.currentDiscussionId)
|
||||||
.sort((id1, id2) =>
|
.sort(([, comment1], [, comment2]) =>
|
||||||
contentComments[id1].created - contentComments[id2].created)
|
comment1.created - comment2.created)
|
||||||
.forEach((commentId) => {
|
.forEach(([commentId, comment]) => {
|
||||||
comments[commentId] = contentComments[commentId];
|
comments[commentId] = comment;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return comments;
|
return comments;
|
||||||
@ -126,10 +125,11 @@ export default {
|
|||||||
createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
|
createNewDiscussion({ commit, dispatch, rootGetters }, selection) {
|
||||||
const loginToken = rootGetters['data/loginToken'];
|
const loginToken = rootGetters['data/loginToken'];
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
dispatch('modal/signInForComment', null, { root: true })
|
dispatch('modal/signInForComment', {
|
||||||
.then(() => googleHelper.signin())
|
onResolve: () => googleHelper.signin()
|
||||||
.then(() => syncSvc.requestSync())
|
.then(() => syncSvc.requestSync())
|
||||||
.then(() => dispatch('createNewDiscussion', selection))
|
.then(() => dispatch('createNewDiscussion', selection)),
|
||||||
|
}, { root: true })
|
||||||
.catch(() => { }); // Cancel
|
.catch(() => { }); // Cancel
|
||||||
} else if (selection) {
|
} else if (selection) {
|
||||||
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
|
let text = rootGetters['content/current'].text.slice(selection.start, selection.end).trim();
|
||||||
@ -150,8 +150,7 @@ export default {
|
|||||||
discussions: {},
|
discussions: {},
|
||||||
comments: {},
|
comments: {},
|
||||||
};
|
};
|
||||||
Object.keys(comments).forEach((commentId) => {
|
Object.entries(comments).forEach(([commentId, comment]) => {
|
||||||
const comment = comments[commentId];
|
|
||||||
const discussion = discussions[comment.discussionId];
|
const discussion = discussions[comment.discussionId];
|
||||||
if (discussion && comment !== filterComment && discussion !== filterDiscussion) {
|
if (discussion && comment !== filterComment && discussion !== filterDiscussion) {
|
||||||
patch.discussions[comment.discussionId] = discussion;
|
patch.discussions[comment.discussionId] = discussion;
|
||||||
|
@ -87,8 +87,7 @@ export default {
|
|||||||
nodeMap[item.id] = new Node(item, locations);
|
nodeMap[item.id] = new Node(item, locations);
|
||||||
});
|
});
|
||||||
const rootNode = new Node(emptyFolder(), [], true, true);
|
const rootNode = new Node(emptyFolder(), [], true, true);
|
||||||
Object.keys(nodeMap).forEach((id) => {
|
Object.entries(nodeMap).forEach(([id, node]) => {
|
||||||
const node = nodeMap[id];
|
|
||||||
let parentNode = nodeMap[node.item.parentId];
|
let parentNode = nodeMap[node.item.parentId];
|
||||||
if (!parentNode || !parentNode.isFolder) {
|
if (!parentNode || !parentNode.isFolder) {
|
||||||
if (id === 'trash') {
|
if (id === 'trash') {
|
||||||
|
@ -43,7 +43,6 @@ const store = new Vuex.Store({
|
|||||||
userInfo,
|
userInfo,
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
ready: false,
|
|
||||||
offline: false,
|
offline: false,
|
||||||
lastOfflineCheck: 0,
|
lastOfflineCheck: 0,
|
||||||
minuteCounter: 0,
|
minuteCounter: 0,
|
||||||
@ -61,9 +60,6 @@ const store = new Vuex.Store({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setReady: (state) => {
|
|
||||||
state.ready = true;
|
|
||||||
},
|
|
||||||
setOffline: (state, value) => {
|
setOffline: (state, value) => {
|
||||||
state.offline = value;
|
state.offline = value;
|
||||||
},
|
},
|
||||||
|
@ -20,14 +20,14 @@ const constants = {
|
|||||||
statusBarHeight: 20,
|
statusBarHeight: 20,
|
||||||
};
|
};
|
||||||
|
|
||||||
function computeStyles(state, getters, localSettings = getters['data/localSettings'], styles = {
|
function computeStyles(state, getters, layoutSettings = getters['data/layoutSettings'], styles = {
|
||||||
showNavigationBar: !localSettings.showEditor || localSettings.showNavigationBar,
|
showNavigationBar: !layoutSettings.showEditor || layoutSettings.showNavigationBar,
|
||||||
showStatusBar: localSettings.showStatusBar,
|
showStatusBar: layoutSettings.showStatusBar,
|
||||||
showEditor: localSettings.showEditor,
|
showEditor: layoutSettings.showEditor,
|
||||||
showSidePreview: localSettings.showSidePreview && localSettings.showEditor,
|
showSidePreview: layoutSettings.showSidePreview && layoutSettings.showEditor,
|
||||||
showPreview: localSettings.showSidePreview || !localSettings.showEditor,
|
showPreview: layoutSettings.showSidePreview || !layoutSettings.showEditor,
|
||||||
showSideBar: localSettings.showSideBar,
|
showSideBar: layoutSettings.showSideBar,
|
||||||
showExplorer: localSettings.showExplorer,
|
showExplorer: layoutSettings.showExplorer,
|
||||||
layoutOverflow: false,
|
layoutOverflow: false,
|
||||||
}) {
|
}) {
|
||||||
styles.innerHeight = state.layout.bodyHeight;
|
styles.innerHeight = state.layout.bodyHeight;
|
||||||
@ -64,7 +64,7 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
styles.showSidePreview = false;
|
styles.showSidePreview = false;
|
||||||
styles.showPreview = false;
|
styles.showPreview = false;
|
||||||
styles.layoutOverflow = false;
|
styles.layoutOverflow = false;
|
||||||
return computeStyles(state, getters, localSettings, styles);
|
return computeStyles(state, getters, layoutSettings, styles);
|
||||||
}
|
}
|
||||||
|
|
||||||
const computedSettings = getters['data/computedSettings'];
|
const computedSettings = getters['data/computedSettings'];
|
||||||
@ -96,7 +96,7 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
if (!styles.showSidePreview) {
|
if (!styles.showSidePreview) {
|
||||||
styles.previewWidth += constants.buttonBarWidth;
|
styles.previewWidth += constants.buttonBarWidth;
|
||||||
}
|
}
|
||||||
styles.previewGutterWidth = showGutter && !localSettings.showEditor
|
styles.previewGutterWidth = showGutter && !layoutSettings.showEditor
|
||||||
? constants.gutterWidth
|
? constants.gutterWidth
|
||||||
: 0;
|
: 0;
|
||||||
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
|
const previewLeftPadding = previewRightPadding + styles.previewGutterWidth;
|
||||||
@ -107,7 +107,7 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
doublePanelWidth;
|
doublePanelWidth;
|
||||||
const editorRightPadding = Math.max(
|
const editorRightPadding = Math.max(
|
||||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||||
styles.editorGutterWidth = showGutter && localSettings.showEditor
|
styles.editorGutterWidth = showGutter && layoutSettings.showEditor
|
||||||
? constants.gutterWidth
|
? constants.gutterWidth
|
||||||
: 0;
|
: 0;
|
||||||
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
|
const editorLeftPadding = editorRightPadding + styles.editorGutterWidth;
|
||||||
@ -129,8 +129,8 @@ function computeStyles(state, getters, localSettings = getters['data/localSettin
|
|||||||
styles.hideLocations = true;
|
styles.hideLocations = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
styles.titleMaxWidth = Math.min(styles.titleMaxWidth, maxTitleMaxWidth);
|
styles.titleMaxWidth = Math.max(minTitleMaxWidth,
|
||||||
styles.titleMaxWidth = Math.max(styles.titleMaxWidth, minTitleMaxWidth);
|
Math.min(maxTitleMaxWidth, styles.titleMaxWidth));
|
||||||
return styles;
|
return styles;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,8 +162,8 @@ export default {
|
|||||||
updateBodySize({ commit, dispatch, rootGetters }) {
|
updateBodySize({ commit, dispatch, rootGetters }) {
|
||||||
commit('updateBodySize');
|
commit('updateBodySize');
|
||||||
// Make sure both explorer and side bar are not open if body width is small
|
// Make sure both explorer and side bar are not open if body width is small
|
||||||
const localSettings = rootGetters['data/localSettings'];
|
const layoutSettings = rootGetters['data/layoutSettings'];
|
||||||
dispatch('data/toggleExplorer', localSettings.showExplorer, { root: true });
|
dispatch('data/toggleExplorer', layoutSettings.showExplorer, { root: true });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -86,24 +86,33 @@ export default {
|
|||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
onResolve,
|
onResolve,
|
||||||
}),
|
}),
|
||||||
signInForSponsorship: ({ dispatch }) => dispatch('open', {
|
workspaceGoogleRedirection: ({ dispatch }, { onResolve }) => dispatch('open', {
|
||||||
|
content: '<p>You have to sign in with Google to access this workspace.</p>',
|
||||||
|
resolveText: 'Ok, sign in',
|
||||||
|
rejectText: 'Cancel',
|
||||||
|
onResolve,
|
||||||
|
}),
|
||||||
|
signInForSponsorship: ({ dispatch }, { onResolve }) => dispatch('open', {
|
||||||
type: 'signInForSponsorship',
|
type: 'signInForSponsorship',
|
||||||
content: `<p>You have to sign in with Google to enable your sponsorship.</p>
|
content: `<p>You have to sign in with Google to enable your sponsorship.</p>
|
||||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||||
resolveText: 'Ok, sign in',
|
resolveText: 'Ok, sign in',
|
||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
|
onResolve,
|
||||||
}),
|
}),
|
||||||
signInForComment: ({ dispatch }) => dispatch('open', {
|
signInForComment: ({ dispatch }, { onResolve }) => dispatch('open', {
|
||||||
content: `<p>You have to sign in with Google to start commenting.</p>
|
content: `<p>You have to sign in with Google to start commenting.</p>
|
||||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||||
resolveText: 'Ok, sign in',
|
resolveText: 'Ok, sign in',
|
||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
|
onResolve,
|
||||||
}),
|
}),
|
||||||
signInForHistory: ({ dispatch }) => dispatch('open', {
|
signInForHistory: ({ dispatch }, { onResolve }) => dispatch('open', {
|
||||||
content: `<p>You have to sign in with Google to enable revision history.</p>
|
content: `<p>You have to sign in with Google to enable revision history.</p>
|
||||||
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
<div class="modal__info"><b>Note:</b> This will sync all your files and settings.</div>`,
|
||||||
resolveText: 'Ok, sign in',
|
resolveText: 'Ok, sign in',
|
||||||
rejectText: 'Cancel',
|
rejectText: 'Cancel',
|
||||||
|
onResolve,
|
||||||
}),
|
}),
|
||||||
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
sponsorOnly: ({ dispatch }) => dispatch('open', {
|
||||||
content: '<p>This feature is restricted to <b>sponsor users</b> as it relies on server resources.</p>',
|
content: '<p>This feature is restricted to <b>sponsor users</b> as it relies on server resources.</p>',
|
||||||
|
@ -33,7 +33,7 @@ export default (empty, simpleHash = false) => {
|
|||||||
itemMap: {},
|
itemMap: {},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
items: state => Object.keys(state.itemMap).map(key => state.itemMap[key]),
|
items: state => Object.entries(state.itemMap).map(([, item]) => item),
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setItem,
|
setItem,
|
||||||
|
29
src/store/workspace.js
Normal file
29
src/store/workspace.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
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 {
|
||||||
|
namespaced: true,
|
||||||
|
state: {
|
||||||
|
currentWorkspaceId: null,
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
setCurrentWorkspaceId: (state, value) => {
|
||||||
|
state.currentWorkspaceId = value;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
currentWorkspace: (state, getters, rootState, rootGetters) => {
|
||||||
|
const workspaces = rootGetters['data/workspaces'];
|
||||||
|
return workspaces[state.currentWorkspaceId] || workspaces.main;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
},
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user