Added backup menu. Added print menu. Fixed splash screen.
This commit is contained in:
parent
3e9b75d3e8
commit
e15b9fae16
@ -48,7 +48,7 @@ export default {
|
|||||||
newItem(isFolder) {
|
newItem(isFolder) {
|
||||||
let parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
|
let parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
|
||||||
if (parentId === 'trash') {
|
if (parentId === 'trash') {
|
||||||
parentId = undefined;
|
parentId = null;
|
||||||
}
|
}
|
||||||
this.$store.dispatch('explorer/openNode', parentId);
|
this.$store.dispatch('explorer/openNode', parentId);
|
||||||
this.$store.commit('explorer/setNewItem', {
|
this.$store.commit('explorer/setNewItem', {
|
||||||
@ -92,6 +92,7 @@ export default {
|
|||||||
id: selectedNode.item.id,
|
id: selectedNode.item.id,
|
||||||
parentId: 'trash',
|
parentId: 'trash',
|
||||||
});
|
});
|
||||||
|
this.$store.commit('file/setCurrentId', this.$store.getters['data/lastOpenedIds'][1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -58,7 +58,7 @@ export default {
|
|||||||
return this.$store.state.explorer.newChildNode.item.name;
|
return this.$store.state.explorer.newChildNode.item.name;
|
||||||
},
|
},
|
||||||
set(value) {
|
set(value) {
|
||||||
this.$store.commit('explorer/setNewItemName', value && value.slice(0, 250));
|
this.$store.commit('explorer/setNewItemName', value);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
editingNodeName: {
|
editingNodeName: {
|
||||||
@ -91,28 +91,18 @@ export default {
|
|||||||
submitNewChild(cancel) {
|
submitNewChild(cancel) {
|
||||||
const newChildNode = this.$store.state.explorer.newChildNode;
|
const newChildNode = this.$store.state.explorer.newChildNode;
|
||||||
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
||||||
const id = utils.uid();
|
|
||||||
if (newChildNode.isFolder) {
|
if (newChildNode.isFolder) {
|
||||||
|
const id = utils.uid();
|
||||||
this.$store.commit('folder/setItem', {
|
this.$store.commit('folder/setItem', {
|
||||||
...newChildNode.item,
|
...newChildNode.item,
|
||||||
id,
|
id,
|
||||||
|
name: utils.sanitizeName(newChildNode.item.name),
|
||||||
});
|
});
|
||||||
|
this.select(id);
|
||||||
} else {
|
} else {
|
||||||
// Add empty line at the end if needed
|
this.$store.dispatch('createFile', newChildNode.item)
|
||||||
const ensureFinalNewLine = text => `${text}\n`.replace(/\n\n$/, '\n');
|
.then(file => this.select(file.id));
|
||||||
const text = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileContent);
|
|
||||||
const properties = ensureFinalNewLine(this.$store.getters['data/computedSettings'].newFileProperties);
|
|
||||||
this.$store.commit('content/setItem', {
|
|
||||||
id: `${id}/content`,
|
|
||||||
text,
|
|
||||||
properties,
|
|
||||||
});
|
|
||||||
this.$store.commit('file/setItem', {
|
|
||||||
...newChildNode.item,
|
|
||||||
id,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
this.select(id);
|
|
||||||
}
|
}
|
||||||
this.$store.commit('explorer/setNewItem', null);
|
this.$store.commit('explorer/setNewItem', null);
|
||||||
},
|
},
|
||||||
@ -123,7 +113,7 @@ export default {
|
|||||||
if (!cancel && id && value) {
|
if (!cancel && id && value) {
|
||||||
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
||||||
id,
|
id,
|
||||||
name: value.slice(0, 250),
|
name: utils.sanitizeName(value),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.$store.commit('explorer/setEditingId', null);
|
this.$store.commit('explorer/setEditingId', null);
|
||||||
|
@ -80,6 +80,7 @@ import editorSvc from '../services/editorSvc';
|
|||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
import publishSvc from '../services/publishSvc';
|
import publishSvc from '../services/publishSvc';
|
||||||
import animationSvc from '../services/animationSvc';
|
import animationSvc from '../services/animationSvc';
|
||||||
|
import utils from '../services/utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -171,7 +172,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const title = this.title.trim();
|
const title = this.title.trim();
|
||||||
if (title) {
|
if (title) {
|
||||||
this.$store.dispatch('file/patchCurrent', { name: title.slice(0, 250) });
|
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) });
|
||||||
} else {
|
} else {
|
||||||
this.title = this.$store.getters['file/current'].name;
|
this.title = this.$store.getters['file/current'].name;
|
||||||
}
|
}
|
||||||
@ -342,9 +343,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.navigation-bar__title--input,
|
.navigation-bar__title--input,
|
||||||
.navigation-bar__inner--edit-buttons,
|
.navigation-bar__inner--edit-buttons {
|
||||||
.navigation-bar__inner--button,
|
|
||||||
.navigation-bar__spinner {
|
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
.navigation-bar--editor & {
|
.navigation-bar--editor & {
|
||||||
@ -355,6 +354,7 @@ export default {
|
|||||||
.navigation-bar__button {
|
.navigation-bar__button {
|
||||||
display: none;
|
display: none;
|
||||||
|
|
||||||
|
.navigation-bar__inner--button &,
|
||||||
.navigation-bar--editor & {
|
.navigation-bar--editor & {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
@ -374,13 +374,13 @@ $b: $d/10;
|
|||||||
$t: 3000ms;
|
$t: 3000ms;
|
||||||
|
|
||||||
.navigation-bar__spinner {
|
.navigation-bar__spinner {
|
||||||
width: 22px;
|
width: 24px;
|
||||||
margin: 7px 0 0 8px;
|
margin: 7px 0 0 8px;
|
||||||
color: #b2b2b2;
|
color: #b2b2b2;
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
width: 22px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
color: transparentize($error-color, 0.5);
|
color: transparentize($error-color, 0.5);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="splash-screen">
|
<div class="splash-screen">
|
||||||
<div class="splash-screen__inner background-logo"></div>
|
<div class="splash-screen__inner logo-background"></div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -81,7 +81,8 @@ textarea {
|
|||||||
|
|
||||||
&:active,
|
&:active,
|
||||||
&:focus,
|
&:focus,
|
||||||
&:hover {
|
&:hover,
|
||||||
|
.hidden-file:focus + & {
|
||||||
color: #333;
|
color: #333;
|
||||||
background-color: rgba(0, 0, 0, 0.067);
|
background-color: rgba(0, 0, 0, 0.067);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@ -193,3 +194,41 @@ textarea {
|
|||||||
background: no-repeat center url('../assets/logo.svg');
|
background: no-repeat center url('../assets/logo.svg');
|
||||||
background-size: contain;
|
background-size: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background-color: transparent !important;
|
||||||
|
color: #000 !important; // Black prints faster
|
||||||
|
overflow: visible !important;
|
||||||
|
position: absolute !important;
|
||||||
|
|
||||||
|
div {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body > .app,
|
||||||
|
body > .app > .layout,
|
||||||
|
body > .app > .layout > .layout__panel,
|
||||||
|
body > .app > .layout > .layout__panel > .layout__panel,
|
||||||
|
body > .app > .layout > .layout__panel > .layout__panel > .layout__panel,
|
||||||
|
body > .app > .layout > .layout__panel > .layout__panel > .layout__panel > .layout__panel--preview,
|
||||||
|
body > .app > .layout > .layout__panel > .layout__panel > .layout__panel > .layout__panel--preview div {
|
||||||
|
background-color: transparent !important;
|
||||||
|
display: block !important;
|
||||||
|
height: auto !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
position: static !important;
|
||||||
|
width: auto !important;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview__inner-2 {
|
||||||
|
padding: 0 50px !important;
|
||||||
|
}
|
||||||
|
// scss-lint:enable ImportantRule
|
||||||
|
}
|
||||||
|
@ -37,10 +37,19 @@
|
|||||||
Markdown cheat sheet
|
Markdown cheat sheet
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="importFile">
|
<menu-entry @click.native="print">
|
||||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
<icon-printer slot="icon"></icon-printer>
|
||||||
Import from disk
|
Print
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
|
<input class="hidden-file" id="import-disk-file-input" type="file" @change="onImportFile">
|
||||||
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-disk-file-input">
|
||||||
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-hard-disk></icon-hard-disk>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--column">
|
||||||
|
Import from disk
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
<menu-entry @click.native="setPanel('export')">
|
<menu-entry @click.native="setPanel('export')">
|
||||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||||
Export to disk
|
Export to disk
|
||||||
@ -58,6 +67,7 @@ import MenuEntry from './MenuEntry';
|
|||||||
import UserImage from '../UserImage';
|
import UserImage from '../UserImage';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
|
import providerUtils from '../../services/providers/providerUtils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -80,13 +90,32 @@ export default {
|
|||||||
() => {}, // Cancel
|
() => {}, // Cancel
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
importFile() {
|
onImportFile(evt) {
|
||||||
return this.$store.dispatch('modal/notImplemented');
|
const file = evt.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const content = e.target.result;
|
||||||
|
if (content.match(/\uFFFD/)) {
|
||||||
|
this.$store.dispatch('notification/error', 'File is not readable.');
|
||||||
|
} else {
|
||||||
|
this.$store.dispatch('createFile', {
|
||||||
|
...providerUtils.parseContent(content),
|
||||||
|
name: file.name,
|
||||||
|
})
|
||||||
|
.then(item => this.$store.commit('file/setCurrentId', item.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fileProperties() {
|
fileProperties() {
|
||||||
return this.$store.dispatch('modal/open', 'fileProperties')
|
return this.$store.dispatch('modal/open', 'fileProperties')
|
||||||
.catch(() => {}); // Cancel
|
.catch(() => {}); // Cancel
|
||||||
},
|
},
|
||||||
|
print() {
|
||||||
|
print();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -37,4 +37,9 @@
|
|||||||
border-radius: $border-radius-base;
|
border-radius: $border-radius-base;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-file {
|
||||||
|
position: fixed;
|
||||||
|
top: -999px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -16,6 +16,20 @@
|
|||||||
<span>Sign out and clean local data.</span>
|
<span>Sign out and clean local data.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
|
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
||||||
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
||||||
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-hard-disk></icon-hard-disk>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--column">
|
||||||
|
Import backup
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<menu-entry href="#exportBackup=true" target="_blank">
|
||||||
|
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||||
|
Export backup
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
<menu-entry @click.native="about">
|
<menu-entry @click.native="about">
|
||||||
<icon-help-circle slot="icon"></icon-help-circle>
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
<span>About StackEdit</span>
|
<span>About StackEdit</span>
|
||||||
@ -34,12 +48,29 @@
|
|||||||
<script>
|
<script>
|
||||||
import MenuEntry from './MenuEntry';
|
import MenuEntry from './MenuEntry';
|
||||||
import localDbSvc from '../../services/localDbSvc';
|
import localDbSvc from '../../services/localDbSvc';
|
||||||
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
MenuEntry,
|
MenuEntry,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
onImportBackup(evt) {
|
||||||
|
const file = evt.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target.result;
|
||||||
|
if (text.match(/\uFFFD/)) {
|
||||||
|
this.$store.dispatch('notification/error', 'File is not readable.');
|
||||||
|
} else {
|
||||||
|
backupSvc.importBackup(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const blob = file.slice(0, 10000000);
|
||||||
|
reader.readAsText(blob);
|
||||||
|
}
|
||||||
|
},
|
||||||
settings() {
|
settings() {
|
||||||
return this.$store.dispatch('modal/open', 'settings')
|
return this.$store.dispatch('modal/open', 'settings')
|
||||||
.then(
|
.then(
|
||||||
|
@ -35,6 +35,7 @@ import yaml from 'js-yaml';
|
|||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import Tab from './Tab';
|
import Tab from './Tab';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
|
import utils from '../../services/utils';
|
||||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||||
|
|
||||||
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
const emptyProperties = '# Add custom properties for the current file here to override the default properties.\n';
|
||||||
@ -79,7 +80,7 @@ export default {
|
|||||||
if (!this.error) {
|
if (!this.error) {
|
||||||
this.$store.commit('content/patchItem', {
|
this.$store.commit('content/patchItem', {
|
||||||
id: this.contentId,
|
id: this.contentId,
|
||||||
properties: this.strippedCustomProperties,
|
properties: utils.sanitizeText(this.strippedCustomProperties),
|
||||||
});
|
});
|
||||||
this.config.resolve();
|
this.config.resolve();
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ export default {
|
|||||||
submitEdit(cancel) {
|
submitEdit(cancel) {
|
||||||
const template = this.templates[this.selectedId];
|
const template = this.templates[this.selectedId];
|
||||||
if (!cancel && this.editingName) {
|
if (!cancel && this.editingName) {
|
||||||
template.name = this.editingName.slice(0, 250);
|
template.name = utils.sanitizeName(this.editingName);
|
||||||
} else {
|
} else {
|
||||||
this.editingName = template.name;
|
this.editingName = template.name;
|
||||||
}
|
}
|
||||||
|
5
src/icons/Printer.vue
Normal file
5
src/icons/Printer.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="M18,3H6V7H18M19,12C18.45,12 18,11.55 18,11C18,10.45 18.45,10 19,10C19.55,10 20,10.45 20,11C20,11.55 19.55,12 19,12M16,19H8V14H16M19,8H5C3.34,8 2,9.34 2,11V17H6V21H18V17H22V11C22,9.34 20.66,8 19,8Z" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
<path d="M4.037,6l3.5,-3.5l3.5,3.5l-2.5,0l0,12l2.5,0l-3.5,3.5l-3.5,-3.5l2.5,0l0,-12l-2.5,0Zm8.926,0l3.5,-3.5l3.5,3.5l-2.5,0l0,12l2.5,0l-3.5,3.5l-3.5,-3.5l2.5,0l0,-12l-2.5,0Z"/>
|
<path d="M9,18l3,0l-4,4l-4,-4l3,0l0,-3l2,0l0,3Zm8,0l3,0l-4,4l-4,-4l3,0l0,-3l2,0l0,3Zm0.055,-5l-10.11,0l0,-2l10.11,0l0,2Zm-8.055,-4l-2,0l0,-3l-3,0l4,-4l4,4l4,-4l4,4l-3,0l0,3l-2,0l0,-3l-6,0l0,3Z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</template>
|
</template>
|
||||||
|
@ -42,6 +42,7 @@ import Alert from './Alert';
|
|||||||
import SignalOff from './SignalOff';
|
import SignalOff from './SignalOff';
|
||||||
import Folder from './Folder';
|
import Folder from './Folder';
|
||||||
import ScrollSync from './ScrollSync';
|
import ScrollSync from './ScrollSync';
|
||||||
|
import Printer from './Printer';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -86,3 +87,4 @@ Vue.component('iconAlert', Alert);
|
|||||||
Vue.component('iconSignalOff', SignalOff);
|
Vue.component('iconSignalOff', SignalOff);
|
||||||
Vue.component('iconFolder', Folder);
|
Vue.component('iconFolder', Folder);
|
||||||
Vue.component('iconScrollSync', ScrollSync);
|
Vue.component('iconScrollSync', ScrollSync);
|
||||||
|
Vue.component('iconPrinter', Printer);
|
||||||
|
68
src/services/backupSvc.js
Normal file
68
src/services/backupSvc.js
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import store from '../store';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
importBackup(jsonValue) {
|
||||||
|
const nameMap = {};
|
||||||
|
const parentIdMap = {};
|
||||||
|
const textMap = {};
|
||||||
|
const propertiesMap = {};
|
||||||
|
const discussionsMap = {};
|
||||||
|
const commentsMap = {};
|
||||||
|
const folderIdMap = {
|
||||||
|
trash: 'trash',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse JSON value
|
||||||
|
const parsedValue = JSON.parse(jsonValue);
|
||||||
|
Object.keys(parsedValue).forEach((id) => {
|
||||||
|
const value = parsedValue[id];
|
||||||
|
if (value) {
|
||||||
|
const v4Match = id.match(/^file\.([^.]+)\.([^.]+)$/);
|
||||||
|
if (v4Match) {
|
||||||
|
// StackEdit v4 format
|
||||||
|
const [, v4Id, type] = v4Match;
|
||||||
|
if (type === 'title') {
|
||||||
|
nameMap[v4Id] = value;
|
||||||
|
} else if (type === 'content') {
|
||||||
|
textMap[v4Id] = value;
|
||||||
|
}
|
||||||
|
} else if (value.type === 'folder') {
|
||||||
|
// StackEdit v5 folder
|
||||||
|
const folderId = utils.uid();
|
||||||
|
const name = utils.sanitizeName(value.name);
|
||||||
|
const parentId = `${value.parentId || ''}` || null;
|
||||||
|
store.commit('folder/setItem', {
|
||||||
|
id: folderId,
|
||||||
|
name,
|
||||||
|
parentId,
|
||||||
|
});
|
||||||
|
folderIdMap[id] = folderId;
|
||||||
|
} else if (value.type === 'file') {
|
||||||
|
// StackEdit v5 file
|
||||||
|
nameMap[id] = utils.sanitizeName(value.name);
|
||||||
|
parentIdMap[id] = `${value.parentId || ''}`;
|
||||||
|
} else if (value.type === 'content') {
|
||||||
|
// StackEdit v5 content
|
||||||
|
const [fileId] = id.split('/');
|
||||||
|
if (fileId) {
|
||||||
|
textMap[fileId] = value.text;
|
||||||
|
propertiesMap[fileId] = value.properties;
|
||||||
|
discussionsMap[fileId] = value.discussions;
|
||||||
|
commentsMap[fileId] = value.comments;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Go through the maps
|
||||||
|
Object.keys(nameMap).forEach(externalId => store.dispatch('createFile', {
|
||||||
|
name: nameMap[externalId],
|
||||||
|
parentId: folderIdMap[parentIdMap[externalId]],
|
||||||
|
text: textMap[externalId],
|
||||||
|
properties: propertiesMap[externalId],
|
||||||
|
discussions: discussionsMap[externalId],
|
||||||
|
comments: commentsMap[externalId],
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
};
|
@ -110,9 +110,8 @@ export default {
|
|||||||
clEditor.on('contentChanged', (text) => {
|
clEditor.on('contentChanged', (text) => {
|
||||||
const oldContent = store.getters['content/current'];
|
const oldContent = store.getters['content/current'];
|
||||||
const newContent = {
|
const newContent = {
|
||||||
...oldContent,
|
...utils.deepCopy(oldContent),
|
||||||
discussions: utils.deepCopy(oldContent.discussions),
|
text: utils.sanitizeText(text),
|
||||||
text,
|
|
||||||
};
|
};
|
||||||
syncDiscussionMarkers(newContent, true);
|
syncDiscussionMarkers(newContent, true);
|
||||||
if (!isChangePatch) {
|
if (!isChangePatch) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import 'babel-polyfill';
|
import 'babel-polyfill';
|
||||||
import 'indexeddbshim/dist/indexeddbshim';
|
import 'indexeddbshim/dist/indexeddbshim';
|
||||||
|
import FileSaver from 'file-saver';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import welcomeFile from '../data/welcomeFile.md';
|
import welcomeFile from '../data/welcomeFile.md';
|
||||||
@ -8,6 +9,7 @@ const indexedDB = window.indexedDB;
|
|||||||
const dbVersion = 1;
|
const dbVersion = 1;
|
||||||
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||||
const dbStoreName = 'objects';
|
const dbStoreName = 'objects';
|
||||||
|
const exportBackup = utils.queryParams.exportBackup;
|
||||||
|
|
||||||
if (!indexedDB) {
|
if (!indexedDB) {
|
||||||
throw new Error('Your browser is not supported. Please upgrade to the latest version.');
|
throw new Error('Your browser is not supported. Please upgrade to the latest version.');
|
||||||
@ -219,7 +221,7 @@ const localDbSvc = {
|
|||||||
// DB item is different from the corresponding store item
|
// DB item is different from the corresponding store item
|
||||||
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
||||||
// Update content only if it exists in the store
|
// Update content only if it exists in the store
|
||||||
if (existingStoreItem || !contentTypes[dbItem.type]) {
|
if (existingStoreItem || !contentTypes[dbItem.type] || exportBackup) {
|
||||||
// Put item in the store
|
// Put item in the store
|
||||||
dbItem.tx = undefined;
|
dbItem.tx = undefined;
|
||||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||||
@ -314,6 +316,16 @@ const ifNoId = cb => (obj) => {
|
|||||||
// Load the DB on boot
|
// Load the DB on boot
|
||||||
localDbSvc.sync()
|
localDbSvc.sync()
|
||||||
.then(() => {
|
.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');
|
store.commit('setReady');
|
||||||
|
|
||||||
// If app was last opened 7 days ago and synchronization is off
|
// If app was last opened 7 days ago and synchronization is off
|
||||||
@ -333,18 +345,10 @@ localDbSvc.sync()
|
|||||||
// If current file has no ID, get the most recent file
|
// If current file has no ID, get the most recent file
|
||||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||||
// If still no ID, create a new file
|
// If still no ID, create a new file
|
||||||
.then(ifNoId(() => {
|
.then(ifNoId(() => store.dispatch('createFile', {
|
||||||
const id = utils.uid();
|
name: 'Welcome file',
|
||||||
store.commit('content/setItem', {
|
text: welcomeFile,
|
||||||
id: `${id}/content`,
|
})))
|
||||||
text: welcomeFile,
|
|
||||||
});
|
|
||||||
store.commit('file/setItem', {
|
|
||||||
id,
|
|
||||||
name: 'Welcome file',
|
|
||||||
});
|
|
||||||
return store.state.file.itemMap[id];
|
|
||||||
}))
|
|
||||||
.then((currentFile) => {
|
.then((currentFile) => {
|
||||||
// Fix current file ID
|
// Fix current file ID
|
||||||
if (store.getters['file/current'].id !== currentFile.id) {
|
if (store.getters['file/current'].id !== currentFile.id) {
|
||||||
|
@ -86,11 +86,7 @@ export default {
|
|||||||
|
|
||||||
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean()
|
msgHandler = event => event.source === wnd && event.origin === utils.origin && clean()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
const data = {};
|
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
||||||
`${event.data}`.slice(1).split('&').forEach((param) => {
|
|
||||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
|
||||||
data[key] = value;
|
|
||||||
});
|
|
||||||
if (data.error || data.state !== state) {
|
if (data.error || data.state !== state) {
|
||||||
reject('Could not get required authorization.');
|
reject('Could not get required authorization.');
|
||||||
} else {
|
} else {
|
||||||
|
@ -114,7 +114,7 @@ export default providerRegistry.register({
|
|||||||
}
|
}
|
||||||
store.commit('file/setItem', {
|
store.commit('file/setItem', {
|
||||||
id,
|
id,
|
||||||
name: name.slice(0, 250),
|
name: utils.sanitizeName(name),
|
||||||
parentId: store.getters['file/current'].parentId,
|
parentId: store.getters['file/current'].parentId,
|
||||||
});
|
});
|
||||||
store.commit('syncLocation/setItem', {
|
store.commit('syncLocation/setItem', {
|
||||||
|
@ -2,8 +2,7 @@ import store from '../../store';
|
|||||||
import githubHelper from './helpers/githubHelper';
|
import githubHelper from './helpers/githubHelper';
|
||||||
import providerUtils from './providerUtils';
|
import providerUtils from './providerUtils';
|
||||||
import providerRegistry from './providerRegistry';
|
import providerRegistry from './providerRegistry';
|
||||||
|
import utils from '../utils';
|
||||||
const defaultDescription = 'Untitled';
|
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default providerRegistry.register({
|
||||||
id: 'gist',
|
id: 'gist',
|
||||||
@ -23,7 +22,7 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation) {
|
uploadContent(token, content, syncLocation) {
|
||||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||||
const description = (file && file.name) || defaultDescription;
|
const description = utils.sanitizeName(file && file.name);
|
||||||
return githubHelper.uploadGist(
|
return githubHelper.uploadGist(
|
||||||
token,
|
token,
|
||||||
description,
|
description,
|
||||||
|
@ -4,8 +4,6 @@ import providerUtils from './providerUtils';
|
|||||||
import providerRegistry from './providerRegistry';
|
import providerRegistry from './providerRegistry';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
|
||||||
const defaultFilename = 'Untitled';
|
|
||||||
|
|
||||||
export default providerRegistry.register({
|
export default providerRegistry.register({
|
||||||
id: 'googleDrive',
|
id: 'googleDrive',
|
||||||
getToken(location) {
|
getToken(location) {
|
||||||
@ -25,7 +23,7 @@ export default providerRegistry.register({
|
|||||||
},
|
},
|
||||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||||
const name = (file && file.name) || defaultFilename;
|
const name = utils.sanitizeName(file && file.name);
|
||||||
const parents = [];
|
const parents = [];
|
||||||
if (syncLocation.driveParentId) {
|
if (syncLocation.driveParentId) {
|
||||||
parents.push(syncLocation.driveParentId);
|
parents.push(syncLocation.driveParentId);
|
||||||
@ -95,7 +93,7 @@ export default providerRegistry.register({
|
|||||||
});
|
});
|
||||||
store.commit('file/setItem', {
|
store.commit('file/setItem', {
|
||||||
id,
|
id,
|
||||||
name: (file.name || defaultFilename).slice(0, 250),
|
name: utils.sanitizeName(file.name),
|
||||||
parentId: store.getters['file/current'].parentId,
|
parentId: store.getters['file/current'].parentId,
|
||||||
});
|
});
|
||||||
store.commit('syncLocation/setItem', {
|
store.commit('syncLocation/setItem', {
|
||||||
|
@ -27,15 +27,26 @@ export default {
|
|||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
parseContent(serializedContent, syncLocation) {
|
parseContent(serializedContent, syncLocation) {
|
||||||
const result = utils.deepCopy(store.state.content.itemMap[`${syncLocation.fileId}/content`]) || emptyContent();
|
const result = utils.deepCopy(store.state.content.itemMap[`${syncLocation.fileId}/content`])
|
||||||
result.text = serializedContent;
|
|| emptyContent();
|
||||||
|
result.text = utils.sanitizeText(serializedContent);
|
||||||
result.history = [];
|
result.history = [];
|
||||||
const extractedData = dataExtractor.exec(serializedContent);
|
const extractedData = dataExtractor.exec(serializedContent);
|
||||||
if (extractedData) {
|
if (extractedData) {
|
||||||
try {
|
try {
|
||||||
const serializedData = extractedData[1].replace(/\s/g, '');
|
const serializedData = extractedData[1].replace(/\s/g, '');
|
||||||
Object.assign(result, JSON.parse(utils.decodeBase64(serializedData)));
|
const parsedData = JSON.parse(utils.decodeBase64(serializedData));
|
||||||
result.text = serializedContent.slice(0, extractedData.index);
|
result.text = utils.sanitizeText(serializedContent.slice(0, extractedData.index));
|
||||||
|
if (parsedData.properties) {
|
||||||
|
result.properties = utils.sanitizeText(parsedData.properties);
|
||||||
|
}
|
||||||
|
if (parsedData.discussions) {
|
||||||
|
result.discussions = parsedData.discussions;
|
||||||
|
}
|
||||||
|
if (parsedData.comments) {
|
||||||
|
result.comments = parsedData.comments;
|
||||||
|
}
|
||||||
|
result.history = parsedData.history || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
|
@ -197,10 +197,12 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update or set content in store
|
// Update or set content in store
|
||||||
delete mergedContent.history;
|
|
||||||
store.commit('content/setItem', {
|
store.commit('content/setItem', {
|
||||||
id: `${fileId}/content`,
|
id: `${fileId}/content`,
|
||||||
...mergedContent,
|
text: utils.sanitizeText(mergedContent.text),
|
||||||
|
properties: utils.sanitizeText(mergedContent.properties),
|
||||||
|
discussions: mergedContent.discussions,
|
||||||
|
comments: mergedContent.comments,
|
||||||
hash: 0,
|
hash: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,12 +33,23 @@ const setLastFocus = () => {
|
|||||||
setLastFocus();
|
setLastFocus();
|
||||||
window.addEventListener('focus', setLastFocus);
|
window.addEventListener('focus', setLastFocus);
|
||||||
|
|
||||||
|
// For parseQueryParams()
|
||||||
|
const parseQueryParams = (params) => {
|
||||||
|
const result = {};
|
||||||
|
params.split('&').forEach((param) => {
|
||||||
|
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||||
|
result[key] = value;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
// For addQueryParams()
|
// For addQueryParams()
|
||||||
const urlParser = window.document.createElement('a');
|
const urlParser = window.document.createElement('a');
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
workspaceId,
|
workspaceId,
|
||||||
origin,
|
origin,
|
||||||
|
queryParams: parseQueryParams(location.hash.slice(1)),
|
||||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||||
lastOpened,
|
lastOpened,
|
||||||
cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
|
cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||||
@ -52,6 +63,15 @@ export default {
|
|||||||
'publishLocation',
|
'publishLocation',
|
||||||
'data',
|
'data',
|
||||||
],
|
],
|
||||||
|
textMaxLength: 150000,
|
||||||
|
sanitizeText(text) {
|
||||||
|
const result = `${text || ''}`.slice(0, this.textMaxLength);
|
||||||
|
// last char must be a `\n`.
|
||||||
|
return `${result}\n`.replace(/\n\n$/, '\n');
|
||||||
|
},
|
||||||
|
sanitizeName(name) {
|
||||||
|
return `${name || ''}`.slice(0, 250) || 'Untitled';
|
||||||
|
},
|
||||||
deepCopy(obj) {
|
deepCopy(obj) {
|
||||||
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
||||||
},
|
},
|
||||||
@ -122,6 +142,7 @@ export default {
|
|||||||
isUserActive() {
|
isUserActive() {
|
||||||
return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused();
|
return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused();
|
||||||
},
|
},
|
||||||
|
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) {
|
if (!keys.length) {
|
||||||
|
@ -46,16 +46,33 @@ const store = new Vuex.Store({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
setOffline: ({ state, commit }, value) => {
|
setOffline: ({ state, commit, dispatch }, value) => {
|
||||||
if (state.offline !== value) {
|
if (state.offline !== value) {
|
||||||
commit('setOffline', value);
|
commit('setOffline', value);
|
||||||
if (state.offline) {
|
if (state.offline) {
|
||||||
return Promise.reject('You are offline.');
|
return Promise.reject('You are offline.');
|
||||||
}
|
}
|
||||||
store.dispatch('notification/info', 'You are back online!');
|
dispatch('notification/info', 'You are back online!');
|
||||||
}
|
}
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
|
createFile({ state, getters, commit }, desc) {
|
||||||
|
const id = utils.uid();
|
||||||
|
commit('content/setItem', {
|
||||||
|
id: `${id}/content`,
|
||||||
|
text: utils.sanitizeText(desc.text || getters['data/computedSettings'].newFileContent),
|
||||||
|
properties: utils.sanitizeText(
|
||||||
|
desc.properties || getters['data/computedSettings'].newFileProperties),
|
||||||
|
discussions: desc.discussions || {},
|
||||||
|
comments: desc.comments || {},
|
||||||
|
});
|
||||||
|
commit('file/setItem', {
|
||||||
|
id,
|
||||||
|
name: utils.sanitizeName(desc.name),
|
||||||
|
parentId: desc.parentId || null,
|
||||||
|
});
|
||||||
|
return Promise.resolve(state.file.itemMap[id]);
|
||||||
|
},
|
||||||
deleteFile({ getters, commit }, fileId) {
|
deleteFile({ getters, commit }, fileId) {
|
||||||
commit('file/deleteItem', fileId);
|
commit('file/deleteItem', fileId);
|
||||||
commit('content/deleteItem', `${fileId}/content`);
|
commit('content/deleteItem', `${fileId}/content`);
|
||||||
|
@ -131,20 +131,27 @@ module.actions.setTemplates = ({ commit }, data) => {
|
|||||||
|
|
||||||
// Last opened
|
// Last opened
|
||||||
module.getters.lastOpened = getter('lastOpened');
|
module.getters.lastOpened = getter('lastOpened');
|
||||||
const getLastOpenedIds = (lastOpened, rootState) => Object.keys(lastOpened)
|
module.getters.lastOpenedIds = (state, getters, rootState) => {
|
||||||
.filter(id => rootState.file.itemMap[id])
|
const lastOpened = getters.lastOpened;
|
||||||
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
return Object.keys(lastOpened)
|
||||||
.slice(0, 20);
|
.filter(id => rootState.file.itemMap[id])
|
||||||
module.getters.lastOpenedIds = (state, getters, rootState) =>
|
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
||||||
getLastOpenedIds(getters.lastOpened, rootState);
|
.slice(0, 20);
|
||||||
module.actions.setLastOpenedId = ({ getters, commit, rootState }, fileId) => {
|
};
|
||||||
|
module.actions.setLastOpenedId = ({ getters, commit, dispatch, rootState }, fileId) => {
|
||||||
const lastOpened = { ...getters.lastOpened };
|
const lastOpened = { ...getters.lastOpened };
|
||||||
lastOpened[fileId] = Date.now();
|
lastOpened[fileId] = Date.now();
|
||||||
const filteredLastOpened = {};
|
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||||
getLastOpenedIds(lastOpened, rootState)
|
dispatch('cleanLastOpenedId');
|
||||||
.forEach((id) => {
|
};
|
||||||
filteredLastOpened[id] = lastOpened[id];
|
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));
|
commit('setItem', itemTemplate('lastOpened', lastOpened));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -5,7 +5,7 @@ const editorTopPadding = 10;
|
|||||||
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
|
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
|
||||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||||
const navigationBarRightButtonWidth = 38 + 8;
|
const navigationBarRightButtonWidth = 38 + 8;
|
||||||
const navigationBarSpinnerWidth = 22 + 8 + 5; // 5 for left margin
|
const navigationBarSpinnerWidth = 24 + 8 + 5; // 5 for left margin
|
||||||
const navigationBarLocationWidth = 20;
|
const navigationBarLocationWidth = 20;
|
||||||
const navigationBarSyncPublishButtonsWidth = 36 + 10;
|
const navigationBarSyncPublishButtonsWidth = 36 + 10;
|
||||||
const navigationBarTitleMargin = 8;
|
const navigationBarTitleMargin = 8;
|
||||||
@ -95,17 +95,16 @@ function computeStyles(state, localSettings, getters, styles = {
|
|||||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||||
|
|
||||||
styles.titleMaxWidth = styles.innerWidth;
|
styles.titleMaxWidth = styles.innerWidth -
|
||||||
|
navigationBarLeftButtonWidth -
|
||||||
|
navigationBarRightButtonWidth -
|
||||||
|
navigationBarSpinnerWidth;
|
||||||
if (styles.showEditor) {
|
if (styles.showEditor) {
|
||||||
const syncLocations = getters['syncLocation/current'];
|
const syncLocations = getters['syncLocation/current'];
|
||||||
const publishLocations = getters['publishLocation/current'];
|
const publishLocations = getters['publishLocation/current'];
|
||||||
styles.titleMaxWidth = styles.innerWidth -
|
styles.titleMaxWidth -= navigationBarEditButtonsWidth +
|
||||||
navigationBarEditButtonsWidth -
|
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) +
|
||||||
navigationBarLeftButtonWidth -
|
(navigationBarSyncPublishButtonsWidth * 2) +
|
||||||
navigationBarRightButtonWidth -
|
|
||||||
navigationBarSpinnerWidth -
|
|
||||||
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
|
||||||
(navigationBarSyncPublishButtonsWidth * 2) -
|
|
||||||
navigationBarTitleMargin;
|
navigationBarTitleMargin;
|
||||||
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
||||||
styles.hideLocations = true;
|
styles.hideLocations = true;
|
||||||
|
Loading…
Reference in New Issue
Block a user