Added backup menu. Added print menu. Fixed splash screen.

This commit is contained in:
benweet 2017-10-09 08:11:18 +01:00
parent 3e9b75d3e8
commit e15b9fae16
26 changed files with 317 additions and 93 deletions

View File

@ -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]);
} }
} }
}); });

View File

@ -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),
}); });
} else {
// Add empty line at the end if needed
const ensureFinalNewLine = text => `${text}\n`.replace(/\n\n$/, '\n');
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.select(id);
} else {
this.$store.dispatch('createFile', newChildNode.item)
.then(file => this.select(file.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);

View File

@ -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);
} }
} }

View File

@ -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>

View File

@ -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
}

View File

@ -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>

View File

@ -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>

View File

@ -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(

View File

@ -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();
} }

View File

@ -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
View 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>

View File

@ -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>

View File

@ -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
View 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],
}));
},
};

View File

@ -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) {

View File

@ -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();
store.commit('content/setItem', {
id: `${id}/content`,
text: welcomeFile,
});
store.commit('file/setItem', {
id,
name: 'Welcome file', name: 'Welcome file',
}); text: welcomeFile,
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) {

View File

@ -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 {

View File

@ -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', {

View File

@ -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,

View File

@ -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', {

View File

@ -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
} }

View File

@ -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,
}); });

View File

@ -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) {

View File

@ -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`);

View File

@ -131,19 +131,26 @@ 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) => {
const lastOpened = getters.lastOpened;
return Object.keys(lastOpened)
.filter(id => rootState.file.itemMap[id]) .filter(id => rootState.file.itemMap[id])
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1]) .sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
.slice(0, 20); .slice(0, 20);
module.getters.lastOpenedIds = (state, getters, rootState) => };
getLastOpenedIds(getters.lastOpened, rootState); module.actions.setLastOpenedId = ({ getters, commit, dispatch, rootState }, fileId) => {
module.actions.setLastOpenedId = ({ getters, commit, 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));
}; };

View File

@ -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;