Reorganized side bar menu

This commit is contained in:
Benoit Schweblin 2019-06-18 13:49:48 +01:00
parent 8cf0b87f5f
commit 2a865ddb44
11 changed files with 173 additions and 260 deletions

View File

@ -2,7 +2,7 @@
<div class="side-bar flex flex--column">
<div class="side-title flex flex--row">
<button v-if="panel !== 'menu'" class="side-title__button button" @click="setPanel('menu')" v-title="'Main menu'">
<icon-arrow-left></icon-arrow-left>
<icon-dots-horizontal></icon-dots-horizontal>
</button>
<div class="side-title__title">
{{panelName}}
@ -18,8 +18,8 @@
<publish-menu v-else-if="panel === 'publish'"></publish-menu>
<history-menu v-else-if="panel === 'history'"></history-menu>
<export-menu v-else-if="panel === 'export'"></export-menu>
<import-menu v-else-if="panel === 'import'"></import-menu>
<more-menu v-else-if="panel === 'more'"></more-menu>
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
<workspace-backup-menu v-else-if="panel === 'workspaceBackup'"></workspace-backup-menu>
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
</div>
@ -39,9 +39,8 @@ import WorkspacesMenu from './menus/WorkspacesMenu';
import SyncMenu from './menus/SyncMenu';
import PublishMenu from './menus/PublishMenu';
import HistoryMenu from './menus/HistoryMenu';
import ExportMenu from './menus/ExportMenu';
import ImportMenu from './menus/ImportMenu';
import MoreMenu from './menus/MoreMenu';
import ImportExportMenu from './menus/ImportExportMenu';
import WorkspaceBackupMenu from './menus/WorkspaceBackupMenu';
import markdownSample from '../data/markdownSample.md';
import markdownConversionSvc from '../services/markdownConversionSvc';
import store from '../store';
@ -54,8 +53,8 @@ const panelNames = {
sync: 'Synchronize',
publish: 'Publish',
history: 'File history',
export: 'Export to disk',
import: 'Import from disk',
importExport: 'Import/export',
workspaceBackup: 'Workspace backup',
};
export default {
@ -66,9 +65,8 @@ export default {
SyncMenu,
PublishMenu,
HistoryMenu,
ExportMenu,
ImportMenu,
MoreMenu,
ImportExportMenu,
WorkspaceBackupMenu,
},
data: () => ({
markdownSample: markdownConversionSvc.highlight(markdownSample),

View File

@ -1,57 +0,0 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<menu-entry @click.native="exportMarkdown">
<icon-download slot="icon"></icon-download>
<div>Export as Markdown</div>
<span>Save plain text file.</span>
</menu-entry>
<menu-entry @click.native="exportHtml">
<icon-download slot="icon"></icon-download>
<div>Export as HTML</div>
<span>Generate an HTML page from a template.</span>
</menu-entry>
<menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
<span>Produce a PDF from an HTML template.</span>
</menu-entry>
<menu-entry @click.native="exportPandoc">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
<span>Convert to PDF, Word, EPUB...</span>
</menu-entry>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import MenuEntry from './common/MenuEntry';
import exportSvc from '../../services/exportSvc';
import store from '../../store';
export default {
components: {
MenuEntry,
},
computed: mapGetters(['isSponsor']),
methods: {
exportMarkdown() {
const currentFile = store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => { /* Cancel */ });
},
exportHtml() {
return store.dispatch('modal/open', 'htmlExport')
.catch(() => { /* Cancel */ });
},
exportPdf() {
return store.dispatch('modal/open', 'pdfExport')
.catch(() => { /* Cancel */ });
},
exportPandoc() {
return store.dispatch('modal/open', 'pandocExport')
.catch(() => { /* Cancel */ });
},
},
};
</script>

View File

@ -20,16 +20,39 @@
<span>Convert an HTML file to Markdown.</span>
</div>
</label>
<hr>
<menu-entry @click.native="exportMarkdown">
<icon-download slot="icon"></icon-download>
<div>Export as Markdown</div>
<span>Save plain text file.</span>
</menu-entry>
<menu-entry @click.native="exportHtml">
<icon-download slot="icon"></icon-download>
<div>Export as HTML</div>
<span>Generate an HTML page from a template.</span>
</menu-entry>
<menu-entry @click.native="exportPdf">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export as PDF</div>
<span>Produce a PDF from an HTML template.</span>
</menu-entry>
<menu-entry @click.native="exportPandoc">
<icon-download slot="icon"></icon-download>
<div><div class="menu-entry__label" :class="{'menu-entry__label--warning': !isSponsor}">sponsor</div> Export with Pandoc</div>
<span>Convert to PDF, Word, EPUB...</span>
</menu-entry>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import TurndownService from 'turndown/lib/turndown.browser.umd';
import htmlSanitizer from '../../libs/htmlSanitizer';
import MenuEntry from './common/MenuEntry';
import Provider from '../../services/providers/common/Provider';
import store from '../../store';
import workspaceSvc from '../../services/workspaceSvc';
import exportSvc from '../../services/exportSvc';
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
@ -52,6 +75,7 @@ export default {
components: {
MenuEntry,
},
computed: mapGetters(['isSponsor']),
methods: {
async onImportMarkdown(evt) {
const file = evt.target.files[0];
@ -73,6 +97,23 @@ export default {
});
store.commit('file/setCurrentId', item.id);
},
exportMarkdown() {
const currentFile = store.getters['file/current'];
return exportSvc.exportToDisk(currentFile.id, 'md')
.catch(() => { /* Cancel */ });
},
exportHtml() {
return store.dispatch('modal/open', 'htmlExport')
.catch(() => { /* Cancel */ });
},
exportPdf() {
return store.dispatch('modal/open', 'pdfExport')
.catch(() => { /* Cancel */ });
},
exportPandoc() {
return store.dispatch('modal/open', 'pandocExport')
.catch(() => { /* Cancel */ });
},
},
};
</script>

View File

@ -75,13 +75,9 @@
Markdown cheat sheet
</menu-entry>
<hr>
<menu-entry @click.native="setPanel('import')">
<menu-entry @click.native="setPanel('importExport')">
<icon-content-save slot="icon"></icon-content-save>
Import from disk
</menu-entry>
<menu-entry @click.native="setPanel('export')">
<icon-content-save slot="icon"></icon-content-save>
Export to disk
Import/export
</menu-entry>
<menu-entry @click.native="print">
<icon-printer slot="icon"></icon-printer>
@ -104,19 +100,10 @@
<span>Manage access to your external accounts.</span>
</menu-entry>
<hr>
<menu-entry @click.native="exportWorkspace">
<menu-entry @click.native="setPanel('workspaceBackup')">
<icon-content-save slot="icon"></icon-content-save>
Export workspace backup
Workspace backup
</menu-entry>
<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-content-save></icon-content-save>
</div>
<div class="flex flex--column">
Import workspace backup
</div>
</label>
<menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout>
<div>Reset application</div>
@ -138,8 +125,6 @@ import UserImage from '../UserImage';
import googleHelper from '../../services/providers/helpers/googleHelper';
import syncSvc from '../../services/syncSvc';
import userSvc from '../../services/userSvc';
import backupSvc from '../../services/backupSvc';
import utils from '../../services/utils';
import store from '../../store';
export default {
@ -199,29 +184,6 @@ export default {
print() {
window.print();
},
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/)) {
store.dispatch('notification/error', 'File is not readable.');
} else {
backupSvc.importBackup(text);
}
};
const blob = file.slice(0, 10000000);
reader.readAsText(blob);
}
},
exportWorkspace() {
window.location.href = utils.addQueryParams('app', {
...utils.queryParams,
exportWorkspace: true,
}, true);
window.location.reload();
},
async settings() {
try {
const settings = await store.dispatch('modal/open', 'settings');

View File

@ -1,114 +0,0 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<menu-entry @click.native="settings">
<icon-settings slot="icon"></icon-settings>
<div>Settings</div>
<span>Tweak application and keyboard shortcuts.</span>
</menu-entry>
<menu-entry @click.native="templates">
<icon-code-braces slot="icon"></icon-code-braces>
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
<span>Configure Handlebars templates for your exports.</span>
</menu-entry>
<menu-entry @click.native="reset">
<icon-logout slot="icon"></icon-logout>
<div>Reset application</div>
<span>Sign out and clean all workspaces.</span>
</menu-entry>
<hr>
<menu-entry @click.native="exportWorkspace">
<icon-content-save slot="icon"></icon-content-save>
Export workspace backup
</menu-entry>
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
<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-content-save></icon-content-save>
</div>
<div class="flex flex--column">
Import workspace backup
</div>
</label>
<hr>
<menu-entry href="editor" target="_blank">
<icon-open-in-new slot="icon"></icon-open-in-new>
<span>StackEdit 4 &mdash; deprecated</span>
</menu-entry>
<menu-entry @click.native="about">
<icon-help-circle slot="icon"></icon-help-circle>
<span>About StackEdit</span>
</menu-entry>
</div>
</template>
<script>
import MenuEntry from './common/MenuEntry';
import backupSvc from '../../services/backupSvc';
import utils from '../../services/utils';
import store from '../../store';
export default {
components: {
MenuEntry,
},
computed: {
templateCount() {
return Object.keys(store.getters['data/allTemplatesById']).length;
},
},
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/)) {
store.dispatch('notification/error', 'File is not readable.');
} else {
backupSvc.importBackup(text);
}
};
const blob = file.slice(0, 10000000);
reader.readAsText(blob);
}
},
exportWorkspace() {
const url = utils.addQueryParams('app', {
...utils.queryParams,
exportWorkspace: true,
}, true);
window.location.href = url;
window.location.reload(true);
},
async settings() {
try {
const settings = await store.dispatch('modal/open', 'settings');
store.dispatch('data/setSettings', settings);
} catch (e) {
// Cancel
}
},
async templates() {
try {
const { templates } = await store.dispatch('modal/open', 'templates');
store.dispatch('data/setTemplatesById', templates);
} catch (e) {
// Cancel
}
},
async reset() {
try {
await store.dispatch('modal/open', 'reset');
window.location.href = '#reset=true';
window.location.reload();
} catch (e) {
// Cancel
}
},
about() {
store.dispatch('modal/open', 'about');
},
},
};
</script>

