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) {
|
||||
let parentId = this.$store.getters['explorer/selectedNodeFolder'].item.id;
|
||||
if (parentId === 'trash') {
|
||||
parentId = undefined;
|
||||
parentId = null;
|
||||
}
|
||||
this.$store.dispatch('explorer/openNode', parentId);
|
||||
this.$store.commit('explorer/setNewItem', {
|
||||
@ -92,6 +92,7 @@ export default {
|
||||
id: selectedNode.item.id,
|
||||
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;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit('explorer/setNewItemName', value && value.slice(0, 250));
|
||||
this.$store.commit('explorer/setNewItemName', value);
|
||||
},
|
||||
},
|
||||
editingNodeName: {
|
||||
@ -91,28 +91,18 @@ export default {
|
||||
submitNewChild(cancel) {
|
||||
const newChildNode = this.$store.state.explorer.newChildNode;
|
||||
if (!cancel && !newChildNode.isNil && newChildNode.item.name) {
|
||||
const id = utils.uid();
|
||||
if (newChildNode.isFolder) {
|
||||
const id = utils.uid();
|
||||
this.$store.commit('folder/setItem', {
|
||||
...newChildNode.item,
|
||||
id,
|
||||
name: utils.sanitizeName(newChildNode.item.name),
|
||||
});
|
||||
this.select(id);
|
||||
} 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.$store.dispatch('createFile', newChildNode.item)
|
||||
.then(file => this.select(file.id));
|
||||
}
|
||||
this.select(id);
|
||||
}
|
||||
this.$store.commit('explorer/setNewItem', null);
|
||||
},
|
||||
@ -123,7 +113,7 @@ export default {
|
||||
if (!cancel && id && value) {
|
||||
this.$store.commit(editingNode.isFolder ? 'folder/patchItem' : 'file/patchItem', {
|
||||
id,
|
||||
name: value.slice(0, 250),
|
||||
name: utils.sanitizeName(value),
|
||||
});
|
||||
}
|
||||
this.$store.commit('explorer/setEditingId', null);
|
||||
|
@ -80,6 +80,7 @@ import editorSvc from '../services/editorSvc';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import publishSvc from '../services/publishSvc';
|
||||
import animationSvc from '../services/animationSvc';
|
||||
import utils from '../services/utils';
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
@ -171,7 +172,7 @@ export default {
|
||||
} else {
|
||||
const title = this.title.trim();
|
||||
if (title) {
|
||||
this.$store.dispatch('file/patchCurrent', { name: title.slice(0, 250) });
|
||||
this.$store.dispatch('file/patchCurrent', { name: utils.sanitizeName(title) });
|
||||
} else {
|
||||
this.title = this.$store.getters['file/current'].name;
|
||||
}
|
||||
@ -342,9 +343,7 @@ export default {
|
||||
}
|
||||
|
||||
.navigation-bar__title--input,
|
||||
.navigation-bar__inner--edit-buttons,
|
||||
.navigation-bar__inner--button,
|
||||
.navigation-bar__spinner {
|
||||
.navigation-bar__inner--edit-buttons {
|
||||
display: none;
|
||||
|
||||
.navigation-bar--editor & {
|
||||
@ -355,6 +354,7 @@ export default {
|
||||
.navigation-bar__button {
|
||||
display: none;
|
||||
|
||||
.navigation-bar__inner--button &,
|
||||
.navigation-bar--editor & {
|
||||
display: inline-block;
|
||||
}
|
||||
@ -374,13 +374,13 @@ $b: $d/10;
|
||||
$t: 3000ms;
|
||||
|
||||
.navigation-bar__spinner {
|
||||
width: 22px;
|
||||
width: 24px;
|
||||
margin: 7px 0 0 8px;
|
||||
color: #b2b2b2;
|
||||
|
||||
.icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
color: transparentize($error-color, 0.5);
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="splash-screen">
|
||||
<div class="splash-screen__inner background-logo"></div>
|
||||
<div class="splash-screen__inner logo-background"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -81,7 +81,8 @@ textarea {
|
||||
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
&:hover,
|
||||
.hidden-file:focus + & {
|
||||
color: #333;
|
||||
background-color: rgba(0, 0, 0, 0.067);
|
||||
outline: 0;
|
||||
@ -193,3 +194,41 @@ textarea {
|
||||
background: no-repeat center url('../assets/logo.svg');
|
||||
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
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="importFile">
|
||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||
Import from disk
|
||||
<menu-entry @click.native="print">
|
||||
<icon-printer slot="icon"></icon-printer>
|
||||
Print
|
||||
</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')">
|
||||
<icon-hard-disk slot="icon"></icon-hard-disk>
|
||||
Export to disk
|
||||
@ -58,6 +67,7 @@ import MenuEntry from './MenuEntry';
|
||||
import UserImage from '../UserImage';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import providerUtils from '../../services/providers/providerUtils';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -80,13 +90,32 @@ export default {
|
||||
() => {}, // Cancel
|
||||
);
|
||||
},
|
||||
importFile() {
|
||||
return this.$store.dispatch('modal/notImplemented');
|
||||
onImportFile(evt) {
|
||||
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() {
|
||||
return this.$store.dispatch('modal/open', 'fileProperties')
|
||||
.catch(() => {}); // Cancel
|
||||
},
|
||||
print() {
|
||||
print();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -37,4 +37,9 @@
|
||||
border-radius: $border-radius-base;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.hidden-file {
|
||||
position: fixed;
|
||||
top: -999px;
|
||||
}
|
||||
</style>
|
||||
|
@ -16,6 +16,20 @@
|
||||
<span>Sign out and clean local data.</span>
|
||||
</menu-entry>
|
||||
<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">
|
||||
<icon-help-circle slot="icon"></icon-help-circle>
|
||||
<span>About StackEdit</span>
|
||||
@ -34,12 +48,29 @@
|
||||
<script>
|
||||
import MenuEntry from './MenuEntry';
|
||||
import localDbSvc from '../../services/localDbSvc';
|
||||
import backupSvc from '../../services/backupSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MenuEntry,
|
||||
},
|
||||
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() {
|
||||
return this.$store.dispatch('modal/open', 'settings')
|
||||
.then(
|
||||
|
@ -35,6 +35,7 @@ import yaml from 'js-yaml';
|
||||
import { mapGetters } from 'vuex';
|
||||
import Tab from './Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import utils from '../../services/utils';
|
||||
import defaultProperties from '../../data/defaultFileProperties.yml';
|
||||
|
||||
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) {
|
||||
this.$store.commit('content/patchItem', {
|
||||
id: this.contentId,
|
||||
properties: this.strippedCustomProperties,
|
||||
properties: utils.sanitizeText(this.strippedCustomProperties),
|
||||
});
|
||||
this.config.resolve();
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ export default {
|
||||
submitEdit(cancel) {
|
||||
const template = this.templates[this.selectedId];
|
||||
if (!cancel && this.editingName) {
|
||||
template.name = this.editingName.slice(0, 250);
|
||||
template.name = utils.sanitizeName(this.editingName);
|
||||
} else {
|
||||
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>
|
||||
<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>
|
||||
</template>
|
||||
|
@ -42,6 +42,7 @@ import Alert from './Alert';
|
||||
import SignalOff from './SignalOff';
|
||||
import Folder from './Folder';
|
||||
import ScrollSync from './ScrollSync';
|
||||
import Printer from './Printer';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
@ -86,3 +87,4 @@ Vue.component('iconAlert', Alert);
|
||||
Vue.component('iconSignalOff', SignalOff);
|
||||
Vue.component('iconFolder', Folder);
|
||||
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) => {
|
||||
const oldContent = store.getters['content/current'];
|
||||
const newContent = {
|
||||
...oldContent,
|
||||
discussions: utils.deepCopy(oldContent.discussions),
|
||||
text,
|
||||
...utils.deepCopy(oldContent),
|
||||
text: utils.sanitizeText(text),
|
||||
};
|
||||
syncDiscussionMarkers(newContent, true);
|
||||
if (!isChangePatch) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'babel-polyfill';
|
||||
import 'indexeddbshim/dist/indexeddbshim';
|
||||
import FileSaver from 'file-saver';
|
||||
import utils from './utils';
|
||||
import store from '../store';
|
||||
import welcomeFile from '../data/welcomeFile.md';
|
||||
@ -8,6 +9,7 @@ const indexedDB = window.indexedDB;
|
||||
const dbVersion = 1;
|
||||
const dbVersionKey = `${utils.workspaceId}/localDbVersion`;
|
||||
const dbStoreName = 'objects';
|
||||
const exportBackup = utils.queryParams.exportBackup;
|
||||
|
||||
if (!indexedDB) {
|
||||
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
|
||||
this.hashMap[dbItem.type][dbItem.id] = dbItem.hash;
|
||||
// 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
|
||||
dbItem.tx = undefined;
|
||||
store.commit(`${dbItem.type}/setItem`, dbItem);
|
||||
@ -314,6 +316,16 @@ const ifNoId = cb => (obj) => {
|
||||
// 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');
|
||||
|
||||
// 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
|
||||
.then(ifNoId(() => store.getters['file/lastOpened']))
|
||||
// If still no ID, create a new file
|
||||
.then(ifNoId(() => {
|
||||
const id = utils.uid();
|
||||
store.commit('content/setItem', {
|
||||
id: `${id}/content`,
|
||||
text: welcomeFile,
|
||||
});
|
||||
store.commit('file/setItem', {
|
||||
id,
|
||||
name: 'Welcome file',
|
||||
});
|
||||
return store.state.file.itemMap[id];
|
||||
}))
|
||||
.then(ifNoId(() => store.dispatch('createFile', {
|
||||
name: 'Welcome file',
|
||||
text: welcomeFile,
|
||||
})))
|
||||
.then((currentFile) => {
|
||||
// Fix current file 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()
|
||||
.then(() => {
|
||||
const data = {};
|
||||
`${event.data}`.slice(1).split('&').forEach((param) => {
|
||||
const [key, value] = param.split('=').map(decodeURIComponent);
|
||||
data[key] = value;
|
||||
});
|
||||
const data = utils.parseQueryParams(`${event.data}`.slice(1));
|
||||
if (data.error || data.state !== state) {
|
||||
reject('Could not get required authorization.');
|
||||
} else {
|
||||
|
@ -114,7 +114,7 @@ export default providerRegistry.register({
|
||||
}
|
||||
store.commit('file/setItem', {
|
||||
id,
|
||||
name: name.slice(0, 250),
|
||||
name: utils.sanitizeName(name),
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
});
|
||||
store.commit('syncLocation/setItem', {
|
||||
|
@ -2,8 +2,7 @@ import store from '../../store';
|
||||
import githubHelper from './helpers/githubHelper';
|
||||
import providerUtils from './providerUtils';
|
||||
import providerRegistry from './providerRegistry';
|
||||
|
||||
const defaultDescription = 'Untitled';
|
||||
import utils from '../utils';
|
||||
|
||||
export default providerRegistry.register({
|
||||
id: 'gist',
|
||||
@ -23,7 +22,7 @@ export default providerRegistry.register({
|
||||
},
|
||||
uploadContent(token, content, syncLocation) {
|
||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||
const description = (file && file.name) || defaultDescription;
|
||||
const description = utils.sanitizeName(file && file.name);
|
||||
return githubHelper.uploadGist(
|
||||
token,
|
||||
description,
|
||||
|
@ -4,8 +4,6 @@ import providerUtils from './providerUtils';
|
||||
import providerRegistry from './providerRegistry';
|
||||
import utils from '../utils';
|
||||
|
||||
const defaultFilename = 'Untitled';
|
||||
|
||||
export default providerRegistry.register({
|
||||
id: 'googleDrive',
|
||||
getToken(location) {
|
||||
@ -25,7 +23,7 @@ export default providerRegistry.register({
|
||||
},
|
||||
uploadContent(token, content, syncLocation, ifNotTooLate) {
|
||||
const file = store.state.file.itemMap[syncLocation.fileId];
|
||||
const name = (file && file.name) || defaultFilename;
|
||||
const name = utils.sanitizeName(file && file.name);
|
||||
const parents = [];
|
||||
if (syncLocation.driveParentId) {
|
||||
parents.push(syncLocation.driveParentId);
|
||||
@ -95,7 +93,7 @@ export default providerRegistry.register({
|
||||
});
|
||||
store.commit('file/setItem', {
|
||||
id,
|
||||
name: (file.name || defaultFilename).slice(0, 250),
|
||||
name: utils.sanitizeName(file.name),
|
||||
parentId: store.getters['file/current'].parentId,
|
||||
});
|
||||
store.commit('syncLocation/setItem', {
|
||||
|
@ -27,15 +27,26 @@ export default {
|
||||
return result;
|
||||
},
|
||||
parseContent(serializedContent, syncLocation) {
|
||||
const result = utils.deepCopy(store.state.content.itemMap[`${syncLocation.fileId}/content`]) || emptyContent();
|
||||
result.text = serializedContent;
|
||||
const result = utils.deepCopy(store.state.content.itemMap[`${syncLocation.fileId}/content`])
|
||||
|| emptyContent();
|
||||
result.text = utils.sanitizeText(serializedContent);
|
||||
result.history = [];
|
||||
const extractedData = dataExtractor.exec(serializedContent);
|
||||
if (extractedData) {
|
||||
try {
|
||||
const serializedData = extractedData[1].replace(/\s/g, '');
|
||||
Object.assign(result, JSON.parse(utils.decodeBase64(serializedData)));
|
||||
result.text = serializedContent.slice(0, extractedData.index);
|
||||
const parsedData = JSON.parse(utils.decodeBase64(serializedData));
|
||||
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) {
|
||||
// Ignore
|
||||
}
|
||||
|
@ -197,10 +197,12 @@ function syncFile(fileId, needSyncRestartParam = false) {
|
||||
}
|
||||
|
||||
// Update or set content in store
|
||||
delete mergedContent.history;
|
||||
store.commit('content/setItem', {
|
||||
id: `${fileId}/content`,
|
||||
...mergedContent,
|
||||
text: utils.sanitizeText(mergedContent.text),
|
||||
properties: utils.sanitizeText(mergedContent.properties),
|
||||
discussions: mergedContent.discussions,
|
||||
comments: mergedContent.comments,
|
||||
hash: 0,
|
||||
});
|
||||
|
||||
|
@ -33,12 +33,23 @@ const setLastFocus = () => {
|
||||
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()
|
||||
const urlParser = window.document.createElement('a');
|
||||
|
||||
export default {
|
||||
workspaceId,
|
||||
origin,
|
||||
queryParams: parseQueryParams(location.hash.slice(1)),
|
||||
oauth2RedirectUri: `${origin}/oauth2/callback`,
|
||||
lastOpened,
|
||||
cleanTrashAfter: 7 * 24 * 60 * 60 * 1000, // 7 days
|
||||
@ -52,6 +63,15 @@ export default {
|
||||
'publishLocation',
|
||||
'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) {
|
||||
return obj == null ? obj : JSON.parse(JSON.stringify(obj));
|
||||
},
|
||||
@ -122,6 +142,7 @@ export default {
|
||||
isUserActive() {
|
||||
return lastActivity > Date.now() - inactiveAfter && this.isWindowFocused();
|
||||
},
|
||||
parseQueryParams,
|
||||
addQueryParams(url = '', params = {}) {
|
||||
const keys = Object.keys(params).filter(key => params[key] != null);
|
||||
if (!keys.length) {
|
||||
|
@ -46,16 +46,33 @@ const store = new Vuex.Store({
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setOffline: ({ state, commit }, value) => {
|
||||
setOffline: ({ state, commit, dispatch }, value) => {
|
||||
if (state.offline !== value) {
|
||||
commit('setOffline', value);
|
||||
if (state.offline) {
|
||||
return Promise.reject('You are offline.');
|
||||
}
|
||||
store.dispatch('notification/info', 'You are back online!');
|
||||
dispatch('notification/info', 'You are back online!');
|
||||
}
|
||||
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) {
|
||||
commit('file/deleteItem', fileId);
|
||||
commit('content/deleteItem', `${fileId}/content`);
|
||||
|
@ -131,20 +131,27 @@ module.actions.setTemplates = ({ commit }, data) => {
|
||||
|
||||
// Last opened
|
||||
module.getters.lastOpened = getter('lastOpened');
|
||||
const getLastOpenedIds = (lastOpened, rootState) => Object.keys(lastOpened)
|
||||
.filter(id => rootState.file.itemMap[id])
|
||||
.sort((id1, id2) => lastOpened[id2] - lastOpened[id1])
|
||||
.slice(0, 20);
|
||||
module.getters.lastOpenedIds = (state, getters, rootState) =>
|
||||
getLastOpenedIds(getters.lastOpened, rootState);
|
||||
module.actions.setLastOpenedId = ({ getters, commit, rootState }, fileId) => {
|
||||
module.getters.lastOpenedIds = (state, getters, rootState) => {
|
||||
const lastOpened = getters.lastOpened;
|
||||
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();
|
||||
const filteredLastOpened = {};
|
||||
getLastOpenedIds(lastOpened, rootState)
|
||||
.forEach((id) => {
|
||||
filteredLastOpened[id] = lastOpened[id];
|
||||
});
|
||||
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));
|
||||
};
|
||||
|
||||
|
@ -5,7 +5,7 @@ const editorTopPadding = 10;
|
||||
const navigationBarEditButtonsWidth = 36 * 12; // 12 buttons
|
||||
const navigationBarLeftButtonWidth = 38 + 4 + 15;
|
||||
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 navigationBarSyncPublishButtonsWidth = 36 + 10;
|
||||
const navigationBarTitleMargin = 8;
|
||||
@ -95,17 +95,16 @@ function computeStyles(state, localSettings, getters, styles = {
|
||||
Math.floor((styles.editorWidth - styles.textWidth) / 2), minPadding);
|
||||
styles.editorPadding = `${editorTopPadding}px ${editorSidePadding}px ${bottomPadding}px`;
|
||||
|
||||
styles.titleMaxWidth = styles.innerWidth;
|
||||
styles.titleMaxWidth = styles.innerWidth -
|
||||
navigationBarLeftButtonWidth -
|
||||
navigationBarRightButtonWidth -
|
||||
navigationBarSpinnerWidth;
|
||||
if (styles.showEditor) {
|
||||
const syncLocations = getters['syncLocation/current'];
|
||||
const publishLocations = getters['publishLocation/current'];
|
||||
styles.titleMaxWidth = styles.innerWidth -
|
||||
navigationBarEditButtonsWidth -
|
||||
navigationBarLeftButtonWidth -
|
||||
navigationBarRightButtonWidth -
|
||||
navigationBarSpinnerWidth -
|
||||
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) -
|
||||
(navigationBarSyncPublishButtonsWidth * 2) -
|
||||
styles.titleMaxWidth -= navigationBarEditButtonsWidth +
|
||||
(navigationBarLocationWidth * (syncLocations.length + publishLocations.length)) +
|
||||
(navigationBarSyncPublishButtonsWidth * 2) +
|
||||
navigationBarTitleMargin;
|
||||
if (styles.titleMaxWidth + navigationBarEditButtonsWidth < minTitleMaxWidth) {
|
||||
styles.hideLocations = true;
|
||||
|
Loading…
Reference in New Issue
Block a user