mirror of
https://gitee.com/mafgwo/stackedit
synced 2024-12-24 11:25:40 +08:00
Added account management modal
This commit is contained in:
parent
91f8cf3c10
commit
8cf0b87f5f
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,6 +2,7 @@
|
||||||
node_modules/
|
node_modules/
|
||||||
dist/
|
dist/
|
||||||
.history
|
.history
|
||||||
|
.idea
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
.vscode
|
.vscode
|
||||||
stackedit_v4
|
stackedit_v4
|
||||||
|
|
13
src/assets/iconGoogle.svg
Normal file
13
src/assets/iconGoogle.svg
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 48 48">
|
||||||
|
<defs>
|
||||||
|
<path id="a" d="M44.5 20H24v8.5h11.8C34.7 33.9 30.1 37 24 37c-7.2 0-13-5.8-13-13s5.8-13 13-13c3.1 0 5.9 1.1 8.1 2.9l6.4-6.4C34.6 4.1 29.6 2 24 2 11.8 2 2 11.8 2 24s9.8 22 22 22c11 0 21-8 21-22 0-1.3-.2-2.7-.5-4z"/>
|
||||||
|
</defs>
|
||||||
|
<clipPath id="b">
|
||||||
|
<use xlink:href="#a" overflow="visible"/>
|
||||||
|
</clipPath>
|
||||||
|
<path clip-path="url(#b)" fill="#FBBC05" d="M0 37V11l17 13z"/>
|
||||||
|
<path clip-path="url(#b)" fill="#EA4335" d="M0 11l17 13 7-6.1L48 14V0H0z"/>
|
||||||
|
<path clip-path="url(#b)" fill="#34A853" d="M0 37l30-23 7.9 1L48 0v48H0z"/>
|
||||||
|
<path clip-path="url(#b)" fill="#4285F4" d="M48 48L17 24l-4-3 35-10z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 729 B |
|
@ -36,6 +36,7 @@ import ImageModal from './modals/ImageModal';
|
||||||
import SyncManagementModal from './modals/SyncManagementModal';
|
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 SponsorModal from './modals/SponsorModal';
|
import SponsorModal from './modals/SponsorModal';
|
||||||
|
|
||||||
// Providers
|
// Providers
|
||||||
|
@ -86,6 +87,7 @@ export default {
|
||||||
SyncManagementModal,
|
SyncManagementModal,
|
||||||
PublishManagementModal,
|
PublishManagementModal,
|
||||||
WorkspaceManagementModal,
|
WorkspaceManagementModal,
|
||||||
|
AccountManagementModal,
|
||||||
SponsorModal,
|
SponsorModal,
|
||||||
// Providers
|
// Providers
|
||||||
GooglePhotoModal,
|
GooglePhotoModal,
|
||||||
|
|
|
@ -56,7 +56,6 @@ const panelNames = {
|
||||||
history: 'File history',
|
history: 'File history',
|
||||||
export: 'Export to disk',
|
export: 'Export to disk',
|
||||||
import: 'Import from disk',
|
import: 'Import from disk',
|
||||||
more: 'More',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -88,8 +88,44 @@
|
||||||
Print
|
Print
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="setPanel('more')">
|
<menu-entry @click.native="settings">
|
||||||
More...
|
<icon-settings slot="icon"></icon-settings>
|
||||||
|
<div>Settings</div>
|
||||||
|
<span>Tweak application and keyboard shortcuts.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="templates">
|
||||||
|
<icon-code-braces slot="icon"></icon-code-braces>
|
||||||
|
<div><div class="menu-entry__label menu-entry__label--count">{{templateCount}}</div> Templates</div>
|
||||||
|
<span>Configure Handlebars templates for your exports.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="accounts">
|
||||||
|
<icon-key slot="icon"></icon-key>
|
||||||
|
<div><div class="menu-entry__label menu-entry__label--count">{{accountCount}}</div> User accounts</div>
|
||||||
|
<span>Manage access to your external accounts.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="exportWorkspace">
|
||||||
|
<icon-content-save slot="icon"></icon-content-save>
|
||||||
|
Export workspace backup
|
||||||
|
</menu-entry>
|
||||||
|
<input class="hidden-file" id="import-backup-file-input" type="file" @change="onImportBackup">
|
||||||
|
<label class="menu-entry button flex flex--row flex--align-center" for="import-backup-file-input">
|
||||||
|
<div class="menu-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-content-save></icon-content-save>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex--column">
|
||||||
|
Import workspace backup
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<menu-entry @click.native="reset">
|
||||||
|
<icon-logout slot="icon"></icon-logout>
|
||||||
|
<div>Reset application</div>
|
||||||
|
<span>Sign out and clean all workspaces.</span>
|
||||||
|
</menu-entry>
|
||||||
|
<hr>
|
||||||
|
<menu-entry @click.native="about">
|
||||||
|
<icon-help-circle slot="icon"></icon-help-circle>
|
||||||
|
About StackEdit
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -102,6 +138,8 @@ import UserImage from '../UserImage';
|
||||||
import googleHelper from '../../services/providers/helpers/googleHelper';
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
import syncSvc from '../../services/syncSvc';
|
import syncSvc from '../../services/syncSvc';
|
||||||
import userSvc from '../../services/userSvc';
|
import userSvc from '../../services/userSvc';
|
||||||
|
import backupSvc from '../../services/backupSvc';
|
||||||
|
import utils from '../../services/utils';
|
||||||
import store from '../../store';
|
import store from '../../store';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -131,6 +169,13 @@ export default {
|
||||||
publishLocationCount() {
|
publishLocationCount() {
|
||||||
return Object.keys(store.getters['publishLocation/current']).length;
|
return Object.keys(store.getters['publishLocation/current']).length;
|
||||||
},
|
},
|
||||||
|
templateCount() {
|
||||||
|
return Object.keys(store.getters['data/allTemplatesById']).length;
|
||||||
|
},
|
||||||
|
accountCount() {
|
||||||
|
return Object.values(store.getters['data/tokensByType'])
|
||||||
|
.reduce((count, tokensBySub) => count + Object.values(tokensBySub).length, 0);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('data', {
|
...mapActions('data', {
|
||||||
|
@ -154,6 +199,64 @@ export default {
|
||||||
print() {
|
print() {
|
||||||
window.print();
|
window.print();
|
||||||
},
|
},
|
||||||
|
onImportBackup(evt) {
|
||||||
|
const file = evt.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const text = e.target.result;
|
||||||
|
if (text.match(/\uFFFD/)) {
|
||||||
|
store.dispatch('notification/error', 'File is not readable.');
|
||||||
|
} else {
|
||||||
|
backupSvc.importBackup(text);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const blob = file.slice(0, 10000000);
|
||||||
|
reader.readAsText(blob);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exportWorkspace() {
|
||||||
|
window.location.href = utils.addQueryParams('app', {
|
||||||
|
...utils.queryParams,
|
||||||
|
exportWorkspace: true,
|
||||||
|
}, true);
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
async settings() {
|
||||||
|
try {
|
||||||
|
const settings = await store.dispatch('modal/open', 'settings');
|
||||||
|
store.dispatch('data/setSettings', settings);
|
||||||
|
} catch (e) {
|
||||||
|
// Cancel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async templates() {
|
||||||
|
try {
|
||||||
|
const { templates } = await store.dispatch('modal/open', 'templates');
|
||||||
|
store.dispatch('data/setTemplatesById', templates);
|
||||||
|
} catch (e) {
|
||||||
|
// Cancel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async accounts() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', 'accountManagement');
|
||||||
|
} catch (e) {
|
||||||
|
// Cancel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async reset() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', 'reset');
|
||||||
|
window.location.href = '#reset=true';
|
||||||
|
window.location.reload();
|
||||||
|
} catch (e) {
|
||||||
|
// Cancel
|
||||||
|
}
|
||||||
|
},
|
||||||
|
about() {
|
||||||
|
store.dispatch('modal/open', 'about');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,23 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||||
<p><b>{{currentFileName}}</b> can not be published as it's a temporary file.</p>
|
<p>{{currentFileName}} can't be published as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<p>You have to <b>link an account</b> to start publishing files.</p>
|
<p>You have to <b>link an account</b> to start publishing files.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar__info" v-if="publishLocations.length">
|
<div class="side-bar__info" v-if="publishLocations.length">
|
||||||
<p><b>{{currentFileName}}</b> is already published.</p>
|
<p>{{currentFileName}} is already published.</p>
|
||||||
<menu-entry @click.native="requestPublish">
|
<menu-entry @click.native="requestPublish">
|
||||||
<icon-upload slot="icon"></icon-upload>
|
<icon-upload slot="icon"></icon-upload>
|
||||||
<div>Publish now</div>
|
<div>Publish now</div>
|
||||||
<span>Update current file publications.</span>
|
<span>Update publications for {{currentFileName}}.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="managePublish">
|
<menu-entry @click.native="managePublish">
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File publication</div>
|
||||||
<span>Manage current file publication locations.</span>
|
<span>Manage publication locations for {{currentFileName}}.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -157,7 +157,7 @@ export default {
|
||||||
return Object.keys(this.publishLocations).length;
|
return Object.keys(this.publishLocations).length;
|
||||||
},
|
},
|
||||||
currentFileName() {
|
currentFileName() {
|
||||||
return store.getters['file/current'].name;
|
return `"${store.getters['file/current'].name}"`;
|
||||||
},
|
},
|
||||||
bloggerTokens() {
|
bloggerTokens() {
|
||||||
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger);
|
return tokensToArray(store.getters['data/googleTokensBySub'], token => token.isBlogger);
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="side-bar__panel side-bar__panel--menu">
|
<div class="side-bar__panel side-bar__panel--menu">
|
||||||
<div class="side-bar__info" v-if="isCurrentTemp">
|
<div class="side-bar__info" v-if="isCurrentTemp">
|
||||||
<p><b>{{currentFileName}}</b> can not be synced as it's a temporary file.</p>
|
<p>{{currentFileName}} can't be synced as it's a temporary file.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="side-bar__info" v-if="noToken">
|
<div class="side-bar__info" v-if="noToken">
|
||||||
<p>You have to <b>link an account</b> to start syncing files.</p>
|
<p>You have to <b>link an account</b> to start syncing files.</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="side-bar__info" v-if="syncLocations.length">
|
<div class="side-bar__info" v-if="syncLocations.length">
|
||||||
<p><b>{{currentFileName}}</b> is already synchronized.</p>
|
<p>{{currentFileName}} is already synchronized.</p>
|
||||||
<menu-entry @click.native="requestSync">
|
<menu-entry @click.native="requestSync">
|
||||||
<icon-sync slot="icon"></icon-sync>
|
<icon-sync slot="icon"></icon-sync>
|
||||||
<div>Synchronize now</div>
|
<div>Synchronize now</div>
|
||||||
|
@ -17,7 +17,7 @@
|
||||||
<menu-entry @click.native="manageSync">
|
<menu-entry @click.native="manageSync">
|
||||||
<icon-view-list slot="icon"></icon-view-list>
|
<icon-view-list slot="icon"></icon-view-list>
|
||||||
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
|
<div><div class="menu-entry__label menu-entry__label--count">{{locationCount}}</div> File synchronization</div>
|
||||||
<span>Manage current file synchronized locations.</span>
|
<span>Manage synchronized locations for {{currentFileName}}.</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
@ -139,7 +139,7 @@ export default {
|
||||||
return Object.keys(this.syncLocations).length;
|
return Object.keys(this.syncLocations).length;
|
||||||
},
|
},
|
||||||
currentFileName() {
|
currentFileName() {
|
||||||
return store.getters['file/current'].name;
|
return `"${store.getters['file/current'].name}"`;
|
||||||
},
|
},
|
||||||
dropboxTokens() {
|
dropboxTokens() {
|
||||||
return tokensToArray(store.getters['data/dropboxTokensBySub']);
|
return tokensToArray(store.getters['data/dropboxTokensBySub']);
|
||||||
|
|
|
@ -9,19 +9,19 @@
|
||||||
<hr>
|
<hr>
|
||||||
<menu-entry @click.native="addCouchdbWorkspace">
|
<menu-entry @click.native="addCouchdbWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="couchdbWorkspace"></icon-provider>
|
||||||
<span>Add a <b>CouchDB</b> backed workspace</span>
|
<span>Add a <b>CouchDB</b> workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGithubWorkspace">
|
<menu-entry @click.native="addGithubWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="githubWorkspace"></icon-provider>
|
||||||
<span>Add a <b>GitHub</b> backed workspace</span>
|
<span>Add a <b>GitHub</b> workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGitlabWorkspace">
|
<menu-entry @click.native="addGitlabWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="gitlabWorkspace"></icon-provider>
|
||||||
<span>Add a <b>GitLab</b> backed workspace</span>
|
<span>Add a <b>GitLab</b> workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="addGoogleDriveWorkspace">
|
<menu-entry @click.native="addGoogleDriveWorkspace">
|
||||||
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
<icon-provider slot="icon" provider-id="googleDriveWorkspace"></icon-provider>
|
||||||
<span>Add a <b>Google Drive</b> backed workspace</span>
|
<span>Add a <b>Google Drive</b> workspace</span>
|
||||||
</menu-entry>
|
</menu-entry>
|
||||||
<menu-entry @click.native="manageWorkspaces">
|
<menu-entry @click.native="manageWorkspaces">
|
||||||
<icon-database slot="icon"></icon-database>
|
<icon-database slot="icon"></icon-database>
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<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-2018 Dock5 Software Ltd.<br>v{{version}}</small>
|
<small>© 2013-2019 Dock5 Software Ltd.<br>v{{version}}</small>
|
||||||
<hr>
|
<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>
|
||||||
|
|
267
src/components/modals/AccountManagementModal.vue
Normal file
267
src/components/modals/AccountManagementModal.vue
Normal file
|
@ -0,0 +1,267 @@
|
||||||
|
<template>
|
||||||
|
<modal-inner class="modal__inner-1--account-management" aria-label="Manage external accounts">
|
||||||
|
<div class="modal__content">
|
||||||
|
<div class="modal__image">
|
||||||
|
<icon-key></icon-key>
|
||||||
|
</div>
|
||||||
|
<p v-if="entries.length">StackEdit has access to the following external accounts:</p>
|
||||||
|
<p v-else>StackEdit has no access to any external account yet.</p>
|
||||||
|
<div>
|
||||||
|
<div class="account-entry flex flex--column" v-for="entry in entries" :key="entry.token.sub">
|
||||||
|
<div class="account-entry__header flex flex--row flex--align-center">
|
||||||
|
<div class="account-entry__icon flex flex--column flex--center">
|
||||||
|
<icon-provider :provider-id="entry.providerId"></icon-provider>
|
||||||
|
</div>
|
||||||
|
<div class="account-entry__description">
|
||||||
|
{{entry.name}}
|
||||||
|
</div>
|
||||||
|
<div class="account-entry__buttons flex flex--row flex--center">
|
||||||
|
<button class="account-entry__button button" @click="remove(entry)" v-title="'Remove access'">
|
||||||
|
<icon-delete></icon-delete>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="account-entry__row">
|
||||||
|
<span v-if="entry.userId">
|
||||||
|
<b>User ID:</b>
|
||||||
|
{{entry.userId}}
|
||||||
|
</span>
|
||||||
|
<span v-if="entry.url">
|
||||||
|
<b>URL:</b>
|
||||||
|
{{entry.url}}
|
||||||
|
</span>
|
||||||
|
<span v-if="entry.scopes">
|
||||||
|
<b>Scopes:</b>
|
||||||
|
{{entry.scopes.join(', ')}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<menu-entry @click.native="addBloggerAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="blogger"></icon-provider>
|
||||||
|
<span>Add Blogger account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addDropboxAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="dropbox"></icon-provider>
|
||||||
|
<span>Add Dropbox account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGithubAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="github"></icon-provider>
|
||||||
|
<span>Add GitHub account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGitlabAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="gitlab"></icon-provider>
|
||||||
|
<span>Add GitLab account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGoogleDriveAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="googleDrive"></icon-provider>
|
||||||
|
<span>Add Google Drive account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addGooglePhotosAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="googlePhotos"></icon-provider>
|
||||||
|
<span>Add Google Photos account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addWordpressAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="wordpress"></icon-provider>
|
||||||
|
<span>Add WordPress account</span>
|
||||||
|
</menu-entry>
|
||||||
|
<menu-entry @click.native="addZendeskAccount">
|
||||||
|
<icon-provider slot="icon" provider-id="zendesk"></icon-provider>
|
||||||
|
<span>Add Zendesk account</span>
|
||||||
|
</menu-entry>
|
||||||
|
</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 MenuEntry from '../menus/common/MenuEntry';
|
||||||
|
import store from '../../store';
|
||||||
|
import utils from '../../services/utils';
|
||||||
|
import googleHelper from '../../services/providers/helpers/googleHelper';
|
||||||
|
import dropboxHelper from '../../services/providers/helpers/dropboxHelper';
|
||||||
|
import githubHelper from '../../services/providers/helpers/githubHelper';
|
||||||
|
import gitlabHelper from '../../services/providers/helpers/gitlabHelper';
|
||||||
|
import wordpressHelper from '../../services/providers/helpers/wordpressHelper';
|
||||||
|
import zendeskHelper from '../../services/providers/helpers/zendeskHelper';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalInner,
|
||||||
|
MenuEntry,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters('modal', [
|
||||||
|
'config',
|
||||||
|
]),
|
||||||
|
entries() {
|
||||||
|
return [
|
||||||
|
...Object.values(store.getters['data/googleTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'google',
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
scopes: ['openid', 'profile', ...token.scopes
|
||||||
|
.map(scope => scope.replace(/^https:\/\/www.googleapis.com\/auth\//, ''))],
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/couchdbTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'couchdb',
|
||||||
|
url: token.dbUrl,
|
||||||
|
name: token.name,
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/dropboxTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'dropbox',
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/githubTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'github',
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
scopes: token.scopes,
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/gitlabTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'gitlab',
|
||||||
|
url: token.serverUrl,
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
scopes: ['api'],
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/wordpressTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'wordpress',
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
scopes: ['global'],
|
||||||
|
})),
|
||||||
|
...Object.values(store.getters['data/zendeskTokensBySub']).map(token => ({
|
||||||
|
token,
|
||||||
|
providerId: 'zendesk',
|
||||||
|
url: `https://${token.subdomain}.zendesk.com/`,
|
||||||
|
userId: token.sub,
|
||||||
|
name: token.name,
|
||||||
|
scopes: ['read', 'hc:write'],
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async remove(entry) {
|
||||||
|
const tokensBySub = utils.deepCopy(store.getters[`data/${entry.providerId}TokensBySub`]);
|
||||||
|
delete tokensBySub[entry.token.sub];
|
||||||
|
await store.dispatch('data/patchTokensByType', {
|
||||||
|
[entry.providerId]: tokensBySub,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async addBloggerAccount() {
|
||||||
|
try {
|
||||||
|
await googleHelper.addBloggerAccount();
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addDropboxAccount() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', { type: 'dropboxAccount' });
|
||||||
|
await dropboxHelper.addAccount(!store.getters['data/localSettings'].dropboxRestrictedAccess);
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addGithubAccount() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', { type: 'githubAccount' });
|
||||||
|
await githubHelper.addAccount(store.getters['data/localSettings'].githubRepoFullAccess);
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addGitlabAccount() {
|
||||||
|
try {
|
||||||
|
const { serverUrl, applicationId } = await store.dispatch('modal/open', { type: 'gitlabAccount' });
|
||||||
|
await gitlabHelper.addAccount(serverUrl, applicationId);
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addGoogleDriveAccount() {
|
||||||
|
try {
|
||||||
|
await store.dispatch('modal/open', { type: 'googleDriveAccount' });
|
||||||
|
await googleHelper.addDriveAccount(!store.getters['data/localSettings'].googleDriveRestrictedAccess);
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addGooglePhotosAccount() {
|
||||||
|
try {
|
||||||
|
await googleHelper.addPhotosAccount();
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addWordpressAccount() {
|
||||||
|
try {
|
||||||
|
await wordpressHelper.addAccount();
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
async addZendeskAccount() {
|
||||||
|
try {
|
||||||
|
const { subdomain, clientId } = await store.dispatch('modal/open', { type: 'zendeskAccount' });
|
||||||
|
await zendeskHelper.addAccount(subdomain, clientId);
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '../../styles/variables.scss';
|
||||||
|
|
||||||
|
.account-entry {
|
||||||
|
margin: 1.5em 0;
|
||||||
|
height: auto;
|
||||||
|
font-size: 17px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
$button-size: 30px;
|
||||||
|
|
||||||
|
.account-entry__header {
|
||||||
|
line-height: $button-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__row {
|
||||||
|
border-top: 1px solid $hr-color;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.67em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__icon {
|
||||||
|
height: 22px;
|
||||||
|
width: 22px;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__description {
|
||||||
|
width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__buttons {
|
||||||
|
margin-left: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.account-entry__button {
|
||||||
|
width: $button-size;
|
||||||
|
height: $button-size;
|
||||||
|
padding: 4px;
|
||||||
|
background-color: transparent;
|
||||||
|
opacity: 0.75;
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&:focus,
|
||||||
|
&:hover {
|
||||||
|
opacity: 1;
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -58,8 +58,10 @@ export default modalTemplate({
|
||||||
this.config.reject();
|
this.config.reject();
|
||||||
callback(null);
|
callback(null);
|
||||||
},
|
},
|
||||||
addGooglePhotosAccount() {
|
async addGooglePhotosAccount() {
|
||||||
return googleHelper.addPhotosAccount();
|
try {
|
||||||
|
await googleHelper.addPhotosAccount();
|
||||||
|
} catch (e) { /* cancel */ }
|
||||||
},
|
},
|
||||||
async openGooglePhotos(token) {
|
async openGooglePhotos(token) {
|
||||||
const { callback } = this.config;
|
const { callback } = this.config;
|
||||||
|
|
|
@ -95,9 +95,7 @@ $small-button-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.publish-entry__row {
|
.publish-entry__row {
|
||||||
margin-top: 1px;
|
border-top: 1px solid $hr-color;
|
||||||
padding-top: 1px;
|
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
|
||||||
line-height: $small-button-size;
|
line-height: $small-button-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -99,9 +99,7 @@ $small-button-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sync-entry__row {
|
.sync-entry__row {
|
||||||
margin-top: 1px;
|
border-top: 1px solid $hr-color;
|
||||||
padding-top: 1px;
|
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
|
||||||
line-height: $small-button-size;
|
line-height: $small-button-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__info">
|
<div class="modal__info">
|
||||||
<b>ProTip:</b> A workspace is accessible <b>offline</b> once it has been opened for the first time.
|
<b>ProTip:</b> Workspaces are accessible offline, try it!
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal__button-bar">
|
<div class="modal__button-bar">
|
||||||
|
@ -149,9 +149,7 @@ $small-button-size: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-entry__row {
|
.workspace-entry__row {
|
||||||
margin-top: 1px;
|
border-top: 1px solid $hr-color;
|
||||||
padding-top: 1px;
|
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.15);
|
|
||||||
line-height: $small-button-size;
|
line-height: $small-button-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
5
src/icons/Key.vue
Normal file
5
src/icons/Key.vue
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24">
|
||||||
|
<path d="M 7,14C 5.9,14 5,13.1 5,12C 5,10.9 5.9,10 7,10C 8.1,10 9,10.9 9,12C 9,13.1 8.1,14 7,14 Z M 12.65,10C 11.83,7.67 9.61,6 7,6C 3.69,6 1,8.69 1,12C 1,15.31 3.69,18 7,18C 9.61,18 11.83,16.33 12.65,14L 17,14L 17,18L 21,18L 21,14L 23,14L 23,10L 12.65,10 Z "/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
|
@ -63,6 +63,10 @@ export default {
|
||||||
background-image: url(../assets/iconGitlab.svg);
|
background-image: url(../assets/iconGitlab.svg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-provider--google {
|
||||||
|
background-image: url(../assets/iconGoogle.svg);
|
||||||
|
}
|
||||||
|
|
||||||
.icon-provider--dropbox {
|
.icon-provider--dropbox {
|
||||||
background-image: url(../assets/iconDropbox.svg);
|
background-image: url(../assets/iconDropbox.svg);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,6 +51,7 @@ import Magnify from './Magnify';
|
||||||
import FormatListChecks from './FormatListChecks';
|
import FormatListChecks from './FormatListChecks';
|
||||||
import CheckCircle from './CheckCircle';
|
import CheckCircle from './CheckCircle';
|
||||||
import ContentCopy from './ContentCopy';
|
import ContentCopy from './ContentCopy';
|
||||||
|
import Key from './Key';
|
||||||
|
|
||||||
Vue.component('iconProvider', Provider);
|
Vue.component('iconProvider', Provider);
|
||||||
Vue.component('iconFormatBold', FormatBold);
|
Vue.component('iconFormatBold', FormatBold);
|
||||||
|
@ -104,3 +105,4 @@ Vue.component('iconMagnify', Magnify);
|
||||||
Vue.component('iconFormatListChecks', FormatListChecks);
|
Vue.component('iconFormatListChecks', FormatListChecks);
|
||||||
Vue.component('iconCheckCircle', CheckCircle);
|
Vue.component('iconCheckCircle', CheckCircle);
|
||||||
Vue.component('iconContentCopy', ContentCopy);
|
Vue.component('iconContentCopy', ContentCopy);
|
||||||
|
Vue.component('iconKey', Key);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import userSvc from '../../userSvc';
|
||||||
const clientId = GOOGLE_CLIENT_ID;
|
const clientId = GOOGLE_CLIENT_ID;
|
||||||
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk';
|
||||||
const appsDomain = null;
|
const appsDomain = null;
|
||||||
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h)
|
const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (tokens expire after 1h)
|
||||||
let googlePlusNotification = true;
|
let googlePlusNotification = true;
|
||||||
|
|
||||||
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata'];
|
||||||
|
@ -37,20 +37,20 @@ if (utils.queryParams.providerId === 'googleDrive') {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://developers.google.com/+/web/api/rest/latest/people/get
|
* https://developers.google.com/people/api/rest/v1/people/get
|
||||||
*/
|
*/
|
||||||
const getUser = async (sub, token) => {
|
const getUser = async (sub, token) => {
|
||||||
const { body } = await networkSvc.request(token
|
const { body } = await networkSvc.request(token
|
||||||
? {
|
? {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `https://www.googleapis.com/plus/v1/people/${sub}`,
|
url: `https://people.googleapis.com/v1/people/${sub}`,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${token.accessToken}`,
|
Authorization: `Bearer ${token.accessToken}`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
url: `https://www.googleapis.com/plus/v1/people/${sub}?key=${apiKey}`,
|
url: `https://people.googleapis.com/v1/people/${sub}?key=${apiKey}`,
|
||||||
}, true);
|
}, true);
|
||||||
return body;
|
return body;
|
||||||
};
|
};
|
||||||
|
@ -141,22 +141,25 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build token object including scopes and sub
|
// Build token object including scopes and sub
|
||||||
const existingToken = store.getters['data/googleTokensBySub'][body.sub];
|
const existingToken = store.getters['data/googleTokensBySub'][body.sub] || {
|
||||||
|
scopes: [],
|
||||||
|
};
|
||||||
|
const mergedScopes = [...new Set([...scopes, ...existingToken.scopes])];
|
||||||
const token = {
|
const token = {
|
||||||
scopes,
|
scopes: mergedScopes,
|
||||||
accessToken,
|
accessToken,
|
||||||
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 || 'Unknown',
|
||||||
isLogin: !store.getters['workspace/mainWorkspaceToken'] &&
|
isLogin: existingToken.isLogin || (!store.getters['workspace/mainWorkspaceToken'] &&
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1,
|
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.appdata') !== -1),
|
||||||
isSponsor: false,
|
isSponsor: existingToken.isSponsor || false,
|
||||||
isDrive: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
isDrive: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1 ||
|
||||||
scopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
mergedScopes.indexOf('https://www.googleapis.com/auth/drive.file') !== -1,
|
||||||
isBlogger: scopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
isBlogger: mergedScopes.indexOf('https://www.googleapis.com/auth/blogger') !== -1,
|
||||||
isPhotos: scopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
isPhotos: mergedScopes.indexOf('https://www.googleapis.com/auth/photos') !== -1,
|
||||||
driveFullAccess: scopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
driveFullAccess: mergedScopes.indexOf('https://www.googleapis.com/auth/drive') !== -1,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Call the user info endpoint
|
// Call the user info endpoint
|
||||||
|
@ -173,20 +176,6 @@ export default {
|
||||||
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
imageUrl: (user.image.url || '').replace(/\bsz?=\d+$/, 'sz=40'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingToken) {
|
|
||||||
// We probably retrieved a new token with restricted scopes.
|
|
||||||
// That's no problem, token will be refreshed later with merged scopes.
|
|
||||||
// Restore flags
|
|
||||||
Object.assign(token, {
|
|
||||||
isLogin: existingToken.isLogin || token.isLogin,
|
|
||||||
isSponsor: existingToken.isSponsor,
|
|
||||||
isDrive: existingToken.isDrive || token.isDrive,
|
|
||||||
isBlogger: existingToken.isBlogger || token.isBlogger,
|
|
||||||
isPhotos: existingToken.isPhotos || token.isPhotos,
|
|
||||||
driveFullAccess: existingToken.driveFullAccess || token.driveFullAccess,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (token.isLogin) {
|
if (token.isLogin) {
|
||||||
try {
|
try {
|
||||||
token.isSponsor = (await networkSvc.request({
|
token.isSponsor = (await networkSvc.request({
|
||||||
|
@ -202,7 +191,7 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add token to google tokens
|
// Add token to google tokens
|
||||||
store.dispatch('data/addGoogleToken', token);
|
await store.dispatch('data/addGoogleToken', token);
|
||||||
return token;
|
return token;
|
||||||
},
|
},
|
||||||
async refreshToken(token, scopes = []) {
|
async refreshToken(token, scopes = []) {
|
||||||
|
|
|
@ -75,7 +75,7 @@ const makeAdditionalTemplate = (name, value, helpers = '\n') => ({
|
||||||
helpers,
|
helpers,
|
||||||
isAdditional: true,
|
isAdditional: true,
|
||||||
});
|
});
|
||||||
const additionalTemplates = {
|
const defaultTemplates = {
|
||||||
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
plainText: makeAdditionalTemplate('Plain text', '{{{files.0.content.text}}}'),
|
||||||
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
plainHtml: makeAdditionalTemplate('Plain HTML', plainHtmlTemplate),
|
||||||
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
styledHtml: makeAdditionalTemplate('Styled HTML', styledHtmlTemplate),
|
||||||
|
@ -153,7 +153,7 @@ export default {
|
||||||
templatesById: getter('templates'),
|
templatesById: getter('templates'),
|
||||||
allTemplatesById: (state, { templatesById }) => ({
|
allTemplatesById: (state, { templatesById }) => ({
|
||||||
...templatesById,
|
...templatesById,
|
||||||
...additionalTemplates,
|
...defaultTemplates,
|
||||||
}),
|
}),
|
||||||
lastCreated: getter('lastCreated'),
|
lastCreated: getter('lastCreated'),
|
||||||
lastOpened: getter('lastOpened'),
|
lastOpened: getter('lastOpened'),
|
||||||
|
@ -238,7 +238,7 @@ export default {
|
||||||
...templatesById,
|
...templatesById,
|
||||||
};
|
};
|
||||||
// We don't store additional templates
|
// We don't store additional templates
|
||||||
Object.keys(additionalTemplates).forEach((id) => {
|
Object.keys(defaultTemplates).forEach((id) => {
|
||||||
delete templatesToCommit[id];
|
delete templatesToCommit[id];
|
||||||
});
|
});
|
||||||
commit('setItem', itemTemplate('templates', templatesToCommit));
|
commit('setItem', itemTemplate('templates', templatesToCommit));
|
||||||
|
|
|
@ -8,7 +8,7 @@ $line-height-title: 1.33;
|
||||||
$font-size-monospace: 0.85em;
|
$font-size-monospace: 0.85em;
|
||||||
$highlighting-color: #ff0;
|
$highlighting-color: #ff0;
|
||||||
$selection-highlighting-color: #ff9632;
|
$selection-highlighting-color: #ff9632;
|
||||||
$info-bg: transparentize($selection-highlighting-color, 0.85);
|
$info-bg: #ffad3326;
|
||||||
$code-border-radius: 3px;
|
$code-border-radius: 3px;
|
||||||
$link-color: #0c93e4;
|
$link-color: #0c93e4;
|
||||||
$error-color: #f31;
|
$error-color: #f31;
|
||||||
|
|
|
@ -450,7 +450,7 @@
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<a href="app" title="The app">The app</a> – <a href="https://community.stackedit.io" target="_blank" title="The app">Community</a><br>
|
<a href="app" title="The app">The app</a> – <a href="https://community.stackedit.io" target="_blank" title="The app">Community</a><br>
|
||||||
Copyright 2013-2018 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br>
|
Copyright 2013-2019 <a href="https://twitter.com/benweet" target="_blank">Benoit Schweblin</a><br>
|
||||||
Licensed under an
|
Licensed under an
|
||||||
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a> –
|
<a target="_blank" href="http://www.apache.org/licenses/LICENSE-2.0">Apache License</a> –
|
||||||
<a href="privacy_policy.html" target="_blank">Privacy Policy</a>
|
<a href="privacy_policy.html" target="_blank">Privacy Policy</a>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user