View File

@ -21,7 +21,7 @@
</menu-entry>
</div>
<hr>
<div v-for="token in bloggerTokens" :key="token.sub">
<div v-for="token in bloggerTokens" :key="'blogger-' + token.sub">
<menu-entry @click.native="publishBlogger(token)">
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
<div>Publish to Blogger</div>

View File

@ -0,0 +1,55 @@
<template>
<div class="side-bar__panel side-bar__panel--menu">
<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-content-save></icon-content-save>
</div>
<div class="flex flex--column">
Import workspace backup
</div>
</label>
<menu-entry @click.native="exportWorkspace">
<icon-content-save slot="icon"></icon-content-save>
Export workspace backup
</menu-entry>
</div>
</template>
<script>
import MenuEntry from './common/MenuEntry';
import utils from '../../services/utils';
import store from '../../store';
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/)) {
store.dispatch('notification/error', 'File is not readable.');
} else {
backupSvc.importBackup(text);
}
};
const blob = file.slice(0, 10000000);
reader.readAsText(blob);
}
},
exportWorkspace() {
window.location.href = utils.addQueryParams('app', {
...utils.queryParams,
exportWorkspace: true,
}, true);
window.location.reload();
},
},
};
</script>

View File

@ -0,0 +1,5 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
<path d="M 16,12C 16,10.8954 16.8954,10 18,10C 19.1046,10 20,10.8954 20,12C 20,13.1046 19.1046,14 18,14C 16.8954,14 16,13.1046 16,12 Z M 10,12C 10,10.8954 10.8954,10 12,10C 13.1046,10 14,10.8954 14,12C 14,13.1046 13.1046,14 12,14C 10.8954,14 10,13.1046 10,12 Z M 4,12C 4,10.8954 4.89543,10 6,10C 7.10457,10 8,10.8954 8,12C 8,13.1046 7.10457,14 6,14C 4.89543,14 4,13.1046 4,12 Z "/>
</svg>
</template>

