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)
|
const outputFormat = Object.prototype.hasOwnProperty.call(outputFormats, req.query.format)
|
||||||
? req.query.format
|
? req.query.format
|
||||||
: 'pdf';
|
: 'pdf';
|
||||||
Promise.all([
|
user.checkSponsor(req.query.idToken)
|
||||||
user.checkSponsor(req.query.idToken),
|
.then((isSponsor) => {
|
||||||
user.checkMonetize(req.query.token),
|
if (!isSponsor) {
|
||||||
])
|
|
||||||
.then(([isSponsor, isMonetize]) => {
|
|
||||||
if (!isSponsor && !isMonetize) {
|
|
||||||
throw new Error('unauthorized');
|
throw new Error('unauthorized');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +76,7 @@ exports.generate = (req, res) => {
|
|||||||
if (!Number.isNaN(options.tocDepth)) {
|
if (!Number.isNaN(options.tocDepth)) {
|
||||||
params.push('--toc-depth', 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);
|
params.push('--highlight-style', options.highlightStyle);
|
||||||
Object.keys(metadata).forEach((key) => {
|
Object.keys(metadata).forEach((key) => {
|
||||||
params.push('-M', `${key}=${metadata[key]}`);
|
params.push('-M', `${key}=${metadata[key]}`);
|
||||||
|
@ -50,12 +50,9 @@ const readJson = (str) => {
|
|||||||
|
|
||||||
exports.generate = (req, res) => {
|
exports.generate = (req, res) => {
|
||||||
let wkhtmltopdfError = '';
|
let wkhtmltopdfError = '';
|
||||||
Promise.all([
|
user.checkSponsor(req.query.idToken)
|
||||||
user.checkSponsor(req.query.idToken),
|
.then((isSponsor) => {
|
||||||
user.checkMonetize(req.query.token),
|
if (!isSponsor) {
|
||||||
])
|
|
||||||
.then(([isSponsor, isMonetize]) => {
|
|
||||||
if (!isSponsor && !isMonetize) {
|
|
||||||
throw new Error('unauthorized');
|
throw new Error('unauthorized');
|
||||||
}
|
}
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -127,7 +124,7 @@ exports.generate = (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Page size
|
// 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
|
// Use a temp file as wkhtmltopdf can't access /dev/stdout on Amazon EC2 for some reason
|
||||||
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
const binPath = process.env.WKHTMLTOPDF_PATH || 'wkhtmltopdf';
|
||||||
|
@ -116,21 +116,3 @@ exports.checkSponsor = (idToken) => {
|
|||||||
return exports.getUserFromToken(idToken)
|
return exports.getUserFromToken(idToken)
|
||||||
.then(userInfo => userInfo && userInfo.sponsorUntil > Date.now(), () => false);
|
.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 SplashScreen from './SplashScreen';
|
||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
import networkSvc from '../services/networkSvc';
|
import networkSvc from '../services/networkSvc';
|
||||||
import sponsorSvc from '../services/sponsorSvc';
|
|
||||||
import tempFileSvc from '../services/tempFileSvc';
|
import tempFileSvc from '../services/tempFileSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import './common/vueGlobals';
|
import './common/vueGlobals';
|
||||||
@ -55,7 +54,6 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await syncSvc.init();
|
await syncSvc.init();
|
||||||
await networkSvc.init();
|
await networkSvc.init();
|
||||||
await sponsorSvc.init();
|
|
||||||
this.ready = true;
|
this.ready = true;
|
||||||
tempFileSvc.setReady();
|
tempFileSvc.setReady();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
@ -22,6 +22,7 @@ import { mapMutations, mapActions } from 'vuex';
|
|||||||
import workspaceSvc from '../services/workspaceSvc';
|
import workspaceSvc from '../services/workspaceSvc';
|
||||||
import explorerSvc from '../services/explorerSvc';
|
import explorerSvc from '../services/explorerSvc';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
import badgeSvc from '../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'explorer-node', // Required for recursivity
|
name: 'explorer-node', // Required for recursivity
|
||||||
@ -81,7 +82,7 @@ export default {
|
|||||||
]),
|
]),
|
||||||
select(id = this.node.item.id, doOpen = true) {
|
select(id = this.node.item.id, doOpen = true) {
|
||||||
const node = store.getters['explorer/nodeMap'][id];
|
const node = store.getters['explorer/nodeMap'][id];
|
||||||
if (!node) {
|
if (!node || node.item.id === store.state.explorer.selectedId) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
store.commit('explorer/setSelectedId', id);
|
store.commit('explorer/setSelectedId', id);
|
||||||
@ -92,6 +93,7 @@ export default {
|
|||||||
store.commit('explorer/toggleOpenNode', id);
|
store.commit('explorer/toggleOpenNode', id);
|
||||||
} else {
|
} else {
|
||||||
store.commit('file/setCurrentId', id);
|
store.commit('file/setCurrentId', id);
|
||||||
|
badgeSvc.addBadge('switchFile');
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
@ -104,9 +106,11 @@ export default {
|
|||||||
if (newChildNode.isFolder) {
|
if (newChildNode.isFolder) {
|
||||||
const item = await workspaceSvc.storeItem(newChildNode.item);
|
const item = await workspaceSvc.storeItem(newChildNode.item);
|
||||||
this.select(item.id);
|
this.select(item.id);
|
||||||
|
badgeSvc.addBadge('createFolder');
|
||||||
} else {
|
} else {
|
||||||
const item = await workspaceSvc.createFile(newChildNode.item);
|
const item = await workspaceSvc.createFile(newChildNode.item);
|
||||||
this.select(item.id);
|
this.select(item.id);
|
||||||
|
badgeSvc.addBadge('createFile');
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancel
|
// Cancel
|
||||||
@ -115,15 +119,16 @@ export default {
|
|||||||
store.commit('explorer/setNewItem', null);
|
store.commit('explorer/setNewItem', null);
|
||||||
},
|
},
|
||||||
async submitEdit(cancel) {
|
async submitEdit(cancel) {
|
||||||
const { item } = store.getters['explorer/editingNode'];
|
const { item, isFolder } = store.getters['explorer/editingNode'];
|
||||||
const value = this.editingValue;
|
const value = this.editingValue;
|
||||||
this.setEditingId(null);
|
this.setEditingId(null);
|
||||||
if (!cancel && item.id && value) {
|
if (!cancel && item.id && value && item.name !== value) {
|
||||||
try {
|
try {
|
||||||
await workspaceSvc.storeItem({
|
await workspaceSvc.storeItem({
|
||||||
...item,
|
...item,
|
||||||
name: value,
|
name: value,
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge(isFolder ? 'renameFolder' : 'renameFile');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancel
|
// Cancel
|
||||||
}
|
}
|
||||||
@ -151,6 +156,7 @@ export default {
|
|||||||
...sourceNode.item,
|
...sourceNode.item,
|
||||||
parentId: targetNode.item.id,
|
parentId: targetNode.item.id,
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge('moveFiles');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async onContextMenu(evt) {
|
async onContextMenu(evt) {
|
||||||
|
@ -177,11 +177,9 @@ export default {
|
|||||||
.sticky-comment,
|
.sticky-comment,
|
||||||
.current-discussion {
|
.current-discussion {
|
||||||
background-color: mix(#000, $editor-background-light, 6.7%);
|
background-color: mix(#000, $editor-background-light, 6.7%);
|
||||||
border-color: $editor-background-light;
|
|
||||||
|
|
||||||
.app--dark & {
|
.app--dark & {
|
||||||
background-color: mix(#fff, $editor-background-dark, 6.7%);
|
background-color: mix(#fff, $editor-background-dark, 6.7%);
|
||||||
border-color: $editor-background-dark;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,7 +202,6 @@ $preview-background-dark: #252525;
|
|||||||
.sticky-comment,
|
.sticky-comment,
|
||||||
.current-discussion {
|
.current-discussion {
|
||||||
background-color: mix(#000, $preview-background-light, 6.7%);
|
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 PublishManagementModal from './modals/PublishManagementModal';
|
||||||
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
import WorkspaceManagementModal from './modals/WorkspaceManagementModal';
|
||||||
import AccountManagementModal from './modals/AccountManagementModal';
|
import AccountManagementModal from './modals/AccountManagementModal';
|
||||||
|
import BadgeManagementModal from './modals/BadgeManagementModal';
|
||||||
import SponsorModal from './modals/SponsorModal';
|
import SponsorModal from './modals/SponsorModal';
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
@ -88,6 +89,7 @@ export default {
|
|||||||
PublishManagementModal,
|
PublishManagementModal,
|
||||||
WorkspaceManagementModal,
|
WorkspaceManagementModal,
|
||||||
AccountManagementModal,
|
AccountManagementModal,
|
||||||
|
BadgeManagementModal,
|
||||||
SponsorModal,
|
SponsorModal,
|
||||||
// Providers
|
// Providers
|
||||||
GooglePhotoModal,
|
GooglePhotoModal,
|
||||||
|
@ -57,6 +57,7 @@ import utils from '../services/utils';
|
|||||||
import pagedownButtons from '../data/pagedownButtons';
|
import pagedownButtons from '../data/pagedownButtons';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
import workspaceSvc from '../services/workspaceSvc';
|
import workspaceSvc from '../services/workspaceSvc';
|
||||||
|
import badgeSvc from '../services/badgeSvc';
|
||||||
|
|
||||||
// According to mousetrap
|
// According to mousetrap
|
||||||
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
const mod = /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'Meta' : 'Ctrl';
|
||||||
@ -178,7 +179,7 @@ export default {
|
|||||||
},
|
},
|
||||||
requestSync() {
|
requestSync() {
|
||||||
if (this.isSyncPossible && !this.isSyncRequested) {
|
if (this.isSyncPossible && !this.isSyncRequested) {
|
||||||
syncSvc.requestSync();
|
syncSvc.requestSync(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
requestPublish() {
|
requestPublish() {
|
||||||
@ -188,7 +189,11 @@ export default {
|
|||||||
},
|
},
|
||||||
pagedownClick(name) {
|
pagedownClick(name) {
|
||||||
if (store.getters['content/isCurrentEditable']) {
|
if (store.getters['content/isCurrentEditable']) {
|
||||||
|
const text = editorSvc.clEditor.getContent();
|
||||||
editorSvc.pagedownEditor.uiManager.doClick(name);
|
editorSvc.pagedownEditor.uiManager.doClick(name);
|
||||||
|
if (text !== editorSvc.clEditor.getContent()) {
|
||||||
|
badgeSvc.addBadge('formatButtons');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async editTitle(toggle) {
|
async editTitle(toggle) {
|
||||||
@ -198,12 +203,13 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
const title = this.title.trim();
|
const title = this.title.trim();
|
||||||
this.title = store.getters['file/current'].name;
|
this.title = store.getters['file/current'].name;
|
||||||
if (title) {
|
if (title && this.title !== title) {
|
||||||
try {
|
try {
|
||||||
await workspaceSvc.storeItem({
|
await workspaceSvc.storeItem({
|
||||||
...store.getters['file/current'],
|
...store.getters['file/current'],
|
||||||
name: title,
|
name: title,
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge('editCurrentFileName');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancel
|
// 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__item flex flex--row flex--align-center" v-for="(item, idx) in items" :key="idx">
|
||||||
<div class="notification__icon flex flex--column flex--center">
|
<div class="notification__icon flex flex--column flex--center">
|
||||||
<icon-alert v-if="item.type === 'error'"></icon-alert>
|
<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>
|
<icon-information v-else></icon-information>
|
||||||
</div>
|
</div>
|
||||||
<div class="notification__content">
|
<div class="notification__content">
|
||||||
|
@ -19,7 +19,7 @@
|
|||||||
<history-menu v-else-if="panel === 'history'"></history-menu>
|
<history-menu v-else-if="panel === 'history'"></history-menu>
|
||||||
<export-menu v-else-if="panel === 'export'"></export-menu>
|
<export-menu v-else-if="panel === 'export'"></export-menu>
|
||||||
<import-export-menu v-else-if="panel === 'importExport'"></import-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">
|
<div v-else-if="panel === 'help'" class="side-bar__panel side-bar__panel--help">
|
||||||
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
<pre class="markdown-highlighting" v-html="markdownSample"></pre>
|
||||||
</div>
|
</div>
|
||||||
@ -54,7 +54,7 @@ const panelNames = {
|
|||||||
publish: 'Publish',
|
publish: 'Publish',
|
||||||
history: 'File history',
|
history: 'File history',
|
||||||
importExport: 'Import/export',
|
importExport: 'Import/export',
|
||||||
workspaceBackup: 'Workspace backup',
|
workspaceBackups: 'Workspace backups',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -174,8 +174,9 @@ export default {
|
|||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 10px;
|
margin: 10px 15px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
font-style: italic;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -10,14 +10,17 @@ import store from '../store';
|
|||||||
export default {
|
export default {
|
||||||
props: ['userId'],
|
props: ['userId'],
|
||||||
computed: {
|
computed: {
|
||||||
|
sanitizedUserId() {
|
||||||
|
return userSvc.sanitizeUserId(this.userId);
|
||||||
|
},
|
||||||
url() {
|
url() {
|
||||||
const userInfo = store.state.userInfo.itemsById[this.userId];
|
const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
|
||||||
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
|
return userInfo && userInfo.imageUrl && `url('${userInfo.imageUrl}')`;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
userId: {
|
sanitizedUserId: {
|
||||||
handler: userId => userSvc.getInfo(userId),
|
handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -9,14 +9,17 @@ import store from '../store';
|
|||||||
export default {
|
export default {
|
||||||
props: ['userId'],
|
props: ['userId'],
|
||||||
computed: {
|
computed: {
|
||||||
|
sanitizedUserId() {
|
||||||
|
return userSvc.sanitizeUserId(this.userId);
|
||||||
|
},
|
||||||
name() {
|
name() {
|
||||||
const userInfo = store.state.userInfo.itemsById[this.userId];
|
const userInfo = store.state.userInfo.itemsById[this.sanitizedUserId];
|
||||||
return userInfo ? userInfo.name : 'Someone';
|
return userInfo ? userInfo.name : 'Someone';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
userId: {
|
sanitizedUserId: {
|
||||||
handler: userId => userSvc.getInfo(userId),
|
handler: sanitizedUserId => userSvc.addUserId(sanitizedUserId),
|
||||||
immediate: true,
|
immediate: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -75,6 +75,6 @@ Vue.directive('clipboard', {
|
|||||||
|
|
||||||
// Global filters
|
// Global filters
|
||||||
Vue.filter('formatTime', time =>
|
Vue.filter('formatTime', time =>
|
||||||
// Access the minute counter for reactive refresh
|
// Access the time counter for reactive refresh
|
||||||
timeSvc.format(time, store.state.minuteCounter));
|
timeSvc.format(time, store.state.timeCounter));
|
||||||
|
|
||||||
|
@ -28,6 +28,7 @@ import UserName from '../UserName';
|
|||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import htmlSanitizer from '../../libs/htmlSanitizer';
|
import htmlSanitizer from '../../libs/htmlSanitizer';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -52,6 +53,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await store.dispatch('modal/open', 'commentDeletion');
|
await store.dispatch('modal/open', 'commentDeletion');
|
||||||
store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
|
store.dispatch('discussion/cleanCurrentFile', { filterComment: this.comment });
|
||||||
|
badgeSvc.addBadge('removeComment');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancel
|
// Cancel
|
||||||
}
|
}
|
||||||
|
@ -133,11 +133,6 @@ export default {
|
|||||||
: editorSvc.previewElt.parentNode;
|
: editorSvc.previewElt.parentNode;
|
||||||
|
|
||||||
this.updateSticky = () => {
|
this.updateSticky = () => {
|
||||||
const commitIfDifferent = (value) => {
|
|
||||||
if (store.state.discussion.stickyComment !== value) {
|
|
||||||
store.commit('discussion/setStickyComment', value);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let offsetTop = this.tops.current;
|
let offsetTop = this.tops.current;
|
||||||
const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
|
const lastCommentElt = this.$el.querySelector(`.comment--${this.currentDiscussionLastCommentId}`);
|
||||||
@ -152,13 +147,15 @@ export default {
|
|||||||
const currentDiscussionElt = document.querySelector('.current-discussion__inner');
|
const currentDiscussionElt = document.querySelector('.current-discussion__inner');
|
||||||
const minOffsetTop = this.scrollerElt.scrollTop + 10;
|
const minOffsetTop = this.scrollerElt.scrollTop + 10;
|
||||||
const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
|
const maxOffsetTop = (this.scrollerElt.scrollTop + this.scrollerElt.clientHeight) - height
|
||||||
- currentDiscussionElt.clientHeight - 10;
|
- currentDiscussionElt.clientHeight;
|
||||||
|
let stickyComment = null;
|
||||||
if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
|
if (offsetTop > maxOffsetTop || maxOffsetTop < minOffsetTop) {
|
||||||
commitIfDifferent('bottom');
|
stickyComment = 'bottom';
|
||||||
} else if (offsetTop < minOffsetTop) {
|
} else if (offsetTop < minOffsetTop) {
|
||||||
commitIfDifferent('top');
|
stickyComment = 'top';
|
||||||
} else {
|
}
|
||||||
commitIfDifferent(null);
|
if (store.state.discussion.stickyComment !== stickyComment) {
|
||||||
|
store.commit('discussion/setStickyComment', stickyComment);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -199,19 +196,6 @@ export default {
|
|||||||
padding-top: 10px;
|
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 */
|
/* use div selector to avoid collision with Prism */
|
||||||
div.comment {
|
div.comment {
|
||||||
padding: 5px 10px 10px;
|
padding: 5px 10px 10px;
|
||||||
|
@ -34,6 +34,7 @@ import animationSvc from '../../services/animationSvc';
|
|||||||
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
import markdownConversionSvc from '../../services/markdownConversionSvc';
|
||||||
import StickyComment from './StickyComment';
|
import StickyComment from './StickyComment';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -103,6 +104,7 @@ export default {
|
|||||||
store.dispatch('discussion/cleanCurrentFile', {
|
store.dispatch('discussion/cleanCurrentFile', {
|
||||||
filterDiscussion: this.currentDiscussion,
|
filterDiscussion: this.currentDiscussion,
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge('removeDiscussion');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Cancel
|
// Cancel
|
||||||
}
|
}
|
||||||
@ -118,7 +120,6 @@ export default {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
border-top: 2px solid;
|
|
||||||
|
|
||||||
.sticky-comment {
|
.sticky-comment {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@ -30,6 +30,7 @@ import markdownConversionSvc from '../../services/markdownConversionSvc';
|
|||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
import userSvc from '../../services/userSvc';
|
import userSvc from '../../services/userSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -70,12 +71,15 @@ export default {
|
|||||||
[utils.uid()]: comment,
|
[utils.uid()]: comment,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Create discussion
|
|
||||||
if (discussionId === store.state.discussion.newDiscussionId) {
|
if (discussionId === store.state.discussion.newDiscussionId) {
|
||||||
|
// Create discussion
|
||||||
patch.discussions = {
|
patch.discussions = {
|
||||||
...store.getters['content/current'].discussions,
|
...store.getters['content/current'].discussions,
|
||||||
[discussionId]: store.getters['discussion/newDiscussion'],
|
[discussionId]: store.getters['discussion/newDiscussion'],
|
||||||
};
|
};
|
||||||
|
badgeSvc.addBadge('createDiscussion');
|
||||||
|
} else {
|
||||||
|
badgeSvc.addBadge('addComment');
|
||||||
}
|
}
|
||||||
store.dispatch('content/patchCurrent', patch);
|
store.dispatch('content/patchCurrent', patch);
|
||||||
store.commit('discussion/setNewCommentText');
|
store.commit('discussion/setNewCommentText');
|
||||||
|
@ -40,7 +40,6 @@ export default {
|
|||||||
right: 0;
|
right: 0;
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
border-bottom: 2px solid;
|
|
||||||
|
|
||||||
.current-discussion & {
|
.current-discussion & {
|
||||||
width: auto !important;
|
width: auto !important;
|
||||||
|
@ -56,6 +56,7 @@ import utils from '../../services/utils';
|
|||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
let editorClassAppliers = [];
|
let editorClassAppliers = [];
|
||||||
let previewClassAppliers = [];
|
let previewClassAppliers = [];
|
||||||
@ -237,10 +238,12 @@ export default {
|
|||||||
syncLocation: {
|
syncLocation: {
|
||||||
immediate: true,
|
immediate: true,
|
||||||
handler(value) {
|
handler(value) {
|
||||||
if (!value) {
|
const firstSyncLocation = this.syncLocations[0];
|
||||||
const firstSyncLocation = this.syncLocations[0];
|
if (firstSyncLocation) {
|
||||||
if (firstSyncLocation) {
|
if (!value) {
|
||||||
this.syncLocationId = firstSyncLocation.id;
|
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 store from '../../store';
|
||||||
import workspaceSvc from '../../services/workspaceSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
const turndownService = new TurndownService(store.getters['data/computedSettings'].turndown);
|
||||||
|
|
||||||
@ -85,6 +86,7 @@ export default {
|
|||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
|
badgeSvc.addBadge('importMarkdown');
|
||||||
},
|
},
|
||||||
async onImportHtml(evt) {
|
async onImportHtml(evt) {
|
||||||
const file = evt.target.files[0];
|
const file = evt.target.files[0];
|
||||||
@ -96,23 +98,29 @@ export default {
|
|||||||
name: file.name,
|
name: file.name,
|
||||||
});
|
});
|
||||||
store.commit('file/setCurrentId', item.id);
|
store.commit('file/setCurrentId', item.id);
|
||||||
|
badgeSvc.addBadge('importHtml');
|
||||||
},
|
},
|
||||||
exportMarkdown() {
|
async exportMarkdown() {
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
return exportSvc.exportToDisk(currentFile.id, 'md')
|
try {
|
||||||
.catch(() => { /* Cancel */ });
|
await exportSvc.exportToDisk(currentFile.id, 'md');
|
||||||
|
badgeSvc.addBadge('exportMarkdown');
|
||||||
|
} catch (e) { /* Cancel */ }
|
||||||
},
|
},
|
||||||
exportHtml() {
|
async exportHtml() {
|
||||||
return store.dispatch('modal/open', 'htmlExport')
|
try {
|
||||||
.catch(() => { /* Cancel */ });
|
await store.dispatch('modal/open', 'htmlExport');
|
||||||
|
} catch (e) { /* Cancel */ }
|
||||||
},
|
},
|
||||||
exportPdf() {
|
async exportPdf() {
|
||||||
return store.dispatch('modal/open', 'pdfExport')
|
try {
|
||||||
.catch(() => { /* Cancel */ });
|
await store.dispatch('modal/open', 'pdfExport');
|
||||||
|
} catch (e) { /* Cancel */ }
|
||||||
},
|
},
|
||||||
exportPandoc() {
|
async exportPandoc() {
|
||||||
return store.dispatch('modal/open', 'pandocExport')
|
try {
|
||||||
.catch(() => { /* Cancel */ });
|
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>
|
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
|
||||||
<span>Manage access to your external accounts.</span>
|
<span>Manage access to your external accounts.</span>
|
||||||
</menu-entry>
|
</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>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('workspaceBackup')">
|
<menu-entry @click.native="setPanel('workspaceBackups')">
|
||||||
<icon-content-save slot="icon"></icon-content-save>
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
Workspace backup
|
Workspace backups
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="reset">
|
<menu-entry @click.native="reset">
|
||||||
<icon-logout slot="icon"></icon-logout>
|
<icon-logout slot="icon"></icon-logout>
|
||||||
<div>Reset application</div>
|
<div>Reset application</div>
|
||||||
<span>Sign out and clean all workspaces.</span>
|
<span>Sign out and clean all workspace data.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="about">
|
<menu-entry @click.native="about">
|
||||||
@ -161,6 +166,12 @@ export default {
|
|||||||
return Object.values(store.getters['data/tokensByType'])
|
return Object.values(store.getters['data/tokensByType'])
|
||||||
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
|
.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: {
|
methods: {
|
||||||
...mapActions('data', {
|
...mapActions('data', {
|
||||||
@ -186,35 +197,30 @@ export default {
|
|||||||
},
|
},
|
||||||
async settings() {
|
async settings() {
|
||||||
try {
|
try {
|
||||||
const settings = await store.dispatch('modal/open', 'settings');
|
await store.dispatch('modal/open', 'settings');
|
||||||
store.dispatch('data/setSettings', settings);
|
} catch (e) { /* Cancel */ }
|
||||||
} catch (e) {
|
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async templates() {
|
async templates() {
|
||||||
try {
|
try {
|
||||||
const { templates } = await store.dispatch('modal/open', 'templates');
|
await store.dispatch('modal/open', 'templates');
|
||||||
store.dispatch('data/setTemplatesById', templates);
|
} catch (e) { /* Cancel */ }
|
||||||
} catch (e) {
|
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async accounts() {
|
async accounts() {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('modal/open', 'accountManagement');
|
await store.dispatch('modal/open', 'accountManagement');
|
||||||
} catch (e) {
|
} catch (e) { /* Cancel */ }
|
||||||
// Cancel
|
},
|
||||||
}
|
async badges() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', 'badgeManagement');
|
||||||
|
} catch (e) { /* Cancel */ }
|
||||||
},
|
},
|
||||||
async reset() {
|
async reset() {
|
||||||
try {
|
try {
|
||||||
await store.dispatch('modal/open', 'reset');
|
await store.dispatch('modal/open', 'reset');
|
||||||
window.location.href = '#reset=true';
|
window.location.href = '#reset=true';
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
} catch (e) {
|
} catch (e) { /* Cancel */ }
|
||||||
// Cancel
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
about() {
|
about() {
|
||||||
store.dispatch('modal/open', 'about');
|
store.dispatch('modal/open', 'about');
|
||||||
|
@ -129,13 +129,13 @@ const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
|||||||
.filter(token => filter(token))
|
.filter(token => filter(token))
|
||||||
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
.sort((token1, token2) => token1.name.localeCompare(token2.name));
|
||||||
|
|
||||||
const publishModalOpener = type => async (token) => {
|
const publishModalOpener = (type, featureId) => async (token) => {
|
||||||
try {
|
try {
|
||||||
const publishLocation = await store.dispatch('modal/open', {
|
const publishLocation = await store.dispatch('modal/open', {
|
||||||
type,
|
type,
|
||||||
token,
|
token,
|
||||||
});
|
});
|
||||||
publishSvc.createPublishLocation(publishLocation);
|
publishSvc.createPublishLocation(publishLocation, featureId);
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -236,15 +236,15 @@ export default {
|
|||||||
await zendeskHelper.addAccount(subdomain, clientId);
|
await zendeskHelper.addAccount(subdomain, clientId);
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
publishBlogger: publishModalOpener('bloggerPublish'),
|
publishBlogger: publishModalOpener('bloggerPublish', 'publishToBlogger'),
|
||||||
publishBloggerPage: publishModalOpener('bloggerPagePublish'),
|
publishBloggerPage: publishModalOpener('bloggerPagePublish', 'publishToBloggerPage'),
|
||||||
publishDropbox: publishModalOpener('dropboxPublish'),
|
publishDropbox: publishModalOpener('dropboxPublish', 'publishToDropbox'),
|
||||||
publishGithub: publishModalOpener('githubPublish'),
|
publishGithub: publishModalOpener('githubPublish', 'publishToGithub'),
|
||||||
publishGist: publishModalOpener('gistPublish'),
|
publishGist: publishModalOpener('gistPublish', 'publishToGist'),
|
||||||
publishGitlab: publishModalOpener('gitlabPublish'),
|
publishGitlab: publishModalOpener('gitlabPublish', 'publishToGitlab'),
|
||||||
publishGoogleDrive: publishModalOpener('googleDrivePublish'),
|
publishGoogleDrive: publishModalOpener('googleDrivePublish', 'publishToGoogleDrive'),
|
||||||
publishWordpress: publishModalOpener('wordpressPublish'),
|
publishWordpress: publishModalOpener('wordpressPublish', 'publishToWordPress'),
|
||||||
publishZendesk: publishModalOpener('zendeskPublish'),
|
publishZendesk: publishModalOpener('zendeskPublish', 'publishToZendesk'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -108,6 +108,7 @@ import githubProvider from '../../services/providers/githubProvider';
|
|||||||
import gitlabProvider from '../../services/providers/gitlabProvider';
|
import gitlabProvider from '../../services/providers/gitlabProvider';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
const tokensToArray = (tokens, filter = () => true) => Object.values(tokens)
|
||||||
.filter(token => filter(token))
|
.filter(token => filter(token))
|
||||||
@ -162,7 +163,7 @@ export default {
|
|||||||
methods: {
|
methods: {
|
||||||
requestSync() {
|
requestSync() {
|
||||||
if (!this.isSyncRequested) {
|
if (!this.isSyncRequested) {
|
||||||
syncSvc.requestSync();
|
syncSvc.requestSync(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async manageSync() {
|
async manageSync() {
|
||||||
@ -194,28 +195,36 @@ export default {
|
|||||||
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
|
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async openGoogleDrive(token) {
|
|
||||||
const files = await googleHelper.openPicker(token, 'doc');
|
|
||||||
store.dispatch(
|
|
||||||
'queue/enqueue',
|
|
||||||
() => googleDriveProvider.openFiles(token, files),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async openDropbox(token) {
|
async openDropbox(token) {
|
||||||
const paths = await dropboxHelper.openChooser(token);
|
const paths = await dropboxHelper.openChooser(token);
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
'queue/enqueue',
|
'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) {
|
async saveGoogleDrive(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'googleDriveSave');
|
await openSyncModal(token, 'googleDriveSave');
|
||||||
} catch (e) { /* cancel */ }
|
badgeSvc.addBadge('saveOnGoogleDrive');
|
||||||
},
|
|
||||||
async saveDropbox(token) {
|
|
||||||
try {
|
|
||||||
await openSyncModal(token, 'dropboxSave');
|
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async openGithub(token) {
|
async openGithub(token) {
|
||||||
@ -226,18 +235,23 @@ export default {
|
|||||||
});
|
});
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
'queue/enqueue',
|
'queue/enqueue',
|
||||||
() => githubProvider.openFile(token, syncLocation),
|
async () => {
|
||||||
|
await githubProvider.openFile(token, syncLocation);
|
||||||
|
badgeSvc.addBadge('openFromGithub');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async saveGithub(token) {
|
async saveGithub(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'githubSave');
|
await openSyncModal(token, 'githubSave');
|
||||||
|
badgeSvc.addBadge('saveOnGithub');
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async saveGist(token) {
|
async saveGist(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'gistSync');
|
await openSyncModal(token, 'gistSync');
|
||||||
|
badgeSvc.addBadge('saveOnGist');
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async openGitlab(token) {
|
async openGitlab(token) {
|
||||||
@ -248,13 +262,17 @@ export default {
|
|||||||
});
|
});
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
'queue/enqueue',
|
'queue/enqueue',
|
||||||
() => gitlabProvider.openFile(token, syncLocation),
|
async () => {
|
||||||
|
await gitlabProvider.openFile(token, syncLocation);
|
||||||
|
badgeSvc.addBadge('openFromGitlab');
|
||||||
|
},
|
||||||
);
|
);
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async saveGitlab(token) {
|
async saveGitlab(token) {
|
||||||
try {
|
try {
|
||||||
await openSyncModal(token, 'gitlabSave');
|
await openSyncModal(token, 'gitlabSave');
|
||||||
|
badgeSvc.addBadge('saveOnGitlab');
|
||||||
} catch (e) { /* cancel */ }
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -2,8 +2,6 @@
|
|||||||
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
<modal-inner class="modal__inner-1--about-modal" aria-label="About">
|
||||||
<div class="modal__content">
|
<div class="modal__content">
|
||||||
<div class="logo-background"></div>
|
<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>
|
StackEdit on <a target="_blank" href="https://github.com/benweet/stackedit/">GitHub</a>
|
||||||
<br>
|
<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>
|
<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>
|
<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>
|
<br>
|
||||||
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
|
StackEdit on <a target="_blank" href="https://twitter.com/stackedit/">Twitter</a>
|
||||||
<div class="modal__info">
|
<hr>
|
||||||
For commercial support or custom development, please <a href="mailto:stackedit.project@gmail.com">send us an email</a>.
|
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||||
</div>
|
|
||||||
<h3>FAQ</h3>
|
<h3>FAQ</h3>
|
||||||
<div class="faq" v-html="faq"></div>
|
<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
|
Licensed under an
|
||||||
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a><br>
|
<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>
|
<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 gitlabHelper from '../../services/providers/helpers/gitlabHelper';
|
||||||
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
||||||
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -160,6 +161,7 @@ export default {
|
|||||||
await store.dispatch('data/patchTokensByType', {
|
await store.dispatch('data/patchTokensByType', {
|
||||||
[entry.providerId]: tokensBySub,
|
[entry.providerId]: tokensBySub,
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge('removeAccount');
|
||||||
},
|
},
|
||||||
async addBloggerAccount() {
|
async addBloggerAccount() {
|
||||||
try {
|
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 utils from '../../services/utils';
|
||||||
import presets from '../../data/presets';
|
import presets from '../../data/presets';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
const simpleProperties = {
|
const metadataProperties = {
|
||||||
title: '',
|
title: '',
|
||||||
author: '',
|
author: '',
|
||||||
tags: '',
|
tags: '',
|
||||||
@ -117,7 +118,7 @@ export default {
|
|||||||
yamlProperties: null,
|
yamlProperties: null,
|
||||||
preset: '',
|
preset: '',
|
||||||
error: null,
|
error: null,
|
||||||
...simpleProperties,
|
...metadataProperties,
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters('modal', [
|
...mapGetters('modal', [
|
||||||
@ -148,10 +149,10 @@ export default {
|
|||||||
const properties = this.properties || {};
|
const properties = this.properties || {};
|
||||||
const extensions = properties.extensions || {};
|
const extensions = properties.extensions || {};
|
||||||
this.preset = extensions.preset;
|
this.preset = extensions.preset;
|
||||||
if (this.presets.indexOf(this.preset) === -1) {
|
if (!this.presets.includes(this.preset)) {
|
||||||
this.preset = 'default';
|
this.preset = 'default';
|
||||||
}
|
}
|
||||||
Object.keys(simpleProperties).forEach((name) => {
|
Object.keys(metadataProperties).forEach((name) => {
|
||||||
this[name] = `${properties[name] || ''}`;
|
this[name] = `${properties[name] || ''}`;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -168,7 +169,7 @@ export default {
|
|||||||
hasChanged = true;
|
hasChanged = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Object.keys(simpleProperties).forEach((name) => {
|
Object.keys(metadataProperties).forEach((name) => {
|
||||||
if (this[name] !== properties[name]) {
|
if (this[name] !== properties[name]) {
|
||||||
if (this[name]) {
|
if (this[name]) {
|
||||||
properties[name] = this[name];
|
properties[name] = this[name];
|
||||||
@ -215,6 +216,17 @@ export default {
|
|||||||
if (this.error) {
|
if (this.error) {
|
||||||
this.setYamlTab();
|
this.setYamlTab();
|
||||||
} else {
|
} 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', {
|
store.commit('content/patchItem', {
|
||||||
id: this.contentId,
|
id: this.contentId,
|
||||||
properties: utils.sanitizeText(this.yamlProperties),
|
properties: utils.sanitizeText(this.yamlProperties),
|
||||||
|
@ -26,6 +26,7 @@ import { mapActions } from 'vuex';
|
|||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
import modalTemplate from './common/modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
data: () => ({
|
data: () => ({
|
||||||
@ -54,11 +55,12 @@ export default modalTemplate({
|
|||||||
...mapActions('notification', [
|
...mapActions('notification', [
|
||||||
'info',
|
'info',
|
||||||
]),
|
]),
|
||||||
resolve() {
|
async resolve() {
|
||||||
const { config } = this;
|
const { config } = this;
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
config.resolve();
|
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>
|
<script>
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import sponsorSvc from '../../services/sponsorSvc';
|
|
||||||
import networkSvc from '../../services/networkSvc';
|
import networkSvc from '../../services/networkSvc';
|
||||||
import editorSvc from '../../services/editorSvc';
|
import editorSvc from '../../services/editorSvc';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import modalTemplate from './common/modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
computedLocalSettings: {
|
computedLocalSettings: {
|
||||||
@ -45,20 +45,14 @@ export default modalTemplate({
|
|||||||
const currentContent = store.getters['content/current'];
|
const currentContent = store.getters['content/current'];
|
||||||
const { selectedFormat } = this;
|
const { selectedFormat } = this;
|
||||||
store.dispatch('queue/enqueue', async () => {
|
store.dispatch('queue/enqueue', async () => {
|
||||||
const [sponsorToken, token] = await Promise.all([
|
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
||||||
Promise.resolve().then(() => {
|
const sponsorToken = tokenToRefresh && await googleHelper.refreshToken(tokenToRefresh);
|
||||||
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
|
||||||
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
|
|
||||||
}),
|
|
||||||
sponsorSvc.getToken(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { body } = await networkSvc.request({
|
const { body } = await networkSvc.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'pandocExport',
|
url: 'pandocExport',
|
||||||
params: {
|
params: {
|
||||||
token,
|
|
||||||
idToken: sponsorToken && sponsorToken.idToken,
|
idToken: sponsorToken && sponsorToken.idToken,
|
||||||
format: selectedFormat,
|
format: selectedFormat,
|
||||||
options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
|
options: JSON.stringify(store.getters['data/computedSettings'].pandoc),
|
||||||
@ -69,6 +63,7 @@ export default modalTemplate({
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
|
FileSaver.saveAs(body, `${currentFile.name}.${selectedFormat}`);
|
||||||
|
badgeSvc.addBadge('exportPandoc');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 401) {
|
if (err.status === 401) {
|
||||||
store.dispatch('modal/open', 'sponsorOnly');
|
store.dispatch('modal/open', 'sponsorOnly');
|
||||||
|
@ -23,11 +23,11 @@
|
|||||||
<script>
|
<script>
|
||||||
import FileSaver from 'file-saver';
|
import FileSaver from 'file-saver';
|
||||||
import exportSvc from '../../services/exportSvc';
|
import exportSvc from '../../services/exportSvc';
|
||||||
import sponsorSvc from '../../services/sponsorSvc';
|
|
||||||
import networkSvc from '../../services/networkSvc';
|
import networkSvc from '../../services/networkSvc';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import modalTemplate from './common/modalTemplate';
|
import modalTemplate from './common/modalTemplate';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default modalTemplate({
|
export default modalTemplate({
|
||||||
computedLocalSettings: {
|
computedLocalSettings: {
|
||||||
@ -38,12 +38,11 @@ export default modalTemplate({
|
|||||||
this.config.resolve();
|
this.config.resolve();
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
store.dispatch('queue/enqueue', async () => {
|
store.dispatch('queue/enqueue', async () => {
|
||||||
const [sponsorToken, token, html] = await Promise.all([
|
const [sponsorToken, html] = await Promise.all([
|
||||||
Promise.resolve().then(() => {
|
Promise.resolve().then(() => {
|
||||||
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
const tokenToRefresh = store.getters['workspace/sponsorToken'];
|
||||||
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
|
return tokenToRefresh && googleHelper.refreshToken(tokenToRefresh);
|
||||||
}),
|
}),
|
||||||
sponsorSvc.getToken(),
|
|
||||||
exportSvc.applyTemplate(
|
exportSvc.applyTemplate(
|
||||||
currentFile.id,
|
currentFile.id,
|
||||||
this.allTemplatesById[this.selectedTemplate],
|
this.allTemplatesById[this.selectedTemplate],
|
||||||
@ -56,7 +55,6 @@ export default modalTemplate({
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'pdfExport',
|
url: 'pdfExport',
|
||||||
params: {
|
params: {
|
||||||
token,
|
|
||||||
idToken: sponsorToken && sponsorToken.idToken,
|
idToken: sponsorToken && sponsorToken.idToken,
|
||||||
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
|
options: JSON.stringify(store.getters['data/computedSettings'].wkhtmltopdf),
|
||||||
},
|
},
|
||||||
@ -65,6 +63,7 @@ export default modalTemplate({
|
|||||||
timeout: 60000,
|
timeout: 60000,
|
||||||
});
|
});
|
||||||
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
|
FileSaver.saveAs(body, `${currentFile.name}.pdf`);
|
||||||
|
badgeSvc.addBadge('exportPdf');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.status === 401) {
|
if (err.status === 401) {
|
||||||
store.dispatch('modal/open', 'sponsorOnly');
|
store.dispatch('modal/open', 'sponsorOnly');
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -72,6 +73,7 @@ export default {
|
|||||||
]),
|
]),
|
||||||
remove(location) {
|
remove(location) {
|
||||||
store.commit('publishLocation/deleteItem', location.id);
|
store.commit('publishLocation/deleteItem', location.id);
|
||||||
|
badgeSvc.addBadge('removePublishLocation');
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
<button class="button" @click="config.reject()">Cancel</button>
|
<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>
|
</div>
|
||||||
</modal-inner>
|
</modal-inner>
|
||||||
</template>
|
</template>
|
||||||
@ -38,6 +38,7 @@ import Tab from './common/Tab';
|
|||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import defaultSettings from '../../data/defaults/defaultSettings.yml';
|
import defaultSettings from '../../data/defaults/defaultSettings.yml';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
const emptySettings = `# Add your custom settings here to override the
|
const emptySettings = `# Add your custom settings here to override the
|
||||||
# default settings.
|
# default settings.
|
||||||
@ -77,6 +78,25 @@ export default {
|
|||||||
this.error = e.message;
|
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>
|
</script>
|
||||||
|
@ -50,6 +50,7 @@
|
|||||||
import { mapGetters, mapActions } from 'vuex';
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -75,6 +76,7 @@ export default {
|
|||||||
this.info('This location can not be removed.');
|
this.info('This location can not be removed.');
|
||||||
} else {
|
} else {
|
||||||
store.commit('syncLocation/deleteItem', location.id);
|
store.commit('syncLocation/deleteItem', location.id);
|
||||||
|
badgeSvc.addBadge('removeSyncLocation');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -53,6 +53,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
import utils from '../../services/utils';
|
import utils from '../../services/utils';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import CodeEditor from '../CodeEditor';
|
import CodeEditor from '../CodeEditor';
|
||||||
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
|
import emptyTemplateValue from '../../data/empties/emptyTemplateValue.html';
|
||||||
@ -153,7 +154,22 @@ export default {
|
|||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
}, 1);
|
}, 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({
|
this.config.resolve({
|
||||||
templates: this.templates,
|
templates: this.templates,
|
||||||
selectedId: this.selectedId,
|
selectedId: this.selectedId,
|
||||||
|
@ -65,6 +65,7 @@ import { mapGetters, mapActions } from 'vuex';
|
|||||||
import ModalInner from './common/ModalInner';
|
import ModalInner from './common/ModalInner';
|
||||||
import workspaceSvc from '../../services/workspaceSvc';
|
import workspaceSvc from '../../services/workspaceSvc';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
import badgeSvc from '../../services/badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
@ -95,13 +96,14 @@ export default {
|
|||||||
submitEdit(cancel) {
|
submitEdit(cancel) {
|
||||||
const workspace = this.workspacesById[this.editedId];
|
const workspace = this.workspacesById[this.editedId];
|
||||||
if (workspace) {
|
if (workspace) {
|
||||||
if (!cancel && this.editingName) {
|
if (!cancel && this.editingName && this.editingName !== workspace.name) {
|
||||||
store.dispatch('workspace/patchWorkspacesById', {
|
store.dispatch('workspace/patchWorkspacesById', {
|
||||||
[this.editedId]: {
|
[this.editedId]: {
|
||||||
...workspace,
|
...workspace,
|
||||||
name: this.editingName,
|
name: this.editingName,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
badgeSvc.addBadge('renameWorkspace');
|
||||||
} else {
|
} else {
|
||||||
this.editingName = workspace.name;
|
this.editingName = workspace.name;
|
||||||
}
|
}
|
||||||
@ -117,6 +119,7 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await store.dispatch('modal/open', 'removeWorkspace');
|
await store.dispatch('modal/open', 'removeWorkspace');
|
||||||
workspaceSvc.removeWorkspace(id);
|
workspaceSvc.removeWorkspace(id);
|
||||||
|
badgeSvc.addBadge('removeWorkspace');
|
||||||
} catch (e) { /* Cancel */ }
|
} catch (e) { /* Cancel */ }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -64,11 +64,10 @@ export default (desc) => {
|
|||||||
};
|
};
|
||||||
// Make use of `function` to have `this` bound to the component
|
// Make use of `function` to have `this` bound to the component
|
||||||
component.methods.configureTemplates = async function () { // eslint-disable-line func-names
|
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',
|
type: 'templates',
|
||||||
selectedId: this.selectedTemplate,
|
selectedId: this.selectedTemplate,
|
||||||
});
|
});
|
||||||
store.dispatch('data/setTemplatesById', templates);
|
|
||||||
store.dispatch('data/patchLocalSettings', {
|
store.dispatch('data/patchLocalSettings', {
|
||||||
[id]: selectedId,
|
[id]: selectedId,
|
||||||
});
|
});
|
||||||
|
@ -19,6 +19,7 @@ export default {
|
|||||||
'settings',
|
'settings',
|
||||||
'layoutSettings',
|
'layoutSettings',
|
||||||
'tokens',
|
'tokens',
|
||||||
|
'badges',
|
||||||
],
|
],
|
||||||
textMaxLength: 250000,
|
textMaxLength: 250000,
|
||||||
defaultName: 'Untitled',
|
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 ContentCopy from './ContentCopy';
|
||||||
import Key from './Key';
|
import Key from './Key';
|
||||||
import DotsHorizontal from './DotsHorizontal';
|
import DotsHorizontal from './DotsHorizontal';
|
||||||
|
import Seal from './Seal';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
@ -108,3 +109,4 @@ Vue.component('iconCheckCircle', CheckCircle);
|
|||||||
Vue.component('iconContentCopy', ContentCopy);
|
Vue.component('iconContentCopy', ContentCopy);
|
||||||
Vue.component('iconKey', Key);
|
Vue.component('iconKey', Key);
|
||||||
Vue.component('iconDotsHorizontal', DotsHorizontal);
|
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 store from '../store';
|
||||||
import workspaceSvc from './workspaceSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
import badgeSvc from './badgeSvc';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
newItem(isFolder = false) {
|
newItem(isFolder = false) {
|
||||||
@ -61,6 +62,7 @@ export default {
|
|||||||
} else {
|
} else {
|
||||||
workspaceSvc.deleteFile(id);
|
workspaceSvc.deleteFile(id);
|
||||||
}
|
}
|
||||||
|
badgeSvc.addBadge('removeFiles');
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedNode === store.getters['explorer/selectedNode']) {
|
if (selectedNode === store.getters['explorer/selectedNode']) {
|
||||||
|
@ -2,6 +2,7 @@ import store from '../../store';
|
|||||||
import couchdbHelper from './helpers/couchdbHelper';
|
import couchdbHelper from './helpers/couchdbHelper';
|
||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
|
import badgeSvc from '../badgeSvc';
|
||||||
|
|
||||||
let syncLastSeq;
|
let syncLastSeq;
|
||||||
|
|
||||||
@ -58,6 +59,7 @@ export default new Provider({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badgeSvc.addBadge('addCouchdbWorkspace');
|
||||||
return store.getters['workspace/workspacesById'][workspaceId];
|
return store.getters['workspace/workspacesById'][workspaceId];
|
||||||
},
|
},
|
||||||
async getChanges() {
|
async getChanges() {
|
||||||
|
@ -66,7 +66,7 @@ export default new Provider({
|
|||||||
|
|
||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const sub = `${githubHelper.subPrefix}:${entry.user.id}`;
|
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 {
|
return {
|
||||||
sub,
|
sub,
|
||||||
id: entry.version,
|
id: entry.version,
|
||||||
|
@ -135,7 +135,7 @@ export default new Provider({
|
|||||||
user = committer;
|
user = committer;
|
||||||
}
|
}
|
||||||
const sub = `${githubHelper.subPrefix}:${user.id}`;
|
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)
|
const date = (commit.author && commit.author.date)
|
||||||
|| (commit.committer && commit.committer.date);
|
|| (commit.committer && commit.committer.date);
|
||||||
return {
|
return {
|
||||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
|||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import userSvc from '../userSvc';
|
import userSvc from '../userSvc';
|
||||||
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
||||||
|
import badgeSvc from '../badgeSvc';
|
||||||
|
|
||||||
const getAbsolutePath = ({ id }) =>
|
const getAbsolutePath = ({ id }) =>
|
||||||
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||||
@ -86,6 +87,7 @@ export default new Provider({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badgeSvc.addBadge('addGithubWorkspace');
|
||||||
return store.getters['workspace/workspacesById'][workspaceId];
|
return store.getters['workspace/workspacesById'][workspaceId];
|
||||||
},
|
},
|
||||||
getChanges() {
|
getChanges() {
|
||||||
@ -247,7 +249,7 @@ export default new Provider({
|
|||||||
user = committer;
|
user = committer;
|
||||||
}
|
}
|
||||||
const sub = `${githubHelper.subPrefix}:${user.id}`;
|
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)
|
const date = (commit.author && commit.author.date)
|
||||||
|| (commit.committer && commit.committer.date)
|
|| (commit.committer && commit.committer.date)
|
||||||
|| 1;
|
|| 1;
|
||||||
|
@ -138,7 +138,7 @@ export default new Provider({
|
|||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const email = entry.author_email || entry.committer_email;
|
const email = entry.author_email || entry.committer_email;
|
||||||
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: sub,
|
id: sub,
|
||||||
name: entry.author_name || entry.committer_name,
|
name: entry.author_name || entry.committer_name,
|
||||||
imageUrl: '',
|
imageUrl: '',
|
||||||
|
@ -4,6 +4,7 @@ import Provider from './common/Provider';
|
|||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import userSvc from '../userSvc';
|
import userSvc from '../userSvc';
|
||||||
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
import gitWorkspaceSvc from '../gitWorkspaceSvc';
|
||||||
|
import badgeSvc from '../badgeSvc';
|
||||||
|
|
||||||
const getAbsolutePath = ({ id }) =>
|
const getAbsolutePath = ({ id }) =>
|
||||||
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
`${store.getters['workspace/currentWorkspace'].path || ''}${id}`;
|
||||||
@ -98,6 +99,7 @@ export default new Provider({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badgeSvc.addBadge('addGitlabWorkspace');
|
||||||
return store.getters['workspace/workspacesById'][workspaceId];
|
return store.getters['workspace/workspacesById'][workspaceId];
|
||||||
},
|
},
|
||||||
getChanges() {
|
getChanges() {
|
||||||
@ -252,7 +254,7 @@ export default new Provider({
|
|||||||
return entries.map((entry) => {
|
return entries.map((entry) => {
|
||||||
const email = entry.author_email || entry.committer_email;
|
const email = entry.author_email || entry.committer_email;
|
||||||
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
const sub = `${gitlabHelper.subPrefix}:${token.serverUrl}/${email}`;
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: sub,
|
id: sub,
|
||||||
name: entry.author_name || entry.committer_name,
|
name: entry.author_name || entry.committer_name,
|
||||||
imageUrl: '', // No way to get user's avatar url...
|
imageUrl: '', // No way to get user's avatar url...
|
||||||
|
@ -3,6 +3,7 @@ import googleHelper from './helpers/googleHelper';
|
|||||||
import Provider from './common/Provider';
|
import Provider from './common/Provider';
|
||||||
import utils from '../utils';
|
import utils from '../utils';
|
||||||
import workspaceSvc from '../workspaceSvc';
|
import workspaceSvc from '../workspaceSvc';
|
||||||
|
import badgeSvc from '../badgeSvc';
|
||||||
|
|
||||||
let fileIdToOpen;
|
let fileIdToOpen;
|
||||||
let syncStartPageToken;
|
let syncStartPageToken;
|
||||||
@ -137,6 +138,7 @@ export default new Provider({
|
|||||||
await initFolder(token, folder);
|
await initFolder(token, folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
badgeSvc.addBadge('addGoogleDriveWorkspace');
|
||||||
return getWorkspace(folderId);
|
return getWorkspace(folderId);
|
||||||
},
|
},
|
||||||
async performAction() {
|
async performAction() {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import networkSvc from '../../networkSvc';
|
import networkSvc from '../../networkSvc';
|
||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const getAppKey = (fullAccess) => {
|
const getAppKey = (fullAccess) => {
|
||||||
if (fullAccess) {
|
if (fullAccess) {
|
||||||
@ -73,7 +74,7 @@ export default {
|
|||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: 'https://api.dropboxapi.com/2/users/get_current_account',
|
url: 'https://api.dropboxapi.com/2/users/get_current_account',
|
||||||
});
|
});
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: `${subPrefix}:${body.account_id}`,
|
id: `${subPrefix}:${body.account_id}`,
|
||||||
name: body.name.display_name,
|
name: body.name.display_name,
|
||||||
imageUrl: body.profile_photo_url || '',
|
imageUrl: body.profile_photo_url || '',
|
||||||
@ -96,8 +97,10 @@ export default {
|
|||||||
store.dispatch('data/addDropboxToken', token);
|
store.dispatch('data/addDropboxToken', token);
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
addAccount(fullAccess = false) {
|
async addAccount(fullAccess = false) {
|
||||||
return this.startOauth2(fullAccess);
|
const token = await this.startOauth2(fullAccess);
|
||||||
|
badgeSvc.addBadge('addDropboxAccount');
|
||||||
|
return token;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -2,6 +2,7 @@ import utils from '../../utils';
|
|||||||
import networkSvc from '../../networkSvc';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = GITHUB_CLIENT_ID;
|
const clientId = GITHUB_CLIENT_ID;
|
||||||
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
const getScopes = token => [token.repoFullAccess ? 'repo' : 'public_repo', 'gist'];
|
||||||
@ -90,7 +91,7 @@ export default {
|
|||||||
access_token: accessToken,
|
access_token: accessToken,
|
||||||
},
|
},
|
||||||
})).body;
|
})).body;
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: `${subPrefix}:${user.id}`,
|
id: `${subPrefix}:${user.id}`,
|
||||||
name: user.login,
|
name: user.login,
|
||||||
imageUrl: user.avatar_url || '',
|
imageUrl: user.avatar_url || '',
|
||||||
@ -107,7 +108,7 @@ export default {
|
|||||||
accessToken,
|
accessToken,
|
||||||
name: user.login,
|
name: user.login,
|
||||||
sub: `${user.id}`,
|
sub: `${user.id}`,
|
||||||
repoFullAccess: scopes.indexOf('repo') !== -1,
|
repoFullAccess: scopes.includes('repo'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add token to github tokens
|
// Add token to github tokens
|
||||||
@ -115,7 +116,9 @@ export default {
|
|||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async addAccount(repoFullAccess = false) {
|
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 networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
|
const request = ({ accessToken, serverUrl }, options) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
@ -65,7 +66,7 @@ export default {
|
|||||||
url: 'user',
|
url: 'user',
|
||||||
});
|
});
|
||||||
const uniqueSub = `${serverUrl}/${user.id}`;
|
const uniqueSub = `${serverUrl}/${user.id}`;
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: `${subPrefix}:${uniqueSub}`,
|
id: `${subPrefix}:${uniqueSub}`,
|
||||||
name: user.username,
|
name: user.username,
|
||||||
imageUrl: user.avatar_url || '',
|
imageUrl: user.avatar_url || '',
|
||||||
@ -88,8 +89,10 @@ export default {
|
|||||||
store.dispatch('data/addGitlabToken', token);
|
store.dispatch('data/addGitlabToken', token);
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
addAccount(serverUrl, applicationId, sub = null) {
|
async addAccount(serverUrl, applicationId, sub = null) {
|
||||||
return this.startOauth2(serverUrl, applicationId, sub);
|
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 networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
import userSvc from '../../userSvc';
|
import userSvc from '../../userSvc';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = GOOGLE_CLIENT_ID;
|
const clientId = GOOGLE_CLIENT_ID;
|
||||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||||
@ -39,8 +40,8 @@ if (utils.queryParams.providerId === 'googleDrive') {
|
|||||||
* https://developers.google.com/people/api/rest/v1/people/get
|
* https://developers.google.com/people/api/rest/v1/people/get
|
||||||
*/
|
*/
|
||||||
const getUser = async (sub, token) => {
|
const getUser = async (sub, token) => {
|
||||||
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos`;
|
const url = `https://people.googleapis.com/v1/people/${sub}?personFields=names,photos&key=${apiKey}`;
|
||||||
const { body } = await networkSvc.request(token
|
const { body } = await networkSvc.request(sub === 'me' && token
|
||||||
? {
|
? {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url,
|
url,
|
||||||
@ -50,7 +51,7 @@ const getUser = async (sub, token) => {
|
|||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `${url}&key=${apiKey}`,
|
url,
|
||||||
}, true);
|
}, true);
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
@ -60,8 +61,8 @@ userSvc.setInfoResolver('google', subPrefix, async (sub) => {
|
|||||||
try {
|
try {
|
||||||
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
|
const googleToken = Object.values(store.getters['data/googleTokensBySub'])[0];
|
||||||
const body = await getUser(sub, googleToken);
|
const body = await getUser(sub, googleToken);
|
||||||
const name = body.names[0] || {};
|
const name = (body.names && body.names[0]) || {};
|
||||||
const photo = body.photos[0] || {};
|
const photo = (body.photos && body.photos[0]) || {};
|
||||||
return {
|
return {
|
||||||
id: `${subPrefix}:${sub}`,
|
id: `${subPrefix}:${sub}`,
|
||||||
name: name.displayName,
|
name: name.displayName,
|
||||||
@ -150,15 +151,15 @@ export default {
|
|||||||
expiresOn: Date.now() + (expiresIn * 1000),
|
expiresOn: Date.now() + (expiresIn * 1000),
|
||||||
idToken,
|
idToken,
|
||||||
sub: body.sub,
|
sub: body.sub,
|
||||||
name: (existingToken || {}).name || 'Unknown',
|
name: (existingToken || {}).name || 'Someone',
|
||||||
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
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,
|
isSponsor: false,
|
||||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: scopes.includes('https://www.googleapis.com/auth/drive') ||
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
scopes.includes('https://www.googleapis.com/auth/drive.file'),
|
||||||
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
isBlogger: scopes.includes('https://www.googleapis.com/auth/blogger'),
|
||||||
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
isPhotos: scopes.includes('https://www.googleapis.com/auth/photos'),
|
||||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
driveFullAccess: scopes.includes('https://www.googleapis.com/auth/drive'),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call the user info endpoint
|
// Call the user info endpoint
|
||||||
@ -169,7 +170,7 @@ export default {
|
|||||||
if (name.displayName) {
|
if (name.displayName) {
|
||||||
token.name = name.displayName;
|
token.name = name.displayName;
|
||||||
}
|
}
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: `${subPrefix}:${userId}`,
|
id: `${subPrefix}:${userId}`,
|
||||||
name: name.displayName,
|
name: name.displayName,
|
||||||
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
imageUrl: (photo.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
||||||
@ -191,13 +192,17 @@ export default {
|
|||||||
|
|
||||||
if (token.isLogin) {
|
if (token.isLogin) {
|
||||||
try {
|
try {
|
||||||
token.isSponsor = (await networkSvc.request({
|
const res = await networkSvc.request({
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: 'userInfo',
|
url: 'userInfo',
|
||||||
params: {
|
params: {
|
||||||
idToken: token.idToken,
|
idToken: token.idToken,
|
||||||
},
|
},
|
||||||
})).body.sponsorUntil > Date.now();
|
});
|
||||||
|
token.isSponsor = res.body.sponsorUntil > Date.now();
|
||||||
|
if (token.isSponsor) {
|
||||||
|
badgeSvc.addBadge('sponsor');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore
|
// Ignore
|
||||||
}
|
}
|
||||||
@ -245,14 +250,20 @@ export default {
|
|||||||
signin() {
|
signin() {
|
||||||
return this.startOauth2(driveAppDataScopes);
|
return this.startOauth2(driveAppDataScopes);
|
||||||
},
|
},
|
||||||
addDriveAccount(fullAccess = false, sub = null) {
|
async addDriveAccount(fullAccess = false, sub = null) {
|
||||||
return this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
|
const token = await this.startOauth2(getDriveScopes({ driveFullAccess: fullAccess }), sub);
|
||||||
|
badgeSvc.addBadge('addGoogleDriveAccount');
|
||||||
|
return token;
|
||||||
},
|
},
|
||||||
addBloggerAccount() {
|
async addBloggerAccount() {
|
||||||
return this.startOauth2(bloggerScopes);
|
const token = await this.startOauth2(bloggerScopes);
|
||||||
|
badgeSvc.addBadge('addBloggerAccount');
|
||||||
|
return token;
|
||||||
},
|
},
|
||||||
addPhotosAccount() {
|
async addPhotosAccount() {
|
||||||
return this.startOauth2(photosScopes);
|
const token = await this.startOauth2(photosScopes);
|
||||||
|
badgeSvc.addBadge('addGooglePhotosAccount');
|
||||||
|
return token;
|
||||||
},
|
},
|
||||||
async getSponsorship(token) {
|
async getSponsorship(token) {
|
||||||
const refreshedToken = await this.refreshToken(token);
|
const refreshedToken = await this.refreshToken(token);
|
||||||
@ -296,10 +307,10 @@ export default {
|
|||||||
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
options.url = `https://www.googleapis.com/drive/v3/files/${fileId}`;
|
||||||
if (parents && oldParents) {
|
if (parents && oldParents) {
|
||||||
params.addParents = parents
|
params.addParents = parents
|
||||||
.filter(parent => oldParents.indexOf(parent) === -1)
|
.filter(parent => !oldParents.includes(parent))
|
||||||
.join(',');
|
.join(',');
|
||||||
params.removeParents = oldParents
|
params.removeParents = oldParents
|
||||||
.filter(parent => parents.indexOf(parent) === -1)
|
.filter(parent => !parents.includes(parent))
|
||||||
.join(',');
|
.join(',');
|
||||||
}
|
}
|
||||||
} else if (parents) {
|
} else if (parents) {
|
||||||
@ -457,7 +468,7 @@ export default {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
revisions.forEach((revision) => {
|
revisions.forEach((revision) => {
|
||||||
userSvc.addInfo({
|
userSvc.addUserInfo({
|
||||||
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
|
id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`,
|
||||||
name: revision.lastModifyingUser.displayName,
|
name: revision.lastModifyingUser.displayName,
|
||||||
imageUrl: revision.lastModifyingUser.photoLink || '',
|
imageUrl: revision.lastModifyingUser.photoLink || '',
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import networkSvc from '../../networkSvc';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const clientId = '23361';
|
const clientId = '23361';
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (WordPress tokens expire after 2 weeks)
|
||||||
@ -63,8 +64,10 @@ export default {
|
|||||||
});
|
});
|
||||||
return this.startOauth2(sub);
|
return this.startOauth2(sub);
|
||||||
},
|
},
|
||||||
addAccount(fullAccess = false) {
|
async addAccount(fullAccess = false) {
|
||||||
return this.startOauth2(fullAccess);
|
const token = await this.startOauth2(fullAccess);
|
||||||
|
badgeSvc.addBadge('addWordpressAccount');
|
||||||
|
return token;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import networkSvc from '../../networkSvc';
|
import networkSvc from '../../networkSvc';
|
||||||
import store from '../../../store';
|
import store from '../../../store';
|
||||||
|
import badgeSvc from '../../badgeSvc';
|
||||||
|
|
||||||
const request = (token, options) => networkSvc.request({
|
const request = (token, options) => networkSvc.request({
|
||||||
...options,
|
...options,
|
||||||
@ -49,8 +50,10 @@ export default {
|
|||||||
store.dispatch('data/addZendeskToken', token);
|
store.dispatch('data/addZendeskToken', token);
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
addAccount(subdomain, clientId) {
|
async addAccount(subdomain, clientId) {
|
||||||
return this.startOauth2(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 exportSvc from './exportSvc';
|
||||||
import providerRegistry from './providers/common/providerRegistry';
|
import providerRegistry from './providers/common/providerRegistry';
|
||||||
import workspaceSvc from './workspaceSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
|
import badgeSvc from './badgeSvc';
|
||||||
|
|
||||||
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
const hasCurrentFilePublishLocations = () => !!store.getters['publishLocation/current'].length;
|
||||||
|
|
||||||
@ -112,10 +113,11 @@ const requestPublish = () => {
|
|||||||
if (networkSvc.isUserActive()) {
|
if (networkSvc.isUserActive()) {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
if (!hasCurrentFilePublishLocations()) {
|
if (!hasCurrentFilePublishLocations()) {
|
||||||
// Cancel sync
|
// Cancel publish
|
||||||
throw new Error('Publish not possible.');
|
throw new Error('Publish not possible.');
|
||||||
}
|
}
|
||||||
await publishFile(store.getters['file/current'].id);
|
await publishFile(store.getters['file/current'].id);
|
||||||
|
badgeSvc.addBadge('triggerPublish');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
intervalId = utils.setInterval(() => attempt(), 1000);
|
intervalId = utils.setInterval(() => attempt(), 1000);
|
||||||
@ -123,7 +125,7 @@ const requestPublish = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const createPublishLocation = (publishLocation) => {
|
const createPublishLocation = (publishLocation, featureId) => {
|
||||||
const currentFile = store.getters['file/current'];
|
const currentFile = store.getters['file/current'];
|
||||||
publishLocation.fileId = currentFile.id;
|
publishLocation.fileId = currentFile.id;
|
||||||
store.dispatch(
|
store.dispatch(
|
||||||
@ -132,6 +134,9 @@ const createPublishLocation = (publishLocation) => {
|
|||||||
const publishLocationToStore = await publish(publishLocation);
|
const publishLocationToStore = await publish(publishLocation);
|
||||||
workspaceSvc.addPublishLocation(publishLocationToStore);
|
workspaceSvc.addPublishLocation(publishLocationToStore);
|
||||||
store.dispatch('notification/info', `A new publication location was added to "${currentFile.name}".`);
|
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 tempFileSvc from './tempFileSvc';
|
||||||
import workspaceSvc from './workspaceSvc';
|
import workspaceSvc from './workspaceSvc';
|
||||||
import constants from '../data/constants';
|
import constants from '../data/constants';
|
||||||
|
import badgeSvc from './badgeSvc';
|
||||||
|
|
||||||
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
const minAutoSyncEvery = 60 * 1000; // 60 sec
|
||||||
const inactivityThreshold = 3 * 1000; // 3 sec
|
const inactivityThreshold = 3 * 1000; // 3 sec
|
||||||
@ -455,7 +456,7 @@ const syncFile = async (fileId, syncContext = new SyncContext()) => {
|
|||||||
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
newSyncedContent.syncHistory[syncLocation.id] = newSyncHistoryItem;
|
||||||
if (serverContent &&
|
if (serverContent &&
|
||||||
(serverContent.hash === newSyncHistoryItem[LAST_SEEN] ||
|
(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
|
// That's the 2nd time we've seen this content, trust it for future merges
|
||||||
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN];
|
newSyncHistoryItem[LAST_MERGED] = newSyncHistoryItem[LAST_SEEN];
|
||||||
@ -723,10 +724,11 @@ const syncWorkspace = async (skipContents = false) => {
|
|||||||
return true;
|
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') {
|
if (workspace.id === 'main') {
|
||||||
await syncDataItem('settings');
|
await syncDataItem('settings');
|
||||||
await syncDataItem('workspaces');
|
await syncDataItem('workspaces');
|
||||||
|
await syncDataItem('badges');
|
||||||
}
|
}
|
||||||
await syncDataItem('templates');
|
await syncDataItem('templates');
|
||||||
|
|
||||||
@ -786,6 +788,10 @@ const syncWorkspace = async (skipContents = false) => {
|
|||||||
if (syncContext.restartSkipContents) {
|
if (syncContext.restartSkipContents) {
|
||||||
await syncWorkspace(true);
|
await syncWorkspace(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (workspace.id === 'main') {
|
||||||
|
badgeSvc.addBadge('syncMainWorkspace');
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err && err.message === 'TOO_LATE') {
|
if (err && err.message === 'TOO_LATE') {
|
||||||
// Restart sync
|
// Restart sync
|
||||||
@ -799,7 +805,7 @@ const syncWorkspace = async (skipContents = false) => {
|
|||||||
/**
|
/**
|
||||||
* Enqueue a sync task, if possible.
|
* Enqueue a sync task, if possible.
|
||||||
*/
|
*/
|
||||||
const requestSync = () => {
|
const requestSync = (addTriggerSyncBadge = false) => {
|
||||||
// No sync in light mode
|
// No sync in light mode
|
||||||
if (store.state.light) {
|
if (store.state.light) {
|
||||||
return;
|
return;
|
||||||
@ -851,6 +857,10 @@ const requestSync = () => {
|
|||||||
workspaceSvc.deleteFile(fileId);
|
workspaceSvc.deleteFile(fileId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (addTriggerSyncBadge) {
|
||||||
|
badgeSvc.addBadge('triggerSync');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
clearInterval(intervalId);
|
clearInterval(intervalId);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,47 @@
|
|||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
import utils from './utils';
|
||||||
|
|
||||||
|
const refreshUserInfoAfter = 60 * 60 * 1000; // 60 minutes
|
||||||
|
|
||||||
const infoPromisesByUserId = {};
|
|
||||||
const infoResolversByType = {};
|
const infoResolversByType = {};
|
||||||
const subPrefixesByType = {};
|
const subPrefixesByType = {};
|
||||||
const typesBySubPrefix = {};
|
const typesBySubPrefix = {};
|
||||||
|
|
||||||
const parseUserId = (userId) => {
|
const lastInfosByUserId = {};
|
||||||
|
const infoPromisedByUserId = {};
|
||||||
|
|
||||||
|
const sanitizeUserId = (userId) => {
|
||||||
const prefix = userId[2] === ':' && userId.slice(0, 2);
|
const prefix = userId[2] === ':' && userId.slice(0, 2);
|
||||||
const type = typesBySubPrefix[prefix];
|
if (typesBySubPrefix[prefix]) {
|
||||||
return type ? [type, userId.slice(3)] : ['google', userId];
|
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 {
|
export default {
|
||||||
setInfoResolver(type, subPrefix, resolver) {
|
setInfoResolver(type, subPrefix, resolver) {
|
||||||
@ -27,53 +58,34 @@ export default {
|
|||||||
const prefix = subPrefixesByType[loginType];
|
const prefix = subPrefixesByType[loginType];
|
||||||
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
|
return prefix ? `${prefix}:${loginToken.sub}` : loginToken.sub;
|
||||||
},
|
},
|
||||||
addInfo(info) {
|
sanitizeUserId,
|
||||||
infoPromisesByUserId[info.id] = Promise.resolve(info);
|
addUserInfo(userInfo) {
|
||||||
store.commit('userInfo/addItem', info);
|
store.commit('userInfo/setItem', userInfo);
|
||||||
|
lastInfosByUserId[userInfo.id] = Date.now();
|
||||||
},
|
},
|
||||||
async getInfo(userId) {
|
addUserId(userId) {
|
||||||
if (!userId) {
|
if (userId) {
|
||||||
return {};
|
const sanitizedUserId = sanitizeUserId(userId);
|
||||||
}
|
const lastInfo = lastInfosByUserId[sanitizedUserId];
|
||||||
|
if (lastInfo === undefined) {
|
||||||
let infoPromise = infoPromisesByUserId[userId];
|
// Try to find a token with this sub to resolve name as soon as possible
|
||||||
if (infoPromise) {
|
const [type, sub] = parseUserId(sanitizedUserId);
|
||||||
return infoPromise;
|
const token = store.getters['data/tokensByType'][type][sub];
|
||||||
}
|
if (token) {
|
||||||
|
store.commit('userInfo/setItem', {
|
||||||
const [type, sub] = parseUserId(userId);
|
id: sanitizedUserId,
|
||||||
|
name: token.name,
|
||||||
// Try to find a token with this sub to resolve name as soon as possible
|
});
|
||||||
const token = store.getters['data/tokensByType'][type][sub];
|
|
||||||
if (token) {
|
|
||||||
store.commit('userInfo/addItem', {
|
|
||||||
id: userId,
|
|
||||||
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({});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
infoPromisesByUserId[userId] = infoPromise;
|
if (lastInfo === undefined || lastInfo + refreshUserInfoAfter < Date.now()) {
|
||||||
return infoPromise;
|
lastInfosByUserId[sanitizedUserId] = 0;
|
||||||
|
refreshUserInfos();
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get user info periodically
|
||||||
|
utils.setInterval(() => refreshUserInfos(), 60 * 1000);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import store from '../store';
|
import store from '../store';
|
||||||
import utils from './utils';
|
import utils from './utils';
|
||||||
import constants from '../data/constants';
|
import constants from '../data/constants';
|
||||||
|
import badgeSvc from './badgeSvc';
|
||||||
|
|
||||||
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
const forbiddenFolderNameMatcher = /^\.stackedit-data$|^\.stackedit-trash$|\.md$|\.sync$|\.publish$/;
|
||||||
|
|
||||||
@ -249,8 +250,13 @@ export default {
|
|||||||
...location,
|
...location,
|
||||||
id: utils.uid(),
|
id: utils.uid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sanitize the workspace
|
// Sanitize the workspace
|
||||||
this.ensureUniqueLocations();
|
this.ensureUniqueLocations();
|
||||||
|
|
||||||
|
if (Object.keys(store.getters['syncLocation/currentWithWorkspaceSyncLocation']).length > 1) {
|
||||||
|
badgeSvc.addBadge('syncMultipleLocations');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addPublishLocation(location) {
|
addPublishLocation(location) {
|
||||||
@ -258,8 +264,13 @@ export default {
|
|||||||
...location,
|
...location,
|
||||||
id: utils.uid(),
|
id: utils.uid(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sanitize the workspace
|
// Sanitize the workspace
|
||||||
this.ensureUniqueLocations();
|
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 empty from '../data/empties/emptyContent';
|
||||||
import utils from '../services/utils';
|
import utils from '../services/utils';
|
||||||
import cledit from '../services/editor/cledit';
|
import cledit from '../services/editor/cledit';
|
||||||
|
import badgeSvc from '../services/badgeSvc';
|
||||||
|
|
||||||
const diffMatchPatch = new DiffMatchPatch();
|
const diffMatchPatch = new DiffMatchPatch();
|
||||||
|
|
||||||
@ -104,6 +105,7 @@ module.actions = {
|
|||||||
...currentContent,
|
...currentContent,
|
||||||
text: revisionContent.originalText,
|
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 styledHtmlWithTocTemplate from '../data/templates/styledHtmlWithTocTemplate.html';
|
||||||
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
import jekyllSiteTemplate from '../data/templates/jekyllSiteTemplate.html';
|
||||||
import constants from '../data/constants';
|
import constants from '../data/constants';
|
||||||
|
import features from '../data/features';
|
||||||
|
|
||||||
const itemTemplate = (id, data = {}) => ({
|
const itemTemplate = (id, data = {}) => ({
|
||||||
id,
|
id,
|
||||||
@ -203,7 +204,19 @@ export default {
|
|||||||
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
gitlabTokensBySub: (state, { tokensByType }) => tokensByType.gitlab || {},
|
||||||
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
wordpressTokensBySub: (state, { tokensByType }) => tokensByType.wordpress || {},
|
||||||
zendeskTokensBySub: (state, { tokensByType }) => tokensByType.zendesk || {},
|
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: {
|
actions: {
|
||||||
setSettings: setter('settings'),
|
setSettings: setter('settings'),
|
||||||
@ -275,5 +288,6 @@ export default {
|
|||||||
addGitlabToken: tokenAdder('gitlab'),
|
addGitlabToken: tokenAdder('gitlab'),
|
||||||
addWordpressToken: tokenAdder('wordpress'),
|
addWordpressToken: tokenAdder('wordpress'),
|
||||||
addZendeskToken: tokenAdder('zendesk'),
|
addZendeskToken: tokenAdder('zendesk'),
|
||||||
|
patchBadges: patcher('badges'),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -3,7 +3,8 @@ import googleHelper from '../services/providers/helpers/googleHelper';
|
|||||||
import syncSvc from '../services/syncSvc';
|
import syncSvc from '../services/syncSvc';
|
||||||
|
|
||||||
const idShifter = offset => (state, getters) => {
|
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;
|
const idx = ids.indexOf(state.currentDiscussionId) + offset + ids.length;
|
||||||
return ids[idx % ids.length];
|
return ids[idx % ids.length];
|
||||||
};
|
};
|
||||||
|
@ -52,8 +52,7 @@ const store = new Vuex.Store({
|
|||||||
light: false,
|
light: false,
|
||||||
offline: false,
|
offline: false,
|
||||||
lastOfflineCheck: 0,
|
lastOfflineCheck: 0,
|
||||||
minuteCounter: 0,
|
timeCounter: 0,
|
||||||
monetizeSponsor: false,
|
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setLight: (state, value) => {
|
setLight: (state, value) => {
|
||||||
@ -65,14 +64,8 @@ const store = new Vuex.Store({
|
|||||||
updateLastOfflineCheck: (state) => {
|
updateLastOfflineCheck: (state) => {
|
||||||
state.lastOfflineCheck = Date.now();
|
state.lastOfflineCheck = Date.now();
|
||||||
},
|
},
|
||||||
updateMinuteCounter: (state) => {
|
updateTimeCounter: (state) => {
|
||||||
state.minuteCounter += 1;
|
state.timeCounter += 1;
|
||||||
},
|
|
||||||
setMonetizeSponsor: (state, value) => {
|
|
||||||
state.monetizeSponsor = value;
|
|
||||||
},
|
|
||||||
setGoogleSponsor: (state, value) => {
|
|
||||||
state.googleSponsor = value;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -161,9 +154,9 @@ const store = new Vuex.Store({
|
|||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
isSponsor: ({ light, monetizeSponsor }, getters) => {
|
isSponsor: ({ light }, getters) => {
|
||||||
const sponsorToken = getters['workspace/sponsorToken'];
|
const sponsorToken = getters['workspace/sponsorToken'];
|
||||||
return light || monetizeSponsor || (sponsorToken && sponsorToken.isSponsor);
|
return light || (sponsorToken && sponsorToken.isSponsor);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
@ -183,7 +176,7 @@ const store = new Vuex.Store({
|
|||||||
});
|
});
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
store.commit('updateMinuteCounter');
|
store.commit('updateTimeCounter');
|
||||||
}, 60 * 1000);
|
}, 30 * 1000);
|
||||||
|
|
||||||
export default store;
|
export default store;
|
||||||
|
@ -10,7 +10,7 @@ pagedownButtons.forEach((button) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const minPadding = 20;
|
const minPadding = 25;
|
||||||
const editorTopPadding = 10;
|
const editorTopPadding = 10;
|
||||||
const navigationBarEditButtonsWidth = (34 * buttonCount) + (8 * spacerCount); // buttons + spacers
|
const navigationBarEditButtonsWidth = (34 * buttonCount) + (8 * spacerCount); // buttons + spacers
|
||||||
const navigationBarLeftButtonWidth = 38 + 4 + 12;
|
const navigationBarLeftButtonWidth = 38 + 4 + 12;
|
||||||
|
@ -45,16 +45,22 @@ export default {
|
|||||||
|
|
||||||
return item.promise;
|
return item.promise;
|
||||||
},
|
},
|
||||||
info({ dispatch }, info) {
|
info({ dispatch }, content) {
|
||||||
return dispatch('showItem', {
|
return dispatch('showItem', {
|
||||||
type: 'info',
|
type: 'info',
|
||||||
content: info,
|
content,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
confirm({ dispatch }, question) {
|
badge({ dispatch }, content) {
|
||||||
|
return dispatch('showItem', {
|
||||||
|
type: 'badge',
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
confirm({ dispatch }, content) {
|
||||||
return dispatch('showItem', {
|
return dispatch('showItem', {
|
||||||
type: 'confirm',
|
type: 'confirm',
|
||||||
content: question,
|
content,
|
||||||
timeout: 10000, // 10 sec
|
timeout: 10000, // 10 sec
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -6,8 +6,20 @@ export default {
|
|||||||
itemsById: {},
|
itemsById: {},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
addItem: ({ itemsById }, item) => {
|
setItem: ({ itemsById }, item) => {
|
||||||
Vue.set(itemsById, item.id, 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;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
& > * {
|
|
||||||
border-left: 2px solid transparent;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.gutter__background {
|
.gutter__background {
|
||||||
@ -289,8 +285,8 @@ textarea {
|
|||||||
color: rgba(0, 0, 0, 0.33);
|
color: rgba(0, 0, 0, 0.33);
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
padding: 2px 3px 2px 0;
|
padding: 3px 3px 3px 0;
|
||||||
width: 20px;
|
width: 22px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user