Removed monetizejs sponsorship support.
Reduced time counter increment interval. Added badge service. Refactored user service. Replaced Google+ with People API.
This commit is contained in:
parent
2a865ddb44
commit
1b2d48ff22
@ -41,12 +41,9 @@ exports.generate = (req, res) => {
|
||||
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
|
||||
? req.query.format
|
||||
: 'pdf';
|
||||
Promise.all([
|
||||
user.checkSponsor(req.query.idToken),
|
||||
user.checkMonetize(req.query.token),
|
||||
])
|
||||
.then(([isSponsor, isMonetize]) => {
|
||||
if (!isSponsor && !isMonetize) {
|
||||
user.checkSponsor(req.query.idToken)
|
||||
.then((isSponsor) => {
|
||||
if (!isSponsor) {
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
|
||||
@ -79,7 +76,7 @@ exports.generate = (req, res) => {
|
||||
if (!Number.isNaN(options.tocDepth)) {
|
||||
params.push('--toc-depth', options.tocDepth);
|
||||
}
|
||||
options.highlightStyle = highlightStyles.indexOf(options.highlightStyle) !== -1 ? options.highlightStyle : 'kate';
|
||||
options.highlightStyle = highlightStyles.includes(options.highlightStyle) ? options.highlightStyle : 'kate';
|
||||
params.push('--highlight-style', options.highlightStyle);
|
||||
Object.keys(metadata).forEach((key) => {
|
||||
params.push('-M', `${key}=${metadata[key]}`);
|
||||
|
@ -50,12 +50,9 @@ const readJson = (str) => {
|
||||
|
||||
exports.generate = (req, res) => {
|
||||
let wkhtmltopdfError = '';
|
||||
Promise.all([
|
||||
user.checkSponsor(req.query.idToken),
|
||||
user.checkMonetize(req.query.token),
|
||||
])
|
||||
.then(([isSponsor, isMonetize]) => {
|
||||
if (!isSponsor && !isMonetize) {
|
||||
user.checkSponsor(req.query.idToken)
|
||||
.then((isSponsor) => {
|
||||
if (!isSponsor) {
|
||||
throw new Error('unauthorized');
|
||||
}
|
||||
return new Promise((resolve, reject) => {
|
||||
@ -127,7 +124,7 @@ exports.generate = (req, res) => {
|
||||
}
|
||||
|
||||
// Page size
|
||||
params.push('--page-size', authorizedPageSizes.indexOf(options.pageSize) === -1 ? 'A4' : options.pageSize);
|
||||
params.push('--page-size', !authorizedPageSizes.includes(options.pageSize) ? 'A4' : options.pageSize);
|
||||
|
||||
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||
|
@ -116,21 +116,3 @@ exports.checkSponsor = (idToken) => {
|
||||
return exports.getUserFromToken(idToken)
|
||||
.then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
|
||||
};
|
||||
|
||||
exports.checkMonetize = (token) => {
|
||||
if (!token) {
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
return new Promise(resolve => request({
|
||||
uri: 'https://monetizejs.com/api/payments',
|
||||
qs: {
|
||||
access_token: token,
|
||||
},
|
||||
json: true,
|
||||
}, (err, paymentsRes, payments) => {
|
||||
const authorized = payments && payments.app === 'ESTHdCYOi18iLhhO' && (
|
||||
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
|
||||
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
|
||||
resolve(!err && paymentsRes.statusCode === 200 && authorized);
|
||||
}));
|
||||
};
|
||||
|
@ -19,7 +19,6 @@ import ContextMenu from './ContextMenu';
|
||||
import SplashScreen from './SplashScreen';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
import networkSvc from '../services/networkSvc';
|
||||
import sponsorSvc from '../services/sponsorSvc';
|
||||
import tempFileSvc from '../services/tempFileSvc';
|
||||
import store from '../store';
|
||||
import './common/vueGlobals';
|
||||
@ -55,7 +54,6 @@ export default {
|
||||
try {
|
||||
await syncSvc.init();
|
||||
await networkSvc.init();
|
||||
await sponsorSvc.init();
|
||||
this.ready = true;
|
||||
tempFileSvc.setReady();
|
||||
} catch (err) {
|
||||
|
@ -22,6 +22,7 @@ import { mapMutations, mapActions } from 'vuex';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
import explorerSvc from '../services/explorerSvc';
|
||||
import store from '../store';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
name: 'explorer-node', // Required for recursivity
|
||||
@ -81,7 +82,7 @@ export default {
|
||||
]),
|
||||
select(id = this.node.item.id, doOpen = true) {
|
||||
const node = store.getters['explorer/nodeMap'][id];
|
||||
if (!node) {
|
||||
if (!node || node.item.id === store.state.explorer.selectedId) {
|
||||
return false;
|
||||
}
|
||||
store.commit('explorer/setSelectedId', id);
|
||||
@ -92,6 +93,7 @@ export default {
|
||||
store.commit('explorer/toggleOpenNode', id);
|
||||
} else {
|
||||
store.commit('file/setCurrentId', id);
|
||||
badgeSvc.addBadge('switchFile');
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
@ -104,9 +106,11 @@ export default {
|
||||
if (newChildNode.isFolder) {
|
||||
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||
this.select(item.id);
|
||||
badgeSvc.addBadge('createFolder');
|
||||
} else {
|
||||
const item = await workspaceSvc.createFile(newChildNode.item);
|
||||
this.select(item.id);
|
||||
badgeSvc.addBadge('createFile');
|
||||
}
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
@ -115,15 +119,16 @@ export default {
|
||||
store.commit('explorer/setNewItem', null);
|
||||
},
|
||||
async submitEdit(cancel) {
|
||||
const { item } = store.getters['explorer/editingNode'];
|
||||
const { item, isFolder } = store.getters['explorer/editingNode'];
|
||||
const value = this.editingValue;
|
||||
this.setEditingId(null);
|
||||
if (!cancel && item.id && value) {
|
||||
if (!cancel && item.id && value && item.name !== value) {
|
||||
try {
|
||||
await workspaceSvc.storeItem({
|
||||
...item,
|
||||
name: value,
|
||||
});
|
||||
badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
@ -151,6 +156,7 @@ export default {
|
||||
...sourceNode.item,
|
||||
parentId: targetNode.item.id,
|
||||
});
|
||||
badgeSvc.addBadge('moveFiles');
|
||||
}
|
||||
},
|
||||
async onContextMenu(evt) {
|
||||
|
@ -177,11 +177,9 @@ export default {
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $editor-background-light, 6.7%);
|
||||
border-color: $editor-background-light;
|
||||
|
||||
.app--dark & {
|
||||
background-color: mix(#fff, $editor-background-dark, 6.7%);
|
||||
border-color: $editor-background-dark;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -204,7 +202,6 @@ $preview-background-dark: #252525;
|
||||
.sticky-comment,
|
||||
.current-discussion {
|
||||
background-color: mix(#000, $preview-background-light, 6.7%);
|
||||
border-color: $preview-background-light;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ import SyncManagementModal from './modals/SyncManagementModal';
|
||||
import PublishManagementModal from './modals/PublishManagementModal';
|
||||
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
||||
import AccountManagementModal from './modals/AccountManagementModal';
|
||||
import BadgeManagementModal from './modals/BadgeManagementModal';
|
||||
import SponsorModal from './modals/SponsorModal';
|
||||
|
||||
// Providers
|
||||
@ -88,6 +89,7 @@ export default {
|
||||
PublishManagementModal,
|
||||
WorkspaceManagementModal,
|
||||
AccountManagementModal,
|
||||
BadgeManagementModal,
|
||||
SponsorModal,
|
||||
// Providers
|
||||
GooglePhotoModal,
|
||||
|
@ -57,6 +57,7 @@ import utils from '../services/utils';
|
||||
import pagedownButtons from '../data/pagedownButtons';
|
||||
import store from '../store';
|
||||
import workspaceSvc from '../services/workspaceSvc';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
|
||||
// According to mousetrap
|
||||
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
||||
@ -178,7 +179,7 @@ export default {
|
||||
},
|
||||
requestSync() {
|
||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||
syncSvc.requestSync();
|
||||
syncSvc.requestSync(true);
|
||||
}
|
||||
},
|
||||
requestPublish() {
|
||||
@ -188,7 +189,11 @@ export default {
|
||||
},
|
||||
pagedownClick(name) {
|
||||
if (store.getters['content/isCurrentEditable']) {
|
||||
const text = editorSvc.clEditor.getContent();
|
||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||
if (text !== editorSvc.clEditor.getContent()) {
|
||||
badgeSvc.addBadge('formatButtons');
|
||||
}
|
||||
}
|
||||
},
|
||||
async editTitle(toggle) {
|
||||
@ -198,12 +203,13 @@ export default {
|
||||
} else {
|
||||
const title = this.title.trim();
|
||||
this.title = store.getters['file/current'].name;
|
||||
if (title) {
|
||||
if (title && this.title !== title) {
|
||||
try {
|
||||
await workspaceSvc.storeItem({
|
||||
...store.getters['file/current'],
|
||||
name: title,
|
||||
});
|
||||
badgeSvc.addBadge('editCurrentFileName');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
<div class="notification__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
||||
<div class="notification__icon flex flex--column flex--center">
|
||||
<icon-alert v-if="item.type === 'error'"></icon-alert>
|
||||
<icon-check-circle v-if="item.type === 'badge'"></icon-check-circle>
|
||||
<icon-information v-else></icon-information>
|
||||
</div>
|
||||
<div class="notification__content">
|
||||
|
@ -19,7 +19,7 @@
|
||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||
<import-export-menu v-else-if="panel === 'importExport'"></import-export-menu>
|
||||
<workspace-backup-menu v-else-if="panel === 'workspaceBackup'"></workspace-backup-menu>
|
||||
<workspace-backup-menu v-else-if="panel === 'workspaceBackups'"></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>
|
||||
@ -54,7 +54,7 @@ const panelNames = {
|
||||
publish: 'Publish',
|
||||
history: 'File history',
|
||||
importExport: 'Import/export',
|
||||
workspaceBackup: 'Workspace backup',
|
||||
workspaceBackups: 'Workspace backups',
|
||||
};
|
||||
|
||||
export default {
|
||||
@ -174,8 +174,9 @@ export default {
|
||||
font-size: 0.95em;
|
||||
|
||||
p {
|
||||
margin: 10px;
|
||||
margin: 10px 15px;
|
||||
line-height: 1.5;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -10,14 +10,17 @@ import store from '../store';
|
||||
export default {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
sanitizedUserId() {
|
||||
return userSvc.sanitizeUserId(this.userId);
|
||||
},
|
||||
url() {
|
||||
const userInfo = store.state.userInfo.itemsById[this.userId];
|
||||
const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
|
||||
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
userId: {
|
||||
handler: userId => userSvc.getInfo(userId),
|
||||
sanitizedUserId: {
|
||||
handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
@ -9,14 +9,17 @@ import store from '../store';
|
||||
export default {
|
||||
props: ['userId'],
|
||||
computed: {
|
||||
sanitizedUserId() {
|
||||
return userSvc.sanitizeUserId(this.userId);
|
||||
},
|
||||
name() {
|
||||
const userInfo = store.state.userInfo.itemsById[this.userId];
|
||||
const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
|
||||
return userInfo ? userInfo.name : 'Someone';
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
userId: {
|
||||
handler: userId => userSvc.getInfo(userId),
|
||||
sanitizedUserId: {
|
||||
handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
|
@ -75,6 +75,6 @@ Vue.directive('clipboard', {
|
||||
|
||||
// Global filters
|
||||
Vue.filter('formatTime', time =>
|
||||
// Access the minute counter for reactive refresh
|
||||
timeSvc.format(time, store.state.minuteCounter));
|
||||
// Access the time counter for reactive refresh
|
||||
timeSvc.format(time, store.state.timeCounter));
|
||||
|
||||
|
@ -28,6 +28,7 @@ import UserName from '../UserName';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -52,6 +53,7 @@ export default {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'commentDeletion');
|
||||
store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
|
||||
badgeSvc.addBadge('removeComment');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
|
@ -133,11 +133,6 @@ export default {
|
||||
: editorSvc.previewElt.parentNode;
|
||||
|
||||
this.updateSticky = () => {
|
||||
const commitIfDifferent = (value) => {
|
||||
if (store.state.discussion.stickyComment !== value) {
|
||||
store.commit('discussion/setStickyComment', value);
|
||||
}
|
||||
};
|
||||
let height = 0;
|
||||
let offsetTop = this.tops.current;
|
||||
const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
|
||||
@ -152,13 +147,15 @@ export default {
|
||||
const currentDiscussionElt = document.querySelector('.current-discussion__inner');
|
||||
const minOffsetTop = this.scrollerElt.scrollTop + 10;
|
||||
const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
|
||||
- currentDiscussionElt.clientHeight - 10;
|
||||
- currentDiscussionElt.clientHeight;
|
||||
let stickyComment = null;
|
||||
if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
|
||||
commitIfDifferent('bottom');
|
||||
stickyComment = 'bottom';
|
||||
} else if (offsetTop < minOffsetTop) {
|
||||
commitIfDifferent('top');
|
||||
} else {
|
||||
commitIfDifferent(null);
|
||||
stickyComment = 'top';
|
||||
}
|
||||
if (store.state.discussion.stickyComment !== stickyComment) {
|
||||
store.commit('discussion/setStickyComment', stickyComment);
|
||||
}
|
||||
};
|
||||
|
||||
@ -199,19 +196,6 @@ export default {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.comment-list__current-discussion {
|
||||
border-top: 2px solid;
|
||||
border-bottom: 2px solid;
|
||||
|
||||
.comment-list--top & {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.comment-list--bottom & {
|
||||
border-top-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* use div selector to avoid collision with Prism */
|
||||
div.comment {
|
||||
padding: 5px 10px 10px;
|
||||
|
@ -34,6 +34,7 @@ import animationSvc from '../../services/animationSvc';
|
||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||
import StickyComment from './StickyComment';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -103,6 +104,7 @@ export default {
|
||||
store.dispatch('discussion/cleanCurrentFile', {
|
||||
filterDiscussion: this.currentDiscussion,
|
||||
});
|
||||
badgeSvc.addBadge('removeDiscussion');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
@ -118,7 +120,6 @@ export default {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
border-top: 2px solid;
|
||||
|
||||
.sticky-comment {
|
||||
position: relative;
|
||||
|
@ -30,6 +30,7 @@ import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||
import utils from '../../services/utils';
|
||||
import userSvc from '../../services/userSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -70,12 +71,15 @@ export default {
|
||||
[utils.uid()]: comment,
|
||||
},
|
||||
};
|
||||
// Create discussion
|
||||
if (discussionId === store.state.discussion.newDiscussionId) {
|
||||
// Create discussion
|
||||
patch.discussions = {
|
||||
...store.getters['content/current'].discussions,
|
||||
[discussionId]: store.getters['discussion/newDiscussion'],
|
||||
};
|
||||
badgeSvc.addBadge('createDiscussion');
|
||||
} else {
|
||||
badgeSvc.addBadge('addComment');
|
||||
}
|
||||
store.dispatch('content/patchCurrent', patch);
|
||||
store.commit('discussion/setNewCommentText');
|
||||
|
@ -40,7 +40,6 @@ export default {
|
||||
right: 0;
|
||||
font-size: 15px;
|
||||
padding-top: 10px;
|
||||
border-bottom: 2px solid;
|
||||
|
||||
.current-discussion & {
|
||||
width: auto !important;
|
||||
|
@ -56,6 +56,7 @@ import utils from '../../services/utils';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
let editorClassAppliers = [];
|
||||
let previewClassAppliers = [];
|
||||
@ -237,10 +238,12 @@ export default {
|
||||
syncLocation: {
|
||||
immediate: true,
|
||||
handler(value) {
|
||||
if (!value) {
|
||||
const firstSyncLocation = this.syncLocations[0];
|
||||
if (firstSyncLocation) {
|
||||
if (!value) {
|
||||
this.syncLocationId = firstSyncLocation.id;
|
||||
} else if (value.id !== firstSyncLocation.id) {
|
||||
badgeSvc.addBadge('chooseHistory');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -53,6 +53,7 @@ import Provider from '../../services/providers/common/Provider';
|
||||
import store from '../../store';
|
||||
import workspaceSvc from '../../services/workspaceSvc';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||
|
||||
@ -85,6 +86,7 @@ export default {
|
||||
name: file.name,
|
||||
});
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
badgeSvc.addBadge('importMarkdown');
|
||||
},
|
||||
async onImportHtml(evt) {
|
||||
const file = evt.target.files[0];
|
||||
@ -96,23 +98,29 @@ export default {
|
||||
name: file.name,
|
||||
});
|
||||
store.commit('file/setCurrentId', item.id);
|
||||
badgeSvc.addBadge('importHtml');
|
||||
},
|
||||
exportMarkdown() {
|
||||
async exportMarkdown() {
|
||||
const currentFile = store.getters['file/current'];
|
||||
return exportSvc.exportToDisk(currentFile.id, 'md')
|
||||
.catch(() => { /* Cancel */ });
|
||||
try {
|
||||
await exportSvc.exportToDisk(currentFile.id, 'md');
|
||||
badgeSvc.addBadge('exportMarkdown');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
exportHtml() {
|
||||
return store.dispatch('modal/open', 'htmlExport')
|
||||
.catch(() => { /* Cancel */ });
|
||||
async exportHtml() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'htmlExport');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
exportPdf() {
|
||||
return store.dispatch('modal/open', 'pdfExport')
|
||||
.catch(() => { /* Cancel */ });
|
||||
async exportPdf() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'pdfExport');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
exportPandoc() {
|
||||
return store.dispatch('modal/open', 'pandocExport')
|
||||
.catch(() => { /* Cancel */ });
|
||||
async exportPandoc() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'pandocExport');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -99,15 +99,20 @@
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
|
||||
<span>Manage access to your external accounts.</span>
|
||||
</menu-entry>
|
||||
<menu-entry @click.native="badges">
|
||||
<icon-seal slot="icon"></icon-seal>
|
||||
<div><div class="menu-entry__label menu-entry__label--count">{{badgeCount}}/{{featureCount}}</div> Badges</div>
|
||||
<span>List application features and earned badges.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="setPanel('workspaceBackup')">
|
||||
<menu-entry @click.native="setPanel('workspaceBackups')">
|
||||
<icon-content-save slot="icon"></icon-content-save>
|
||||
Workspace backup
|
||||
Workspace backups
|
||||
</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>
|
||||
<span>Sign out and clean all workspace data.</span>
|
||||
</menu-entry>
|
||||
<hr>
|
||||
<menu-entry @click.native="about">
|
||||
@ -161,6 +166,12 @@ export default {
|
||||
return Object.values(store.getters['data/tokensByType'])
|
||||
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
|
||||
},
|
||||
badgeCount() {
|
||||
return store.getters['data/allBadges'].filter(badge => badge.isEarned).length;
|
||||
},
|
||||
featureCount() {
|
||||
return store.getters['data/allBadges'].length;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('data', {
|
||||
@ -186,35 +197,30 @@ export default {
|
||||
},
|
||||
async settings() {
|
||||
try {
|
||||
const settings = await store.dispatch('modal/open', 'settings');
|
||||
store.dispatch('data/setSettings', settings);
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
await store.dispatch('modal/open', 'settings');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async templates() {
|
||||
try {
|
||||
const { templates } = await store.dispatch('modal/open', 'templates');
|
||||
store.dispatch('data/setTemplatesById', templates);
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
await store.dispatch('modal/open', 'templates');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async accounts() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'accountManagement');
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async badges() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'badgeManagement');
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
async reset() {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'reset');
|
||||
window.location.href = '#reset=true';
|
||||
window.location.reload();
|
||||
} catch (e) {
|
||||
// Cancel
|
||||
}
|
||||
} catch (e) { /* Cancel */ }
|
||||
},
|
||||
about() {
|
||||
store.dispatch('modal/open', 'about');
|
||||
|
@ -129,13 +129,13 @@ const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||
.filter(token => filter(token))
|
||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||
|
||||
const publishModalOpener = type => async (token) => {
|
||||
const publishModalOpener = (type, featureId) => async (token) => {
|
||||
try {
|
||||
const publishLocation = await store.dispatch('modal/open', {
|
||||
type,
|
||||
token,
|
||||
});
|
||||
publishSvc.createPublishLocation(publishLocation);
|
||||
publishSvc.createPublishLocation(publishLocation, featureId);
|
||||
} catch (e) { /* cancel */ }
|
||||
};
|
||||
|
||||
@ -236,15 +236,15 @@ export default {
|
||||
await zendeskHelper.addAccount(subdomain, clientId);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
publishBlogger: publishModalOpener('bloggerPublish'),
|
||||
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
|
||||
publishDropbox: publishModalOpener('dropboxPublish'),
|
||||
publishGithub: publishModalOpener('githubPublish'),
|
||||
publishGist: publishModalOpener('gistPublish'),
|
||||
publishGitlab: publishModalOpener('gitlabPublish'),
|
||||
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
|
||||
publishWordpress: publishModalOpener('wordpressPublish'),
|
||||
publishZendesk: publishModalOpener('zendeskPublish'),
|
||||
publishBlogger: publishModalOpener('bloggerPublish', 'publishToBlogger'),
|
||||
publishBloggerPage: publishModalOpener('bloggerPagePublish', 'publishToBloggerPage'),
|
||||
publishDropbox: publishModalOpener('dropboxPublish', 'publishToDropbox'),
|
||||
publishGithub: publishModalOpener('githubPublish', 'publishToGithub'),
|
||||
publishGist: publishModalOpener('gistPublish', 'publishToGist'),
|
||||
publishGitlab: publishModalOpener('gitlabPublish', 'publishToGitlab'),
|
||||
publishGoogleDrive: publishModalOpener('googleDrivePublish', 'publishToGoogleDrive'),
|
||||
publishWordpress: publishModalOpener('wordpressPublish', 'publishToWordPress'),
|
||||
publishZendesk: publishModalOpener('zendeskPublish', 'publishToZendesk'),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -108,6 +108,7 @@ import githubProvider from '../../services/providers/githubProvider';
|
||||
import gitlabProvider from '../../services/providers/gitlabProvider';
|
||||
import syncSvc from '../../services/syncSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||
.filter(token => filter(token))
|
||||
@ -162,7 +163,7 @@ export default {
|
||||
methods: {
|
||||
requestSync() {
|
||||
if (!this.isSyncRequested) {
|
||||
syncSvc.requestSync();
|
||||
syncSvc.requestSync(true);
|
||||
}
|
||||
},
|
||||
async manageSync() {
|
||||
@ -194,28 +195,36 @@ export default {
|
||||
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGoogleDrive(token) {
|
||||
const files = await googleHelper.openPicker(token, 'doc');
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => googleDriveProvider.openFiles(token, files),
|
||||
);
|
||||
},
|
||||
async openDropbox(token) {
|
||||
const paths = await dropboxHelper.openChooser(token);
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => dropboxProvider.openFiles(token, paths),
|
||||
async () => {
|
||||
await dropboxProvider.openFiles(token, paths);
|
||||
badgeSvc.addBadge('openFromDropbox');
|
||||
},
|
||||
);
|
||||
},
|
||||
async saveDropbox(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'dropboxSave');
|
||||
badgeSvc.addBadge('saveOnDropbox');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGoogleDrive(token) {
|
||||
const files = await googleHelper.openPicker(token, 'doc');
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
async () => {
|
||||
await googleDriveProvider.openFiles(token, files);
|
||||
badgeSvc.addBadge('openFromGoogleDrive');
|
||||
},
|
||||
);
|
||||
},
|
||||
async saveGoogleDrive(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'googleDriveSave');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveDropbox(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'dropboxSave');
|
||||
badgeSvc.addBadge('saveOnGoogleDrive');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGithub(token) {
|
||||
@ -226,18 +235,23 @@ export default {
|
||||
});
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => githubProvider.openFile(token, syncLocation),
|
||||
async () => {
|
||||
await githubProvider.openFile(token, syncLocation);
|
||||
badgeSvc.addBadge('openFromGithub');
|
||||
},
|
||||
);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGithub(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'githubSave');
|
||||
badgeSvc.addBadge('saveOnGithub');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGist(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'gistSync');
|
||||
badgeSvc.addBadge('saveOnGist');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async openGitlab(token) {
|
||||
@ -248,13 +262,17 @@ export default {
|
||||
});
|
||||
store.dispatch(
|
||||
'queue/enqueue',
|
||||
() => gitlabProvider.openFile(token, syncLocation),
|
||||
async () => {
|
||||
await gitlabProvider.openFile(token, syncLocation);
|
||||
badgeSvc.addBadge('openFromGitlab');
|
||||
},
|
||||
);
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
async saveGitlab(token) {
|
||||
try {
|
||||
await openSyncModal(token, 'gitlabSave');
|
||||
badgeSvc.addBadge('saveOnGitlab');
|
||||
} catch (e) { /* cancel */ }
|
||||
},
|
||||
},
|
||||
|
@ -2,8 +2,6 @@
|
||||
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
||||
<div class="modal__content">
|
||||
<div class="logo-background"></div>
|
||||
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||
<hr>
|
||||
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
|
||||
<br>
|
||||
<a target="_blank" href="https://github.com/benweet/stackedit/issues">Issue tracker</a> — <a target="_blank" href="https://github.com/benweet/stackedit/releases">Changelog</a>
|
||||
@ -13,12 +11,13 @@
|
||||
<a target="_blank" href="https://community.stackedit.io/">Community</a> — <a target="_blank" href="https://community.stackedit.io/c/how-to">Tutos and How To</a>
|
||||
<br>
|
||||
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
|
||||
<div class="modal__info">
|
||||
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
|
||||
</div>
|
||||
<hr>
|
||||
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||
<h3>FAQ</h3>
|
||||
<div class="faq" v-html="faq"></div>
|
||||
<hr>
|
||||
<div class="modal__info">
|
||||
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">contact us</a>.
|
||||
</div>
|
||||
Licensed under an
|
||||
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a><br>
|
||||
<a target="_blank" href="privacy_policy.html">Privacy Policy</a>
|
||||
|
@ -88,6 +88,7 @@ import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
|
||||
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
||||
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -160,6 +161,7 @@ export default {
|
||||
await store.dispatch('data/patchTokensByType', {
|
||||
[entry.providerId]: tokensBySub,
|
||||
});
|
||||
badgeSvc.addBadge('removeAccount');
|
||||
},
|
||||
async addBloggerAccount() {
|
||||
try {
|
||||
|
109
src/components/modals/BadgeManagementModal.vue
Normal file
109
src/components/modals/BadgeManagementModal.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<modal-inner class="modal__inner-1--badge-management" aria-label="Manage badges">
|
||||
<div class="modal__content">
|
||||
<div class="modal__image">
|
||||
<icon-seal></icon-seal>
|
||||
</div>
|
||||
<p v-if="badgeCount > 1">{{badgeCount}} badges earned</p>
|
||||
<p v-else>{{badgeCount}} badge earned</p>
|
||||
<div class="badge-entry" :class="{'badge-entry--earned': badge.isEarned}" v-for="badge in badgeTree" :key="badge.featureId">
|
||||
<div class="flex flex--row">
|
||||
<icon-seal></icon-seal>
|
||||
<div>
|
||||
<span class="badge-entry__name">{{badge.name}}</span>
|
||||
<span class="badge-entry__description">— {{badge.description}}</span>
|
||||
<div class="badge-entry" :class="{'badge-entry--earned': child.isEarned}" v-for="child in badge.children" :key="child.featureId">
|
||||
<div class="flex flex--row">
|
||||
<icon-seal></icon-seal>
|
||||
<div>
|
||||
<span class="badge-entry__name">{{child.name}}</span>
|
||||
<span class="badge-entry__description">— {{child.description}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button button--resolve" @click="config.resolve()">Close</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import store from '../../store';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ModalInner,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
'config',
|
||||
]),
|
||||
...mapGetters('data', [
|
||||
'badgeTree',
|
||||
]),
|
||||
badgeCount() {
|
||||
return store.getters['data/allBadges'].filter(badge => badge.isEarned).length;
|
||||
},
|
||||
featureCount() {
|
||||
return store.getters['data/allBadges'].length;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../../styles/variables.scss';
|
||||
|
||||
.modal__inner-1.modal__inner-1--badge-management {
|
||||
max-width: 520px;
|
||||
|
||||
p {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-entry {
|
||||
line-height: 1.4;
|
||||
margin: 2rem 0;
|
||||
font-size: 0.9em;
|
||||
|
||||
.badge-entry {
|
||||
font-size: 0.8em;
|
||||
margin: 0.75rem 0 0;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: 1.67em;
|
||||
height: 1.67em;
|
||||
margin-right: 0.25em;
|
||||
opacity: 0.33;
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-entry--earned svg {
|
||||
opacity: 1;
|
||||
color: goldenrod;
|
||||
}
|
||||
|
||||
.badge-entry__description {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.badge-entry__name {
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
opacity: 0.5;
|
||||
|
||||
.badge-entry--earned & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -93,8 +93,9 @@ import CodeEditor from '../CodeEditor';
|
||||
import utils from '../../services/utils';
|
||||
import presets from '../../data/presets';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
const simpleProperties = {
|
||||
const metadataProperties = {
|
||||
title: '',
|
||||
author: '',
|
||||
tags: '',
|
||||
@ -117,7 +118,7 @@ export default {
|
||||
yamlProperties: null,
|
||||
preset: '',
|
||||
error: null,
|
||||
...simpleProperties,
|
||||
...metadataProperties,
|
||||
}),
|
||||
computed: {
|
||||
...mapGetters('modal', [
|
||||
@ -148,10 +149,10 @@ export default {
|
||||
const properties = this.properties || {};
|
||||
const extensions = properties.extensions || {};
|
||||
this.preset = extensions.preset;
|
||||
if (this.presets.indexOf(this.preset) === -1) {
|
||||
if (!this.presets.includes(this.preset)) {
|
||||
this.preset = 'default';
|
||||
}
|
||||
Object.keys(simpleProperties).forEach((name) => {
|
||||
Object.keys(metadataProperties).forEach((name) => {
|
||||
this[name] = `${properties[name] || ''}`;
|
||||
});
|
||||
},
|
||||
@ -168,7 +169,7 @@ export default {
|
||||
hasChanged = true;
|
||||
}
|
||||
}
|
||||
Object.keys(simpleProperties).forEach((name) => {
|
||||
Object.keys(metadataProperties).forEach((name) => {
|
||||
if (this[name] !== properties[name]) {
|
||||
if (this[name]) {
|
||||
properties[name] = this[name];
|
||||
@ -215,6 +216,17 @@ export default {
|
||||
if (this.error) {
|
||||
this.setYamlTab();
|
||||
} else {
|
||||
const properties = this.properties || {};
|
||||
if (Object.keys(metadataProperties).some(key => properties[key])) {
|
||||
badgeSvc.addBadge('setMetadata');
|
||||
}
|
||||
const extensions = properties.extensions || {};
|
||||
if (extensions.preset) {
|
||||
badgeSvc.addBadge('changePreset');
|
||||
}
|
||||
if (Object.keys(extensions).filter(key => key !== 'preset').length) {
|
||||
badgeSvc.addBadge('changeExtension');
|
||||
}
|
||||
store.commit('content/patchItem', {
|
||||
id: this.contentId,
|
||||
properties: utils.sanitizeText(this.yamlProperties),
|
||||
|
@ -26,6 +26,7 @@ import { mapActions } from 'vuex';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default modalTemplate({
|
||||
data: () => ({
|
||||
@ -54,11 +55,12 @@ export default modalTemplate({
|
||||
...mapActions('notification', [
|
||||
'info',
|
||||
]),
|
||||
resolve() {
|
||||
async resolve() {
|
||||
const { config } = this;
|
||||
const currentFile = store.getters['file/current'];
|
||||
config.resolve();
|
||||
exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
|
||||
await exportSvc.exportToDisk(currentFile.id, 'html', this.allTemplatesById[this.selectedTemplate]);
|
||||
badgeSvc.addBadge('exportHtml');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -27,12 +27,12 @@
|
||||
|
||||
<script>
|
||||
import FileSaver from 'file-saver';
|
||||
import sponsorSvc from '../../services/sponsorSvc';
|
||||
import networkSvc from '../../services/networkSvc';
|
||||
import editorSvc from '../../services/editorSvc';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default modalTemplate({
|
||||
computedLocalSettings: {
|
||||
@ -45,20 +45,14 @@ export default modalTemplate({
|
||||
const currentContent = store.getters['content/current'];
|
||||
const { selectedFormat } = this;
|
||||
store.dispatch('queue/enqueue', async () => {
|
||||
const [sponsorToken, token] = await Promise.all([
|
||||
Promise.resolve().then(() => {
|
||||
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
||||
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
|
||||
}),
|
||||
sponsorSvc.getToken(),
|
||||
]);
|
||||
const sponsorToken = tokenToRefresh && await googleHelper.refreshToken(tokenToRefresh);
|
||||
|
||||
try {
|
||||
const { body } = await networkSvc.request({
|
||||
method: 'POST',
|
||||
url: 'pandocExport',
|
||||
params: {
|
||||
token,
|
||||
idToken: sponsorToken && sponsorToken.idToken,
|
||||
format: selectedFormat,
|
||||
options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
|
||||
@ -69,6 +63,7 @@ export default modalTemplate({
|
||||
timeout: 60000,
|
||||
});
|
||||
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
|
||||
badgeSvc.addBadge('exportPandoc');
|
||||
} catch (err) {
|
||||
if (err.status === 401) {
|
||||
store.dispatch('modal/open', 'sponsorOnly');
|
||||
|
@ -23,11 +23,11 @@
|
||||
<script>
|
||||
import FileSaver from 'file-saver';
|
||||
import exportSvc from '../../services/exportSvc';
|
||||
import sponsorSvc from '../../services/sponsorSvc';
|
||||
import networkSvc from '../../services/networkSvc';
|
||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||
import modalTemplate from './common/modalTemplate';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default modalTemplate({
|
||||
computedLocalSettings: {
|
||||
@ -38,12 +38,11 @@ export default modalTemplate({
|
||||
this.config.resolve();
|
||||
const currentFile = store.getters['file/current'];
|
||||
store.dispatch('queue/enqueue', async () => {
|
||||
const [sponsorToken, token, html] = await Promise.all([
|
||||
const [sponsorToken, html] = await Promise.all([
|
||||
Promise.resolve().then(() => {
|
||||
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
||||
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
|
||||
}),
|
||||
sponsorSvc.getToken(),
|
||||
exportSvc.applyTemplate(
|
||||
currentFile.id,
|
||||
this.allTemplatesById[this.selectedTemplate],
|
||||
@ -56,7 +55,6 @@ export default modalTemplate({
|
||||
method: 'POST',
|
||||
url: 'pdfExport',
|
||||
params: {
|
||||
token,
|
||||
idToken: sponsorToken && sponsorToken.idToken,
|
||||
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
|
||||
},
|
||||
@ -65,6 +63,7 @@ export default modalTemplate({
|
||||
timeout: 60000,
|
||||
});
|
||||
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
|
||||
badgeSvc.addBadge('exportPdf');
|
||||
} catch (err) {
|
||||
if (err.status === 401) {
|
||||
store.dispatch('modal/open', 'sponsorOnly');
|
||||
|
@ -50,6 +50,7 @@
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -72,6 +73,7 @@ export default {
|
||||
]),
|
||||
remove(location) {
|
||||
store.commit('publishLocation/deleteItem', location.id);
|
||||
badgeSvc.addBadge('removePublishLocation');
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -25,7 +25,7 @@
|
||||
</div>
|
||||
<div class="modal__button-bar">
|
||||
<button class="button" @click="config.reject()">Cancel</button>
|
||||
<button class="button button--resolve" @click="!error && config.resolve(strippedCustomSettings)">Ok</button>
|
||||
<button class="button button--resolve" @click="resolve">Ok</button>
|
||||
</div>
|
||||
</modal-inner>
|
||||
</template>
|
||||
@ -38,6 +38,7 @@ import Tab from './common/Tab';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import defaultSettings from '../../data/defaults/defaultSettings.yml';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
const emptySettings = `# Add your custom settings here to override the
|
||||
# default settings.
|
||||
@ -77,6 +78,25 @@ export default {
|
||||
this.error = e.message;
|
||||
}
|
||||
},
|
||||
async resolve() {
|
||||
if (!this.error) {
|
||||
const settings = this.strippedCustomSettings;
|
||||
await store.dispatch('data/setSettings', settings);
|
||||
const customSettings = yaml.safeLoad(settings);
|
||||
if (customSettings.shortcuts) {
|
||||
badgeSvc.addBadge('changeShortcuts');
|
||||
}
|
||||
const computedSettings = store.getters['data/computedSettings'];
|
||||
const customSettingsCount = Object
|
||||
.keys(customSettings)
|
||||
.filter(key => key !== 'shortcuts' && computedSettings[key])
|
||||
.length;
|
||||
if (customSettingsCount) {
|
||||
badgeSvc.addBadge('changeSettings');
|
||||
}
|
||||
this.config.resolve(settings);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -50,6 +50,7 @@
|
||||
import { mapGetters, mapActions } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -75,6 +76,7 @@ export default {
|
||||
this.info('This location can not be removed.');
|
||||
} else {
|
||||
store.commit('syncLocation/deleteItem', location.id);
|
||||
badgeSvc.addBadge('removeSyncLocation');
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -53,6 +53,7 @@
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import utils from '../../services/utils';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import CodeEditor from '../CodeEditor';
|
||||
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
|
||||
@ -153,7 +154,22 @@ export default {
|
||||
this.isEditing = false;
|
||||
}, 1);
|
||||
},
|
||||
resolve() {
|
||||
async resolve() {
|
||||
const oldTemplateIds = Object.keys(store.getters['data/templatesById']);
|
||||
await store.dispatch('data/setTemplatesById', this.templates);
|
||||
const newTemplateIds = Object.keys(store.getters['data/templatesById']);
|
||||
const createdCount = newTemplateIds
|
||||
.filter(id => !oldTemplateIds.includes(id))
|
||||
.length;
|
||||
const removedCount = oldTemplateIds
|
||||
.filter(id => !newTemplateIds.includes(id))
|
||||
.length;
|
||||
if (createdCount) {
|
||||
badgeSvc.addBadge('addTemplate');
|
||||
}
|
||||
if (removedCount) {
|
||||
badgeSvc.addBadge('removeTemplate');
|
||||
}
|
||||
this.config.resolve({
|
||||
templates: this.templates,
|
||||
selectedId: this.selectedId,
|
||||
|
@ -65,6 +65,7 @@ import { mapGetters, mapActions } from 'vuex';
|
||||
import ModalInner from './common/ModalInner';
|
||||
import workspaceSvc from '../../services/workspaceSvc';
|
||||
import store from '../../store';
|
||||
import badgeSvc from '../../services/badgeSvc';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -95,13 +96,14 @@ export default {
|
||||
submitEdit(cancel) {
|
||||
const workspace = this.workspacesById[this.editedId];
|
||||
if (workspace) {
|
||||
if (!cancel && this.editingName) {
|
||||
if (!cancel && this.editingName && this.editingName !== workspace.name) {
|
||||
store.dispatch('workspace/patchWorkspacesById', {
|
||||
[this.editedId]: {
|
||||
...workspace,
|
||||
name: this.editingName,
|
||||
},
|
||||
});
|
||||
badgeSvc.addBadge('renameWorkspace');
|
||||
} else {
|
||||
this.editingName = workspace.name;
|
||||
}
|
||||
@ -117,6 +119,7 @@ export default {
|
||||
try {
|
||||
await store.dispatch('modal/open', 'removeWorkspace');
|
||||
workspaceSvc.removeWorkspace(id);
|
||||
badgeSvc.addBadge('removeWorkspace');
|
||||
} catch (e) { /* Cancel */ }
|
||||
}
|
||||
},
|
||||
|
@ -64,11 +64,10 @@ export default (desc) => {
|
||||
};
|
||||
// Make use of `function` to have `this` bound to the component
|
||||
component.methods.configureTemplates = async function () { // eslint-disable-line func-names
|
||||
const { templates, selectedId } = await store.dispatch('modal/open', {
|
||||
const { selectedId } = await store.dispatch('modal/open', {
|
||||
type: 'templates',
|
||||
selectedId: this.selectedTemplate,
|
||||
});
|
||||
store.dispatch('data/setTemplatesById', templates);
|
||||
store.dispatch('data/patchLocalSettings', {
|
||||
[id]: selectedId,
|
||||
});
|
||||
|
@ -19,6 +19,7 @@ export default {
|
||||
'settings',
|
||||
'layoutSettings',
|
||||
'tokens',
|
||||
'badges',
|
||||
],
|
||||
textMaxLength: 250000,
|
||||
defaultName: 'Untitled',
|
||||
|
466
src/data/features.js
Normal file
466
src/data/features.js
Normal file
@ -0,0 +1,466 @@
|
||||
class Badge {
|
||||
constructor(featureId, name, description, children, isEarned) {
|
||||
this.featureId = featureId;
|
||||
this.name = name;
|
||||
this.description = description;
|
||||
this.children = children;
|
||||
this.isEarned = isEarned;
|
||||
}
|
||||
}
|
||||
|
||||
class Feature {
|
||||
constructor(id, badgeName, description, children = null) {
|
||||
this.id = id;
|
||||
this.badgeName = badgeName;
|
||||
this.description = description;
|
||||
this.children = children;
|
||||
}
|
||||
|
||||
toBadge(earnings) {
|
||||
const children = this.children
|
||||
? this.children.map(child => child.toBadge(earnings))
|
||||
: null;
|
||||
return new Badge(this.id, this.badgeName, this.description, children, children
|
||||
? children.every(child => child.isEarned)
|
||||
: !!earnings[this.id]);
|
||||
}
|
||||
}
|
||||
|
||||
export default [
|
||||
new Feature(
|
||||
'navigationBar',
|
||||
'Nav bar expert',
|
||||
'Master the navigation bar by formatting some Markdown and renaming the current file.',
|
||||
[
|
||||
new Feature(
|
||||
'formatButtons',
|
||||
'Formatter',
|
||||
'Use the format buttons to change formatting in your Markdown file.',
|
||||
),
|
||||
new Feature(
|
||||
'editCurrentFileName',
|
||||
'Renamer',
|
||||
'Use the name field in the navigation bar to rename the current file.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'explorer',
|
||||
'Explorer',
|
||||
'Use the file explorer to manage files and folders in your workspace.',
|
||||
[
|
||||
new Feature(
|
||||
'createFile',
|
||||
'File creator',
|
||||
'Use the file explorer to create a new file in your workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'switchFile',
|
||||
'File switcher',
|
||||
'Use the file explorer to switch from one file to another in your workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'createFolder',
|
||||
'Folder creator',
|
||||
'Use the file explorer to create a new folder in your workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'moveFiles',
|
||||
'File mover',
|
||||
'Drag files in the file explorer to move them around.',
|
||||
),
|
||||
new Feature(
|
||||
'renameFile',
|
||||
'File renamer',
|
||||
'Use the file explorer to rename a file in your workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'renameFolder',
|
||||
'Folder renamer',
|
||||
'Use the file explorer to rename a folder in your workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'removeFiles',
|
||||
'File remover',
|
||||
'Use the file explorer to remove files in your workspace.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'signIn',
|
||||
'Logged in',
|
||||
'Sign in with Google, sync your main workspace and unlock functionalities.',
|
||||
[
|
||||
new Feature(
|
||||
'syncMainWorkspace',
|
||||
'Main workspace synced',
|
||||
'Sign in with Google to sync your main workspace with your Google Drive app data folder.',
|
||||
),
|
||||
new Feature(
|
||||
'sponsor',
|
||||
'Sponsor',
|
||||
'Sign in with Google and sponsor StackEdit to unlock PDF and Pandoc exports.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'workspaces',
|
||||
'Workspace expert',
|
||||
'Use the workspace menu to create all kinds of workspaces and to manage them.',
|
||||
[
|
||||
new Feature(
|
||||
'addCouchdbWorkspace',
|
||||
'CouchDB workspace creator',
|
||||
'Use the workspace menu to create a CouchDB workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'addGithubWorkspace',
|
||||
'GitHub workspace creator',
|
||||
'Use the workspace menu to create a GitHub workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'addGitlabWorkspace',
|
||||
'GitLab workspace creator',
|
||||
'Use the workspace menu to create a GitLab workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'addGoogleDriveWorkspace',
|
||||
'Google Drive workspace creator',
|
||||
'Use the workspace menu to create a Google Drive workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'renameWorkspace',
|
||||
'Workspace renamer',
|
||||
'Use the "Manage workspaces" dialog to rename a workspace.',
|
||||
),
|
||||
new Feature(
|
||||
'removeWorkspace',
|
||||
'Workspace remover',
|
||||
'Use the "Manage workspaces" dialog to remove a workspace locally.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'manageAccounts',
|
||||
'Account manager',
|
||||
'Link all kinds of external accounts and use the "User accounts" dialog to manage them.',
|
||||
[
|
||||
new Feature(
|
||||
'addBloggerAccount',
|
||||
'Blogger user',
|
||||
'Link your Blogger account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addDropboxAccount',
|
||||
'Dropbox user',
|
||||
'Link your Dropbox account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addGitHubAccount',
|
||||
'GitHub user',
|
||||
'Link your GitHub account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addGitLabAccount',
|
||||
'GitLab user',
|
||||
'Link your GitLab account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addGoogleDriveAccount',
|
||||
'Google Drive user',
|
||||
'Link your Google Drive account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addGooglePhotosAccount',
|
||||
'Google Photos user',
|
||||
'Link your Google Photos account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addWordpressAccount',
|
||||
'WordPress user',
|
||||
'Link your WordPress account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'addZendeskAccount',
|
||||
'Zendesk user',
|
||||
'Link your Zendesk account to StackEdit.',
|
||||
),
|
||||
new Feature(
|
||||
'removeAccount',
|
||||
'Revoker',
|
||||
'Use the "User accounts" dialog to remove access to an external account.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'syncFiles',
|
||||
'File synchronizer',
|
||||
'Master the "Synchronize" menu by opening and saving files with all kinds of external accounts.',
|
||||
[
|
||||
new Feature(
|
||||
'openFromDropbox',
|
||||
'Dropbox reader',
|
||||
'Use the "Synchronize" menu to open a file from your Dropbox account.',
|
||||
),
|
||||
new Feature(
|
||||
'saveOnDropbox',
|
||||
'Dropbox writer',
|
||||
'Use the "Synchronize" menu to save a file in your Dropbox account.',
|
||||
),
|
||||
new Feature(
|
||||
'openFromGithub',
|
||||
'GitHub reader',
|
||||
'Use the "Synchronize" menu to open a file from a GitHub repository.',
|
||||
),
|
||||
new Feature(
|
||||
'saveOnGithub',
|
||||
'GitHub writer',
|
||||
'Use the "Synchronize" menu to save a file in a GitHub repository.',
|
||||
),
|
||||
new Feature(
|
||||
'saveOnGist',
|
||||
'Gist writer',
|
||||
'Use the "Synchronize" menu to save a file in a Gist.',
|
||||
),
|
||||
new Feature(
|
||||
'openFromGitlab',
|
||||
'GitLab reader',
|
||||
'Use the "Synchronize" menu to open a file from a GitLab repository.',
|
||||
),
|
||||
new Feature(
|
||||
'saveOnGitlab',
|
||||
'GitLab writer',
|
||||
'Use the "Synchronize" menu to save a file in a GitLab repository.',
|
||||
),
|
||||
new Feature(
|
||||
'openFromGoogleDrive',
|
||||
'Google Drive reader',
|
||||
'Use the "Synchronize" menu to open a file from your Google Drive account.',
|
||||
),
|
||||
new Feature(
|
||||
'saveOnGoogleDrive',
|
||||
'Google Drive writer',
|
||||
'Use the "Synchronize" menu to save a file in your Google Drive account.',
|
||||
),
|
||||
new Feature(
|
||||
'triggerSync',
|
||||
'Sync trigger',
|
||||
'Use the "Synchronize" menu or the navigation bar to manually trigger synchronization.',
|
||||
),
|
||||
new Feature(
|
||||
'syncMultipleLocations',
|
||||
'Multi-sync',
|
||||
'Use the "Synchronize" menu to synchronize a file with multiple external locations.',
|
||||
),
|
||||
new Feature(
|
||||
'removeSyncLocation',
|
||||
'Desynchronizer',
|
||||
'Use the "File synchronization" dialog to remove a sync location.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'publishFiles',
|
||||
'File publisher',
|
||||
'Master the "Publish" menu by publishing files to all kinds of external accounts.',
|
||||
[
|
||||
new Feature(
|
||||
'publishToBlogger',
|
||||
'Blogger publisher',
|
||||
'Use the "Publish" menu to publish a Blogger article.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToBloggerPage',
|
||||
'Blogger Page publisher',
|
||||
'Use the "Publish" menu to publish a Blogger page.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToDropbox',
|
||||
'Dropbox publisher',
|
||||
'Use the "Publish" menu to publish a file to your Dropbox account.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToGithub',
|
||||
'GitHub publisher',
|
||||
'Use the "Publish" menu to publish a file to a GitHub repository.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToGist',
|
||||
'Gist publisher',
|
||||
'Use the "Publish" menu to publish a file to a Gist.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToGitlab',
|
||||
'GitLab publisher',
|
||||
'Use the "Publish" menu to publish a file to a GitLab repository.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToGoogleDrive',
|
||||
'Google Drive publisher',
|
||||
'Use the "Publish" menu to publish a file to your Google Drive account.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToWordPress',
|
||||
'WordPress publisher',
|
||||
'Use the "Publish" menu to publish a WordPress article.',
|
||||
),
|
||||
new Feature(
|
||||
'publishToZendesk',
|
||||
'Zendesk publisher',
|
||||
'Use the "Publish" menu to publish a Zendesk Help Center article.',
|
||||
),
|
||||
new Feature(
|
||||
'triggerPublish',
|
||||
'Publication reviser',
|
||||
'Use the "Publish" menu or the navigation bar to manually update publications.',
|
||||
),
|
||||
new Feature(
|
||||
'publishMultipleLocations',
|
||||
'Multi-publication',
|
||||
'Use the "Publish" menu to publish a file to multiple external locations.',
|
||||
),
|
||||
new Feature(
|
||||
'removePublishLocation',
|
||||
'Unpublisher',
|
||||
'Use the "File publication" dialog to remove a publish location.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'manageHistory',
|
||||
'Historian',
|
||||
'Use the "File history" menu to see version history and restore old versions of the current file.',
|
||||
[
|
||||
new Feature(
|
||||
'restoreVersion',
|
||||
'Restorer',
|
||||
'Use the "File history" menu to restore an old version of the current file.',
|
||||
),
|
||||
new Feature(
|
||||
'chooseHistory',
|
||||
'History chooser',
|
||||
'Select a different history for a file that is synced with multiple external locations.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'manageProperties',
|
||||
'Property expert',
|
||||
'Use the "File properties" dialog to change properties for the current file.',
|
||||
[
|
||||
new Feature(
|
||||
'setMetadata',
|
||||
'Metadata setter',
|
||||
'Use the "File properties" dialog to set metadata for the current file.',
|
||||
),
|
||||
new Feature(
|
||||
'changePreset',
|
||||
'Preset changer',
|
||||
'Use the "File properties" dialog to change the Markdown engine preset.',
|
||||
),
|
||||
new Feature(
|
||||
'changeExtension',
|
||||
'Extension expert',
|
||||
'Use the "File properties" dialog to enable, disable or configure Markdown engine extensions.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'comment',
|
||||
'Comment expert',
|
||||
'Start and remove discussions, add and remove comments.',
|
||||
[
|
||||
new Feature(
|
||||
'createDiscussion',
|
||||
'Discussion starter',
|
||||
'Use the "comment" button to start a new discussion.',
|
||||
),
|
||||
new Feature(
|
||||
'addComment',
|
||||
'Commenter',
|
||||
'Use the discussion gutter to add a comment to an existing discussion.',
|
||||
),
|
||||
new Feature(
|
||||
'removeComment',
|
||||
'Moderator',
|
||||
'Use the discussion gutter to remove a comment in a discussion.',
|
||||
),
|
||||
new Feature(
|
||||
'removeDiscussion',
|
||||
'Discussion closer',
|
||||
'Use the discussion gutter to remove a discussion.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'importExport',
|
||||
'Import/export',
|
||||
'Use the "Import/export" menu to import and export files.',
|
||||
[
|
||||
new Feature(
|
||||
'importMarkdown',
|
||||
'Markdown importer',
|
||||
'Use the "Import/export" menu to import a Markdown file from disk.',
|
||||
),
|
||||
new Feature(
|
||||
'exportMarkdown',
|
||||
'Markdown exporter',
|
||||
'Use the "Import/export" menu to export a Markdown file to disk.',
|
||||
),
|
||||
new Feature(
|
||||
'importHtml',
|
||||
'HTML importer',
|
||||
'Use the "Import/export" menu to import an HTML file from disk and convert it to Markdown.',
|
||||
),
|
||||
new Feature(
|
||||
'exportHtml',
|
||||
'HTML exporter',
|
||||
'Use the "Import/export" menu to export a file to disk as an HTML file using a Handlebars template.',
|
||||
),
|
||||
new Feature(
|
||||
'exportPdf',
|
||||
'PDF exporter',
|
||||
'Use the "Import/export" menu to export a file to disk as a PDF file.',
|
||||
),
|
||||
new Feature(
|
||||
'exportPandoc',
|
||||
'Pandoc exporter',
|
||||
'Use the "Import/export" menu to export a file to disk using Pandoc.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'manageSettings',
|
||||
'Settings expert',
|
||||
'Use the "Settings" dialog to tweak the application behaviors and change keyboard shortcuts.',
|
||||
[
|
||||
new Feature(
|
||||
'changeSettings',
|
||||
'Tweaker',
|
||||
'Use the "Settings" dialog to tweak the application behaviors.',
|
||||
),
|
||||
new Feature(
|
||||
'changeShortcuts',
|
||||
'Shortcut editor',
|
||||
'Use the "Settings" dialog to change keyboard shortcuts.',
|
||||
),
|
||||
],
|
||||
),
|
||||
new Feature(
|
||||
'manageTemplates',
|
||||
'Template expert',
|
||||
'Use the "Templates" dialog to create, remove or modify Handlebars templates.',
|
||||
[
|
||||
new Feature(
|
||||
'addTemplate',
|
||||
'Template creator',
|
||||
'Use the "Templates" dialog to create a Handlebars template.',
|
||||
),
|
||||
new Feature(
|
||||
'removeTemplate',
|
||||
'Template remover',
|
||||
'Use the "Templates" dialog to remove a Handlebars template.',
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
5
src/icons/Seal.vue
Normal file
5
src/icons/Seal.vue
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="1 1 23 23">
|
||||
<path d="M 20.3943,19.3706L 16.3828,17.9893L 15.0016,22.0008L 11.9248,15.9996L 8.99895,21.9986L 7.61768,17.9871L 3.60619,19.3683L 6.53159,13.3704C 5.57315,12.1727 5,10.6533 5,9C 5,5.13401 8.13401,2 12,2C 15.866,2 19,5.13401 19,9C 19,10.6535 18.4267,12.1731 17.468,13.3708L 20.3943,19.3706 Z M 7,9.00001L 9.68578,10.3429L 9.50615,13.3356L 12.012,11.6811L 14.514,13.333L 14.334,10.3356L 17.0156,8.9948L 14.323,7.64851L 14.5017,4.6727L 12.0162,6.31371L 9.49384,4.64828L 9.67477,7.66262L 7,9.00001 Z "/>
|
||||
</svg>
|
||||
</template>
|
@ -53,6 +53,7 @@ import CheckCircle from './CheckCircle';
|
||||
import ContentCopy from './ContentCopy';
|
||||
import Key from './Key';
|
||||
import DotsHorizontal from './DotsHorizontal';
|
||||
import Seal from './Seal';
|
||||
|
||||
Vue.component('iconProvider', Provider);
|
||||
Vue.component('iconFormatBold', FormatBold);
|
||||
@ -108,3 +109,4 @@ Vue.component('iconCheckCircle', CheckCircle);
|
||||
Vue.component('iconContentCopy', ContentCopy);
|
||||
Vue.component('iconKey', Key);
|
||||
Vue.component('iconDotsHorizontal', DotsHorizontal);
|
||||
Vue.component('iconSeal', Seal);
|
||||
|
37
src/services/badgeSvc.js
Normal file
37
src/services/badgeSvc.js
Normal file
@ -0,0 +1,37 @@
|
||||
import store from '../store';
|
||||
|
||||
let lastEarnedFeatureIds = null;
|
||||
let debounceTimeoutId;
|
||||
|
||||
const showInfo = () => {
|
||||
const earnedBadges = store.getters['data/allBadges']
|
||||
.filter(badge => badge.isEarned && !lastEarnedFeatureIds.has(badge.featureId));
|
||||
if (earnedBadges.length) {
|
||||
store.dispatch('notification/badge', earnedBadges.length > 1
|
||||
? `You've earned ${earnedBadges.length} badges: ${earnedBadges.map(badge => `"${badge.name}"`).join(', ')}.`
|
||||
: `You've earned 1 badge: "${earnedBadges[0].name}".`);
|
||||
}
|
||||
lastEarnedFeatureIds = null;
|
||||
};
|
||||
|
||||
export default {
|
||||
addBadge(featureId) {
|
||||
if (!store.getters['data/badges'][featureId]) {
|
||||
if (!lastEarnedFeatureIds) {
|
||||
const earnedFeatureIds = store.getters['data/allBadges']
|
||||
.filter(badge => badge.isEarned)
|
||||
.map(badge => badge.featureId);
|
||||
lastEarnedFeatureIds = new Set(earnedFeatureIds);
|
||||
}
|
||||
|
||||
store.dispatch('data/patchBadges', {
|
||||
[featureId]: {
|
||||
created: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
clearTimeout(debounceTimeoutId);
|
||||
debounceTimeoutId = setTimeout(() => showInfo(), 5000);
|
||||
}
|
||||
},
|
||||
};
|
@ -1,5 +1,6 @@
|
||||
import store from '../store';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import badgeSvc from './badgeSvc';
|
||||
|
||||
export default {
|
||||
newItem(isFolder = false) {
|
||||
@ -61,6 +62,7 @@ export default {
|
||||
} else {
|
||||
workspaceSvc.deleteFile(id);
|
||||
}
|
||||
badgeSvc.addBadge('removeFiles');
|
||||
};
|
||||
|
||||
if (selectedNode === store.getters['explorer/selectedNode']) {
|
||||
|
@ -2,6 +2,7 @@ import store from '../../store';
|
||||
import couchdbHelper from './helpers/couchdbHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import badgeSvc from '../badgeSvc';
|
||||
|
||||
let syncLastSeq;
|
||||
|
||||
@ -58,6 +59,7 @@ export default new Provider({
|
||||
}
|
||||
}
|
||||
|
||||
badgeSvc.addBadge('addCouchdbWorkspace');
|
||||
return store.getters['workspace/workspacesById'][workspaceId];
|
||||
},
|
||||
async getChanges() {
|
||||
|
@ -66,7 +66,7 @@ export default new Provider({
|
||||
|
||||
return entries.map((entry) => {
|
||||
const sub = `${githubHelper.subPrefix}:${entry.user.id}`;
|
||||
userSvc.addInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url });
|
||||
userSvc.addUserInfo({ id: sub, name: entry.user.login, imageUrl: entry.user.avatar_url });
|
||||
return {
|
||||
sub,
|
||||
id: entry.version,
|
||||
|
@ -135,7 +135,7 @@ export default new Provider({
|
||||
user = committer;
|
||||
}
|
||||
const sub = `${githubHelper.subPrefix}:${user.id}`;
|
||||
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
|
||||
userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
|
||||
const date = (commit.author && commit.author.date)
|
||||
|| (commit.committer && commit.committer.date);
|
||||
return {
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import userSvc from '../userSvc';
|
||||
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
||||
import badgeSvc from '../badgeSvc';
|
||||
|
||||
const getAbsolutePath = ({ id }) =>
|
||||
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||
@ -86,6 +87,7 @@ export default new Provider({
|
||||
});
|
||||
}
|
||||
|
||||
badgeSvc.addBadge('addGithubWorkspace');
|
||||
return store.getters['workspace/workspacesById'][workspaceId];
|
||||
},
|
||||
getChanges() {
|
||||
@ -247,7 +249,7 @@ export default new Provider({
|
||||
user = committer;
|
||||
}
|
||||
const sub = `${githubHelper.subPrefix}:${user.id}`;
|
||||
userSvc.addInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
|
||||
userSvc.addUserInfo({ id: sub, name: user.login, imageUrl: user.avatar_url });
|
||||
const date = (commit.author && commit.author.date)
|
||||
|| (commit.committer && commit.committer.date)
|
||||
|| 1;
|
||||
|
@ -138,7 +138,7 @@ export default new Provider({
|
||||
return entries.map((entry) => {
|
||||
const email = entry.author_email || entry.committer_email;
|
||||
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: sub,
|
||||
name: entry.author_name || entry.committer_name,
|
||||
imageUrl: '',
|
||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import userSvc from '../userSvc';
|
||||
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
||||
import badgeSvc from '../badgeSvc';
|
||||
|
||||
const getAbsolutePath = ({ id }) =>
|
||||
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||
@ -98,6 +99,7 @@ export default new Provider({
|
||||
});
|
||||
}
|
||||
|
||||
badgeSvc.addBadge('addGitlabWorkspace');
|
||||
return store.getters['workspace/workspacesById'][workspaceId];
|
||||
},
|
||||
getChanges() {
|
||||
@ -252,7 +254,7 @@ export default new Provider({
|
||||
return entries.map((entry) => {
|
||||
const email = entry.author_email || entry.committer_email;
|
||||
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: sub,
|
||||
name: entry.author_name || entry.committer_name,
|
||||
imageUrl: '', // No way to get user's avatar url...
|
||||
|
@ -3,6 +3,7 @@ import googleHelper from './helpers/googleHelper';
|
||||
import Provider from './common/Provider';
|
||||
import utils from '../utils';
|
||||
import workspaceSvc from '../workspaceSvc';
|
||||
import badgeSvc from '../badgeSvc';
|
||||
|
||||
let fileIdToOpen;
|
||||
let syncStartPageToken;
|
||||
@ -137,6 +138,7 @@ export default new Provider({
|
||||
await initFolder(token, folder);
|
||||
}
|
||||
|
||||
badgeSvc.addBadge('addGoogleDriveWorkspace');
|
||||
return getWorkspace(folderId);
|
||||
},
|
||||
async performAction() {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import networkSvc from '../../networkSvc';
|
||||
import userSvc from '../../userSvc';
|
||||
import store from '../../../store';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const getAppKey = (fullAccess) => {
|
||||
if (fullAccess) {
|
||||
@ -73,7 +74,7 @@ export default {
|
||||
method: 'POST',
|
||||
url: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||
});
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${body.account_id}`,
|
||||
name: body.name.display_name,
|
||||
imageUrl: body.profile_photo_url || '',
|
||||
@ -96,8 +97,10 @@ export default {
|
||||
store.dispatch('data/addDropboxToken', token);
|
||||
return token;
|
||||
},
|
||||
addAccount(fullAccess = false) {
|
||||
return this.startOauth2(fullAccess);
|
||||
async addAccount(fullAccess = false) {
|
||||
const token = await this.startOauth2(fullAccess);
|
||||
badgeSvc.addBadge('addDropboxAccount');
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import userSvc from '../../userSvc';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const clientId = GITHUB_CLIENT_ID;
|
||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||
@ -90,7 +91,7 @@ export default {
|
||||
access_token: accessToken,
|
||||
},
|
||||
})).body;
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${user.id}`,
|
||||
name: user.login,
|
||||
imageUrl: user.avatar_url || '',
|
||||
@ -107,7 +108,7 @@ export default {
|
||||
accessToken,
|
||||
name: user.login,
|
||||
sub: `${user.id}`,
|
||||
repoFullAccess: scopes.indexOf('repo') !== -1,
|
||||
repoFullAccess: scopes.includes('repo'),
|
||||
};
|
||||
|
||||
// Add token to github tokens
|
||||
@ -115,7 +116,9 @@ export default {
|
||||
return token;
|
||||
},
|
||||
async addAccount(repoFullAccess = false) {
|
||||
return this.startOauth2(getScopes({ repoFullAccess }));
|
||||
const token = await this.startOauth2(getScopes({ repoFullAccess }));
|
||||
badgeSvc.addBadge('addGitHubAccount');
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import userSvc from '../../userSvc';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
|
||||
...options,
|
||||
@ -65,7 +66,7 @@ export default {
|
||||
url: 'user',
|
||||
});
|
||||
const uniqueSub = `${serverUrl}/${user.id}`;
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${uniqueSub}`,
|
||||
name: user.username,
|
||||
imageUrl: user.avatar_url || '',
|
||||
@ -88,8 +89,10 @@ export default {
|
||||
store.dispatch('data/addGitlabToken', token);
|
||||
return token;
|
||||
},
|
||||
addAccount(serverUrl, applicationId, sub = null) {
|
||||
return this.startOauth2(serverUrl, applicationId, sub);
|
||||
async addAccount(serverUrl, applicationId, sub = null) {
|
||||
const token = await this.startOauth2(serverUrl, applicationId, sub);
|
||||
badgeSvc.addBadge('addGitLabAccount');
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -2,6 +2,7 @@ import utils from '../../utils';
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import userSvc from '../../userSvc';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const clientId = GOOGLE_CLIENT_ID;
|
||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||
@ -39,8 +40,8 @@ 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
|
||||
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
|
||||
const { body } = await networkSvc.request(sub === 'me' && token
|
||||
? {
|
||||
method: 'GET',
|
||||
url,
|
||||
@ -50,7 +51,7 @@ const getUser = async (sub, token) => {
|
||||
}
|
||||
: {
|
||||
method: 'GET',
|
||||
url: `${url}&key=${apiKey}`,
|
||||
url,
|
||||
}, true);
|
||||
return body;
|
||||
};
|
||||
@ -60,8 +61,8 @@ 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] || {};
|
||||
const name = (body.names && body.names[0]) || {};
|
||||
const photo = (body.photos && body.photos[0]) || {};
|
||||
return {
|
||||
id: `${subPrefix}:${sub}`,
|
||||
name: name.displayName,
|
||||
@ -150,15 +151,15 @@ export default {
|
||||
expiresOn: Date.now() + (expiresIn * 1000),
|
||||
idToken,
|
||||
sub: body.sub,
|
||||
name: (existingToken || {}).name || 'Unknown',
|
||||
name: (existingToken || {}).name || 'Someone',
|
||||
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
||||
scopes.includes('https://www.googleapis.com/auth/drive.appdata'),
|
||||
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,
|
||||
isDrive: scopes.includes('https://www.googleapis.com/auth/drive') ||
|
||||
scopes.includes('https://www.googleapis.com/auth/drive.file'),
|
||||
isBlogger: scopes.includes('https://www.googleapis.com/auth/blogger'),
|
||||
isPhotos: scopes.includes('https://www.googleapis.com/auth/photos'),
|
||||
driveFullAccess: scopes.includes('https://www.googleapis.com/auth/drive'),
|
||||
};
|
||||
|
||||
// Call the user info endpoint
|
||||
@ -169,7 +170,7 @@ export default {
|
||||
if (name.displayName) {
|
||||
token.name = name.displayName;
|
||||
}
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${userId}`,
|
||||
name: name.displayName,
|
||||
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
||||
@ -191,13 +192,17 @@ export default {
|
||||
|
||||
if (token.isLogin) {
|
||||
try {
|
||||
token.isSponsor = (await networkSvc.request({
|
||||
const res = await networkSvc.request({
|
||||
method: 'GET',
|
||||
url: 'userInfo',
|
||||
params: {
|
||||
idToken: token.idToken,
|
||||
},
|
||||
})).body.sponsorUntil > Date.now();
|
||||
});
|
||||
token.isSponsor = res.body.sponsorUntil > Date.now();
|
||||
if (token.isSponsor) {
|
||||
badgeSvc.addBadge('sponsor');
|
||||
}
|
||||
} catch (err) {
|
||||
// Ignore
|
||||
}
|
||||
@ -245,14 +250,20 @@ export default {
|
||||
signin() {
|
||||
return this.startOauth2(driveAppDataScopes);
|
||||
},
|
||||
addDriveAccount(fullAccess = false, sub = null) {
|
||||
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
|
||||
async addDriveAccount(fullAccess = false, sub = null) {
|
||||
const token = await this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
|
||||
badgeSvc.addBadge('addGoogleDriveAccount');
|
||||
return token;
|
||||
},
|
||||
addBloggerAccount() {
|
||||
return this.startOauth2(bloggerScopes);
|
||||
async addBloggerAccount() {
|
||||
const token = await this.startOauth2(bloggerScopes);
|
||||
badgeSvc.addBadge('addBloggerAccount');
|
||||
return token;
|
||||
},
|
||||
addPhotosAccount() {
|
||||
return this.startOauth2(photosScopes);
|
||||
async addPhotosAccount() {
|
||||
const token = await this.startOauth2(photosScopes);
|
||||
badgeSvc.addBadge('addGooglePhotosAccount');
|
||||
return token;
|
||||
},
|
||||
async getSponsorship(token) {
|
||||
const refreshedToken = await this.refreshToken(token);
|
||||
@ -296,10 +307,10 @@ export default {
|
||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||
if (parents && oldParents) {
|
||||
params.addParents = parents
|
||||
.filter(parent => oldParents.indexOf(parent) === -1)
|
||||
.filter(parent => !oldParents.includes(parent))
|
||||
.join(',');
|
||||
params.removeParents = oldParents
|
||||
.filter(parent => parents.indexOf(parent) === -1)
|
||||
.filter(parent => !parents.includes(parent))
|
||||
.join(',');
|
||||
}
|
||||
} else if (parents) {
|
||||
@ -457,7 +468,7 @@ export default {
|
||||
},
|
||||
});
|
||||
revisions.forEach((revision) => {
|
||||
userSvc.addInfo({
|
||||
userSvc.addUserInfo({
|
||||
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
|
||||
name: revision.lastModifyingUser.displayName,
|
||||
imageUrl: revision.lastModifyingUser.photoLink || '',
|
||||
|
@ -1,5 +1,6 @@
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const clientId = '23361';
|
||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
||||
@ -63,8 +64,10 @@ export default {
|
||||
});
|
||||
return this.startOauth2(sub);
|
||||
},
|
||||
addAccount(fullAccess = false) {
|
||||
return this.startOauth2(fullAccess);
|
||||
async addAccount(fullAccess = false) {
|
||||
const token = await this.startOauth2(fullAccess);
|
||||
badgeSvc.addBadge('addWordpressAccount');
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,6 @@
|
||||
import networkSvc from '../../networkSvc';
|
||||
import store from '../../../store';
|
||||
import badgeSvc from '../../badgeSvc';
|
||||
|
||||
const request = (token, options) => networkSvc.request({
|
||||
...options,
|
||||
@ -49,8 +50,10 @@ export default {
|
||||
store.dispatch('data/addZendeskToken', token);
|
||||
return token;
|
||||
},
|
||||
addAccount(subdomain, clientId) {
|
||||
return this.startOauth2(subdomain, clientId);
|
||||
async addAccount(subdomain, clientId) {
|
||||
const token = await this.startOauth2(subdomain, clientId);
|
||||
badgeSvc.addBadge('addZendeskAccount');
|
||||
return token;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -5,6 +5,7 @@ import networkSvc from './networkSvc';
|
||||
import exportSvc from './exportSvc';
|
||||
import providerRegistry from './providers/common/providerRegistry';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import badgeSvc from './badgeSvc';
|
||||
|
||||
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||
|
||||
@ -112,10 +113,11 @@ const requestPublish = () => {
|
||||
if (networkSvc.isUserActive()) {
|
||||
clearInterval(intervalId);
|
||||
if (!hasCurrentFilePublishLocations()) {
|
||||
// Cancel sync
|
||||
// Cancel publish
|
||||
throw new Error('Publish not possible.');
|
||||
}
|
||||
await publishFile(store.getters['file/current'].id);
|
||||
badgeSvc.addBadge('triggerPublish');
|
||||
}
|
||||
};
|
||||
intervalId = utils.setInterval(() => attempt(), 1000);
|
||||
@ -123,7 +125,7 @@ const requestPublish = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const createPublishLocation = (publishLocation) => {
|
||||
const createPublishLocation = (publishLocation, featureId) => {
|
||||
const currentFile = store.getters['file/current'];
|
||||
publishLocation.fileId = currentFile.id;
|
||||
store.dispatch(
|
||||
@ -132,6 +134,9 @@ const createPublishLocation = (publishLocation) => {
|
||||
const publishLocationToStore = await publish(publishLocation);
|
||||
workspaceSvc.addPublishLocation(publishLocationToStore);
|
||||
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
|
||||
if (featureId) {
|
||||
badgeSvc.addBadge(featureId);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
@ -1,55 +0,0 @@
|
||||
import store from '../store';
|
||||
import networkSvc from './networkSvc';
|
||||
import utils from './utils';
|
||||
|
||||
const checkPaymentEvery = 15 * 60 * 1000; // 15 min
|
||||
let lastCheck = 0;
|
||||
|
||||
const appId = 'ESTHdCYOi18iLhhO';
|
||||
let monetize;
|
||||
|
||||
const getMonetize = async () => {
|
||||
await networkSvc.loadScript('https://cdn.monetizejs.com/api/js/latest/monetize.min.js');
|
||||
monetize = monetize || new window.MonetizeJS({
|
||||
applicationID: appId,
|
||||
});
|
||||
};
|
||||
|
||||
const isGoogleSponsor = () => {
|
||||
const sponsorToken = store.getters['workspace/sponsorToken'];
|
||||
return sponsorToken && sponsorToken.isSponsor;
|
||||
};
|
||||
|
||||
const checkPayment = async () => {
|
||||
const currentDate = Date.now();
|
||||
if (!isGoogleSponsor()
|
||||
&& networkSvc.isUserActive()
|
||||
&& !store.state.offline
|
||||
&& !store.state.light
|
||||
&& lastCheck + checkPaymentEvery < currentDate
|
||||
) {
|
||||
lastCheck = currentDate;
|
||||
await getMonetize();
|
||||
monetize.getPaymentsImmediate((err, payments) => {
|
||||
const isSponsor = payments && payments.app === appId && (
|
||||
(payments.chargeOption && payments.chargeOption.alias === 'once') ||
|
||||
(payments.subscriptionOption && payments.subscriptionOption.alias === 'yearly'));
|
||||
if (isSponsor !== store.state.monetizeSponsor) {
|
||||
store.commit('setMonetizeSponsor', isSponsor);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export default {
|
||||
init: () => {
|
||||
utils.setInterval(checkPayment, 2000);
|
||||
},
|
||||
async getToken() {
|
||||
if (isGoogleSponsor() || store.state.offline) {
|
||||
return null;
|
||||
}
|
||||
await getMonetize();
|
||||
return new Promise(resolve => monetize.getTokenImmediate((err, result) => resolve(result)));
|
||||
},
|
||||
};
|
@ -12,6 +12,7 @@ import './providers/googleDriveWorkspaceProvider';
|
||||
import tempFileSvc from './tempFileSvc';
|
||||
import workspaceSvc from './workspaceSvc';
|
||||
import constants from '../data/constants';
|
||||
import badgeSvc from './badgeSvc';
|
||||
|
||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||
@ -455,7 +456,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||
if (serverContent &&
|
||||
(serverContent.hash === newSyncHistoryItem[LAST_SEEN] ||
|
||||
serverContent.history.indexOf(newSyncHistoryItem[LAST_SEEN]) !== -1)
|
||||
serverContent.history.includes(newSyncHistoryItem[LAST_SEEN]))
|
||||
) {
|
||||
// That's the 2nd time we've seen this content, trust it for future merges
|
||||
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN];
|
||||
@ -723,10 +724,11 @@ const syncWorkspace = async (skipContents = false) => {
|
||||
return true;
|
||||
}));
|
||||
|
||||
// Sync settings and workspaces only in the main workspace
|
||||
// Sync settings, workspaces and badges only in the main workspace
|
||||
if (workspace.id === 'main') {
|
||||
await syncDataItem('settings');
|
||||
await syncDataItem('workspaces');
|
||||
await syncDataItem('badges');
|
||||
}
|
||||
await syncDataItem('templates');
|
||||
|
||||
@ -786,6 +788,10 @@ const syncWorkspace = async (skipContents = false) => {
|
||||
if (syncContext.restartSkipContents) {
|
||||
await syncWorkspace(true);
|
||||
}
|
||||
|
||||
if (workspace.id === 'main') {
|
||||
badgeSvc.addBadge('syncMainWorkspace');
|
||||
}
|
||||
} catch (err) {
|
||||
if (err && err.message === 'TOO_LATE') {
|
||||
// Restart sync
|
||||
@ -799,7 +805,7 @@ const syncWorkspace = async (skipContents = false) => {
|
||||
/**
|
||||
* Enqueue a sync task, if possible.
|
||||
*/
|
||||
const requestSync = () => {
|
||||
const requestSync = (addTriggerSyncBadge = false) => {
|
||||
// No sync in light mode
|
||||
if (store.state.light) {
|
||||
return;
|
||||
@ -851,6 +857,10 @@ const requestSync = () => {
|
||||
workspaceSvc.deleteFile(fileId);
|
||||
}
|
||||
});
|
||||
|
||||
if (addTriggerSyncBadge) {
|
||||
badgeSvc.addBadge('triggerSync');
|
||||
}
|
||||
} finally {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
|
@ -1,16 +1,47 @@
|
||||
import store from '../store';
|
||||
import utils from './utils';
|
||||
|
||||
const refreshUserInfoAfter = 60 * 60 * 1000; // 60 minutes
|
||||
|
||||
const infoPromisesByUserId = {};
|
||||
const infoResolversByType = {};
|
||||
const subPrefixesByType = {};
|
||||
const typesBySubPrefix = {};
|
||||
|
||||
const parseUserId = (userId) => {
|
||||
const lastInfosByUserId = {};
|
||||
const infoPromisedByUserId = {};
|
||||
|
||||
const sanitizeUserId = (userId) => {
|
||||
const prefix = userId[2] === ':' && userId.slice(0, 2);
|
||||
const type = typesBySubPrefix[prefix];
|
||||
return type ? [type, userId.slice(3)] : ['google', userId];
|
||||
if (typesBySubPrefix[prefix]) {
|
||||
return userId;
|
||||
}
|
||||
return `go:${userId}`;
|
||||
};
|
||||
|
||||
const parseUserId = userId => [typesBySubPrefix[userId.slice(0, 2)], userId.slice(3)];
|
||||
|
||||
const refreshUserInfos = () => {
|
||||
if (store.state.offline) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(lastInfosByUserId)
|
||||
.filter(([userId, lastInfo]) => lastInfo === 0 && !infoPromisedByUserId[userId])
|
||||
.forEach(async ([userId]) => {
|
||||
const [type, sub] = parseUserId(userId);
|
||||
const infoResolver = infoResolversByType[type];
|
||||
if (infoResolver) {
|
||||
try {
|
||||
infoPromisedByUserId[userId] = true;
|
||||
const userInfo = await infoResolver(sub);
|
||||
store.commit('userInfo/setItem', userInfo);
|
||||
} finally {
|
||||
infoPromisedByUserId[userId] = false;
|
||||
lastInfosByUserId[userId] = Date.now();
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export default {
|
||||
setInfoResolver(type, subPrefix, resolver) {
|
||||
@ -27,53 +58,34 @@ export default {
|
||||
const prefix = subPrefixesByType[loginType];
|
||||
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
|
||||
},
|
||||
addInfo(info) {
|
||||
infoPromisesByUserId[info.id] = Promise.resolve(info);
|
||||
store.commit('userInfo/addItem', info);
|
||||
sanitizeUserId,
|
||||
addUserInfo(userInfo) {
|
||||
store.commit('userInfo/setItem', userInfo);
|
||||
lastInfosByUserId[userInfo.id] = Date.now();
|
||||
},
|
||||
async getInfo(userId) {
|
||||
if (!userId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let infoPromise = infoPromisesByUserId[userId];
|
||||
if (infoPromise) {
|
||||
return infoPromise;
|
||||
}
|
||||
|
||||
const [type, sub] = parseUserId(userId);
|
||||
|
||||
addUserId(userId) {
|
||||
if (userId) {
|
||||
const sanitizedUserId = sanitizeUserId(userId);
|
||||
const lastInfo = lastInfosByUserId[sanitizedUserId];
|
||||
if (lastInfo === undefined) {
|
||||
// Try to find a token with this sub to resolve name as soon as possible
|
||||
const [type, sub] = parseUserId(sanitizedUserId);
|
||||
const token = store.getters['data/tokensByType'][type][sub];
|
||||
if (token) {
|
||||
store.commit('userInfo/addItem', {
|
||||
id: userId,
|
||||
store.commit('userInfo/setItem', {
|
||||
id: sanitizedUserId,
|
||||
name: token.name,
|
||||
});
|
||||
}
|
||||
|
||||
if (store.state.offline) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// Get user info from helper
|
||||
infoPromise = new Promise(async (resolve) => {
|
||||
const infoResolver = infoResolversByType[type];
|
||||
if (infoResolver) {
|
||||
try {
|
||||
const userInfo = await infoResolver(sub);
|
||||
this.addInfo(userInfo);
|
||||
resolve(userInfo);
|
||||
} catch (err) {
|
||||
if (err && err.message === 'RETRY') {
|
||||
infoPromisesByUserId[userId] = null;
|
||||
}
|
||||
resolve({});
|
||||
if (lastInfo === undefined || lastInfo + refreshUserInfoAfter < Date.now()) {
|
||||
lastInfosByUserId[sanitizedUserId] = 0;
|
||||
refreshUserInfos();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
infoPromisesByUserId[userId] = infoPromise;
|
||||
return infoPromise;
|
||||
},
|
||||
};
|
||||
|
||||
// Get user info periodically
|
||||
utils.setInterval(() => refreshUserInfos(), 60 * 1000);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import store from '../store';
|
||||
import utils from './utils';
|
||||
import constants from '../data/constants';
|
||||
import badgeSvc from './badgeSvc';
|
||||
|
||||
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
||||
|
||||
@ -249,8 +250,13 @@ export default {
|
||||
...location,
|
||||
id: utils.uid(),
|
||||
});
|
||||
|
||||
// Sanitize the workspace
|
||||
this.ensureUniqueLocations();
|
||||
|
||||
if (Object.keys(store.getters['syncLocation/currentWithWorkspaceSyncLocation']).length > 1) {
|
||||
badgeSvc.addBadge('syncMultipleLocations');
|
||||
}
|
||||
},
|
||||
|
||||
addPublishLocation(location) {
|
||||
@ -258,8 +264,13 @@ export default {
|
||||
...location,
|
||||
id: utils.uid(),
|
||||
});
|
||||
|
||||
// Sanitize the workspace
|
||||
this.ensureUniqueLocations();
|
||||
|
||||
if (Object.keys(store.getters['publishLocation/current']).length > 1) {
|
||||
badgeSvc.addBadge('publishMultipleLocations');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -3,6 +3,7 @@ import moduleTemplate from './moduleTemplate';
|
||||
import empty from '../data/empties/emptyContent';
|
||||
import utils from '../services/utils';
|
||||
import cledit from '../services/editor/cledit';
|
||||
import badgeSvc from '../services/badgeSvc';
|
||||
|
||||
const diffMatchPatch = new DiffMatchPatch();
|
||||
|
||||
@ -104,6 +105,7 @@ module.actions = {
|
||||
...currentContent,
|
||||
text: revisionContent.originalText,
|
||||
});
|
||||
badgeSvc.addBadge('restoreVersion');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -10,6 +10,7 @@ import styledHtmlTemplate from '../data/templates/styledHtmlTemplate.html';
|
||||
import styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTemplate.html';
|
||||
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
||||
import constants from '../data/constants';
|
||||
import features from '../data/features';
|
||||
|
||||
const itemTemplate = (id, data = {}) => ({
|
||||
id,
|
||||
@ -203,7 +204,19 @@ export default {
|
||||
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
||||
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
||||
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
||||
badgesByFeatureId: getter('badges'),
|
||||
badges: getter('badges'),
|
||||
badgeTree: (state, { badges }) => features.map(feature => feature.toBadge(badges)),
|
||||
allBadges: (state, { badgeTree }) => {
|
||||
const result = [];
|
||||
const processBadgeNodes = nodes => nodes.forEach((node) => {
|
||||
result.push(node);
|
||||
if (node.children) {
|
||||
processBadgeNodes(node.children);
|
||||
}
|
||||
});
|
||||
processBadgeNodes(badgeTree);
|
||||
return result;
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
setSettings: setter('settings'),
|
||||
@ -275,5 +288,6 @@ export default {
|
||||
addGitlabToken: tokenAdder('gitlab'),
|
||||
addWordpressToken: tokenAdder('wordpress'),
|
||||
addZendeskToken: tokenAdder('zendesk'),
|
||||
patchBadges: patcher('badges'),
|
||||
},
|
||||
};
|
||||
|
@ -3,7 +3,8 @@ import googleHelper from '../services/providers/helpers/googleHelper';
|
||||
import syncSvc from '../services/syncSvc';
|
||||
|
||||
const idShifter = offset => (state, getters) => {
|
||||
const ids = Object.keys(getters.currentFileDiscussions);
|
||||
const ids = Object.keys(getters.currentFileDiscussions)
|
||||
.filter(id => id !== state.newDiscussionId);
|
||||
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
|
||||
return ids[idx % ids.length];
|
||||
};
|
||||
|
@ -52,8 +52,7 @@ const store = new Vuex.Store({
|
||||
light: false,
|
||||
offline: false,
|
||||
lastOfflineCheck: 0,
|
||||
minuteCounter: 0,
|
||||
monetizeSponsor: false,
|
||||
timeCounter: 0,
|
||||
},
|
||||
mutations: {
|
||||
setLight: (state, value) => {
|
||||
@ -65,14 +64,8 @@ const store = new Vuex.Store({
|
||||
updateLastOfflineCheck: (state) => {
|
||||
state.lastOfflineCheck = Date.now();
|
||||
},
|
||||
updateMinuteCounter: (state) => {
|
||||
state.minuteCounter += 1;
|
||||
},
|
||||
setMonetizeSponsor: (state, value) => {
|
||||
state.monetizeSponsor = value;
|
||||
},
|
||||
setGoogleSponsor: (state, value) => {
|
||||
state.googleSponsor = value;
|
||||
updateTimeCounter: (state) => {
|
||||
state.timeCounter += 1;
|
||||
},
|
||||
},
|
||||
getters: {
|
||||
@ -161,9 +154,9 @@ const store = new Vuex.Store({
|
||||
});
|
||||
return result;
|
||||
},
|
||||
isSponsor: ({ light, monetizeSponsor }, getters) => {
|
||||
isSponsor: ({ light }, getters) => {
|
||||
const sponsorToken = getters['workspace/sponsorToken'];
|
||||
return light || monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
||||
return light || (sponsorToken && sponsorToken.isSponsor);
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
@ -183,7 +176,7 @@ const store = new Vuex.Store({
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
store.commit('updateMinuteCounter');
|
||||
}, 60 * 1000);
|
||||
store.commit('updateTimeCounter');
|
||||
}, 30 * 1000);
|
||||
|
||||
export default store;
|
||||
|
@ -10,7 +10,7 @@ pagedownButtons.forEach((button) => {
|
||||
}
|
||||
});
|
||||
|
||||
const minPadding = 20;
|
||||
const minPadding = 25;
|
||||
const editorTopPadding = 10;
|
||||
const navigationBarEditButtonsWidth = (34 * buttonCount) + (8 * spacerCount); // buttons + spacers
|
||||
const navigationBarLeftButtonWidth = 38 + 4 + 12;
|
||||
|
@ -45,16 +45,22 @@ export default {
|
||||
|
||||
return item.promise;
|
||||
},
|
||||
info({ dispatch }, info) {
|
||||
info({ dispatch }, content) {
|
||||
return dispatch('showItem', {
|
||||
type: 'info',
|
||||
content: info,
|
||||
content,
|
||||
});
|
||||
},
|
||||
confirm({ dispatch }, question) {
|
||||
badge({ dispatch }, content) {
|
||||
return dispatch('showItem', {
|
||||
type: 'badge',
|
||||
content,
|
||||
});
|
||||
},
|
||||
confirm({ dispatch }, content) {
|
||||
return dispatch('showItem', {
|
||||
type: 'confirm',
|
||||
content: question,
|
||||
content,
|
||||
timeout: 10000, // 10 sec
|
||||
});
|
||||
},
|
||||
|
@ -6,8 +6,20 @@ export default {
|
||||
itemsById: {},
|
||||
},
|
||||
mutations: {
|
||||
addItem: ({ itemsById }, item) => {
|
||||
Vue.set(itemsById, item.id, item);
|
||||
setItem: ({ itemsById }, item) => {
|
||||
const itemToSet = {
|
||||
...item,
|
||||
};
|
||||
const existingItem = itemsById[item.id];
|
||||
if (existingItem) {
|
||||
if (!itemToSet.name) {
|
||||
itemToSet.name = existingItem.name;
|
||||
}
|
||||
if (!itemToSet.imageUrl) {
|
||||
itemToSet.imageUrl = existingItem.imageUrl;
|
||||
}
|
||||
}
|
||||
Vue.set(itemsById, item.id, itemToSet);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -273,10 +273,6 @@ textarea {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
|
||||
& > * {
|
||||
border-left: 2px solid transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.gutter__background {
|
||||
@ -289,8 +285,8 @@ textarea {
|
||||
color: rgba(0, 0, 0, 0.33);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
padding: 2px 3px 2px 0;
|
||||
width: 20px;
|
||||
padding: 3px 3px 3px 0;
|
||||
width: 22px;
|
||||
height: 21px;
|
||||
line-height: 1;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user