View File

@ -52,6 +52,7 @@ import FormatListChecks from './FormatListChecks';
import CheckCircle from './CheckCircle';
import ContentCopy from './ContentCopy';
import Key from './Key';
import DotsHorizontal from './DotsHorizontal';
Vue.component('iconProvider', Provider);
Vue.component('iconFormatBold', FormatBold);
@ -106,3 +107,4 @@ Vue.component('iconFormatListChecks', FormatListChecks);
Vue.component('iconCheckCircle', CheckCircle);
Vue.component('iconContentCopy', ContentCopy);
Vue.component('iconKey', Key);
Vue.component('iconDotsHorizontal', DotsHorizontal);

View File

@ -7,7 +7,6 @@ const clientId = GOOGLE_CLIENT_ID;
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
const appsDomain = null;
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
let googlePlusNotification = true;
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
const getDriveScopes = token => [token.driveFullAccess
@ -40,17 +39,18 @@ if (utils.queryParams.providerId === 'googleDrive') {
* https://developers.google.com/people/api/rest/v1/people/get
*/
const getUser = async (sub, token) => {
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`;
const { body } = await networkSvc.request(token
? {
method: 'GET',
url: `https://people.googleapis.com/v1/people/${sub}`,
url,
headers: {
Authorization: `Bearer ${token.accessToken}`,
},
}
: {
method: 'GET',
url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`,
url: `${url}&key=${apiKey}`,
}, true);
return body;
};
@ -60,10 +60,12 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => {
try {
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
const body = await getUser(sub, googleToken);
const name = body.names[0] || {};
const photo = body.photos[0] || {};
return {
id: `${subPrefix}:${body.id}`,
name: body.displayName,
imageUrl: (body.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
id: `${subPrefix}:${sub}`,
name: name.displayName,
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
};
} catch (err) {
if (err.status !== 404) {
@ -141,41 +143,52 @@ export default {
}
// Build token object including scopes and sub
const existingToken = store.getters['data/googleTokensBySub'][body.sub] || {
scopes: [],
};
const mergedScopes = [...new Set([...scopes, ...existingToken.scopes])];
const existingToken = store.getters['data/googleTokensBySub'][body.sub];
const token = {
scopes: mergedScopes,
scopes,
accessToken,
expiresOn: Date.now() + (expiresIn * 1000),
idToken,
sub: body.sub,
name: existingToken.name || 'Unknown',
isLogin: existingToken.isLogin || (!store.getters['workspace/mainWorkspaceToken'] &&
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1),
isSponsor: existingToken.isSponsor || false,
isDrive: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: mergedScopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: mergedScopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
name: (existingToken || {}).name || 'Unknown',
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
isSponsor: false,
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
};
// Call the user info endpoint
const user = await getUser('me', token);
if (user.displayName) {
token.name = user.displayName;
} else if (googlePlusNotification) {
store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.');
googlePlusNotification = false;
const userId = user.resourceName.split('/')[1];
const name = user.names[0] || {};
const photo = user.photos[0] || {};
if (name.displayName) {
token.name = name.displayName;
}
userSvc.addInfo({
id: `${subPrefix}:${user.id}`,
name: user.displayName,
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
id: `${subPrefix}:${userId}`,
name: name.displayName,
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
});
if (existingToken) {
// We probably retrieved a new token with restricted scopes.
// That's no problem, token will be refreshed later with merged scopes.
// Restore flags
Object.assign(token, {
isLogin: existingToken.isLogin || token.isLogin,
isSponsor: existingToken.isSponsor,
isDrive: existingToken.isDrive || token.isDrive,
isBlogger: existingToken.isBlogger || token.isBlogger,
isPhotos: existingToken.isPhotos || token.isPhotos,
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
});
}
if (token.isLogin) {
try {
token.isSponsor = (await networkSvc.request({

View File

@ -34,17 +34,24 @@ const empty = (id) => {
};
// Item IDs that will be stored in the localStorage
const lsItemIdSet = new Set(constants.localStorageDataIds);
const localStorageIdSet = new Set(constants.localStorageDataIds);
// Getter/setter/patcher factories
const getter = id => state => ((lsItemIdSet.has(id)
? state.lsItemsById
: state.itemsById)[id] || {}).data || empty(id).data;
const getter = id => (state) => {
const itemsById = localStorageIdSet.has(id)
? state.lsItemsById
: state.itemsById;
if (itemsById[id]) {
return itemsById[id].data;
}
return empty(id).data;
};
const setter = id => ({ commit }, data) => commit('setItem', itemTemplate(id, data));
const patcher = id => ({ state, commit }, data) => {
const item = Object.assign(empty(id), (lsItemIdSet.has(id)
const itemsById = localStorageIdSet.has(id)
? state.lsItemsById
: state.itemsById)[id]);
: state.itemsById;
const item = Object.assign(empty(id), itemsById[id]);
commit('setItem', {
...empty(id),
data: typeof data === 'object' ? {
@ -116,7 +123,7 @@ export default {
});
// Store item in itemsById or lsItemsById if its stored in the localStorage
Vue.set(lsItemIdSet.has(item.id) ? lsItemsById : itemsById, item.id, item);
Vue.set(localStorageIdSet.has(item.id) ? lsItemsById : itemsById, item.id, item);
},
deleteItem({ itemsById }, id) {
// Only used by localDbSvc to clean itemsById from object moved to localStorage
@ -196,6 +203,7 @@ export default {
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
badgesByFeatureId: getter('badges'),
},
actions: {
setSettings: setter('settings'),