diff --git a/build/webpack.base.conf.js b/build/webpack.base.conf.js index d1720fa6..f0f27e48 100644 --- a/build/webpack.base.conf.js +++ b/build/webpack.base.conf.js @@ -5,7 +5,6 @@ var config = require('../config') var VueLoaderPlugin = require('vue-loader/lib/plugin') var vueLoaderConfig = require('./vue-loader.conf') var StylelintPlugin = require('stylelint-webpack-plugin') -var FaviconsWebpackPlugin = require('favicons-webpack-plugin') function resolve (dir) { return path.join(__dirname, '..', dir) @@ -48,16 +47,33 @@ module.exports = { loader: 'vue-loader', options: vueLoaderConfig }, + // We can't pass graphlibrary to babel + { + test: /\.js$/, + loader: 'string-replace-loader', + include: [ + resolve('node_modules/graphlibrary') + ], + options: { + search: '^\\s*(?:let|const) ', + replace: 'var ', + flags: 'gm' + } + }, { test: /\.js$/, loader: 'babel-loader', - include: [resolve('src'), resolve('test'), resolve('node_modules/mermaid/src')], + include: [ + resolve('src'), + resolve('test'), + resolve('node_modules/mermaid') + ], exclude: [ resolve('node_modules/mermaid/src/diagrams/class/parser'), resolve('node_modules/mermaid/src/diagrams/flowchart/parser'), resolve('node_modules/mermaid/src/diagrams/gantt/parser'), resolve('node_modules/mermaid/src/diagrams/git/parser'), - resolve('node_modules/mermaid/src/diagrams/sequence/parser'), + resolve('node_modules/mermaid/src/diagrams/sequence/parser') ], }, { @@ -86,10 +102,6 @@ module.exports = { new StylelintPlugin({ files: ['**/*.vue', '**/*.scss'] }), - new FaviconsWebpackPlugin({ - logo: resolve('src/assets/favicon.png'), - title: 'StackEdit', - }), new webpack.DefinePlugin({ VERSION: JSON.stringify(require('../package.json').version) }) diff --git a/build/webpack.prod.conf.js b/build/webpack.prod.conf.js index 0118c749..e88398e9 100644 --- a/build/webpack.prod.conf.js +++ b/build/webpack.prod.conf.js @@ -9,6 +9,12 @@ var HtmlWebpackPlugin = require('html-webpack-plugin') var ExtractTextPlugin = require('extract-text-webpack-plugin') var OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') var OfflinePlugin = require('offline-plugin'); +var WebpackPwaManifest = require('webpack-pwa-manifest') +var FaviconsWebpackPlugin = require('favicons-webpack-plugin') + +function resolve (dir) { + return path.join(__dirname, '..', dir) +} var env = config.build.env @@ -94,6 +100,22 @@ var webpackConfig = merge(baseWebpackConfig, { ignore: ['.*'] } ]), + new FaviconsWebpackPlugin({ + logo: resolve('src/assets/favicon.png'), + title: 'StackEdit', + }), + new WebpackPwaManifest({ + name: 'StackEdit', + description: 'Full-featured, open-source Markdown editor', + display: 'standalone', + start_url: 'app', + background_color: '#ffffff', + crossorigin: 'use-credentials', + icons: [{ + src: resolve('src/assets/favicon.png'), + sizes: [96, 128, 192, 256, 384, 512] + }] + }), new OfflinePlugin({ ServiceWorker: { events: true @@ -101,7 +123,7 @@ var webpackConfig = merge(baseWebpackConfig, { AppCache: true, excludes: ['**/.*', '**/*.map', '**/index.html', '**/static/oauth2/callback.html', '**/icons-*/*.png', '**/static/fonts/KaTeX_*'], externals: ['/', '/app', '/oauth2/callback'] - }) + }), ] }) diff --git a/package-lock.json b/package-lock.json index ca0dcee4..e7cda4bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16854,6 +16854,69 @@ } } }, + "string-replace-loader": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-replace-loader/-/string-replace-loader-2.1.1.tgz", + "integrity": "sha512-0Nvw1LDclF45AFNuYPcD2Jvkv0mwb/dQSnJZMvhqGrT+zzmrpG3OJFD600qfQfNUd5aqfp7fCm2mQMfF7zLbyQ==", + "dev": true, + "requires": { + "loader-utils": "^1.1.0", + "schema-utils": "^0.4.5" + }, + "dependencies": { + "ajv": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.5.3.tgz", + "integrity": "sha512-LqZ9wY+fx3UMiiPd741yB2pj3hhil+hQc8taf4o2QGRFpWgZ2V5C8HA165DY9sS3fJwsk7uT7ZlFEyC3Ig3lLg==", + "dev": true, + "requires": { + "fast-deep-equal": "^2.0.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.2.0.tgz", + "integrity": "sha1-6GuBnGAs+IIa1jdBNpjx3sAhhHo=", + "dev": true + }, + "fast-deep-equal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", + "integrity": "sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk=", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "loader-utils": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.1.0.tgz", + "integrity": "sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0=", + "dev": true, + "requires": { + "big.js": "^3.1.3", + "emojis-list": "^2.0.0", + "json5": "^0.5.0" + } + }, + "schema-utils": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", + "integrity": "sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-keywords": "^3.1.0" + } + } + } + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -19774,6 +19837,25 @@ } } }, + "webpack-pwa-manifest": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/webpack-pwa-manifest/-/webpack-pwa-manifest-3.7.1.tgz", + "integrity": "sha512-G37fVCa1ndij3jyz6WaOaxHLHdp2URyOHwp2GLmxt39sXL8ZdOFM1qvHagEJBkNh+3hu25eIgy6TD5J/8sgQcQ==", + "dev": true, + "requires": { + "css-color-names": "0.0.4", + "jimp": "^0.2.28", + "mime": "^1.6.0" + }, + "dependencies": { + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true + } + } + }, "webpack-sources": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.1.0.tgz", diff --git a/package.json b/package.json index 4c5035a1..1580eee3 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "sass-loader": "^7.0.1", "semver": "^5.5.0", "shelljs": "^0.8.1", + "string-replace-loader": "^2.1.1", "stylelint": "^9.2.0", "stylelint-config-standard": "^16.0.0", "stylelint-processor-html": "^1.0.0", @@ -124,6 +125,7 @@ "webpack-dev-middleware": "^1.10.0", "webpack-hot-middleware": "^2.18.0", "webpack-merge": "^4.1.2", + "webpack-pwa-manifest": "^3.7.1", "worker-loader": "^1.1.1" }, "engines": { diff --git a/src/components/Notification.vue b/src/components/Notification.vue index e2a94b46..f1c0ee9c 100644 --- a/src/components/Notification.vue +++ b/src/components/Notification.vue @@ -2,12 +2,18 @@
- - + +
{{item.content}}
+ +
@@ -49,4 +55,17 @@ export default { margin-right: 12px; flex: none; } + +.notification__button { + color: $navbar-color; + padding: 8px; + flex: none; + + &:active, + &:focus, + &:hover { + color: $navbar-hover-color; + background-color: $navbar-hover-background; + } +} diff --git a/src/index.js b/src/index.js index 8b9c79cb..8f373d90 100644 --- a/src/index.js +++ b/src/index.js @@ -33,6 +33,22 @@ if (localStorage.updated) { setTimeout(() => localStorage.removeItem('updated'), 2000); } +if (!localStorage.installPrompted) { + window.addEventListener('beforeinstallprompt', async (promptEvent) => { + // Prevent Chrome 67 and earlier from automatically showing the prompt + promptEvent.preventDefault(); + + try { + await store.dispatch('notification/confirm', 'Add StackEdit to your home screen?'); + promptEvent.prompt(); + await promptEvent.userChoice; + } catch (err) { + // Cancel + } + localStorage.installPrompted = true; + }); +} + Vue.config.productionTip = false; /* eslint-disable no-new */ diff --git a/src/services/providers/helpers/googleHelper.js b/src/services/providers/helpers/googleHelper.js index 997eaa99..e9b673e7 100644 --- a/src/services/providers/helpers/googleHelper.js +++ b/src/services/providers/helpers/googleHelper.js @@ -7,6 +7,7 @@ const clientId = GOOGLE_CLIENT_ID; const apiKey = 'AIzaSyC_M4RA9pY6XmM9pmFxlT59UPMO7aHr9kk'; const appsDomain = null; const tokenExpirationMargin = 5 * 60 * 1000; // 5 min (Google tokens expire after 1h) +let googlePlusNotification = true; const driveAppDataScopes = ['https://www.googleapis.com/auth/drive.appdata']; const getDriveScopes = token => [token.driveFullAccess @@ -160,7 +161,12 @@ export default { // Call the user info endpoint const user = await getUser('me', token); - token.name = user.displayName; + if (user.displayName) { + token.name = user.displayName; + } else if (googlePlusNotification) { + store.dispatch('notification/info', 'Please activate Google Plus to change your account name and photo.'); + googlePlusNotification = false; + } userSvc.addInfo({ id: `${subPrefix}:${user.id}`, name: user.displayName, @@ -449,10 +455,10 @@ export default { }, }); revisions.forEach((revision) => { - store.commit('userInfo/addItem', { + userSvc.addInfo({ id: `${subPrefix}:${revision.lastModifyingUser.permissionId}`, name: revision.lastModifyingUser.displayName, - imageUrl: revision.lastModifyingUser.photoLink, + imageUrl: revision.lastModifyingUser.photoLink || '', }); allRevisions.push(revision); }); diff --git a/src/store/notification.js b/src/store/notification.js index 5787c47e..d0ea8b84 100644 --- a/src/store/notification.js +++ b/src/store/notification.js @@ -1,6 +1,7 @@ import providerRegistry from '../services/providers/common/providerRegistry'; +import utils from '../services/utils'; -const defaultTimeout = 5000; +const defaultTimeout = 5000; // 5 sec export default { namespaced: true, @@ -14,20 +15,49 @@ export default { }, actions: { showItem({ state, commit }, item) { - if (state.items.every(other => other.type !== item.type || other.content !== item.content)) { + const existingItem = utils.someResult( + state.items, + other => other.type === item.type && other.content === item.content && item, + ); + if (existingItem) { + return existingItem.promise; + } + + item.promise = new Promise((resolve, reject) => { commit('setItems', [...state.items, item]); + const removeItem = () => commit( + 'setItems', + state.items.filter(otherItem => otherItem !== item), + ); setTimeout( - () => commit('setItems', state.items.filter(otherItem => otherItem !== item)), + () => removeItem(), item.timeout || defaultTimeout, ); - } + item.resolve = (res) => { + removeItem(); + resolve(res); + }; + item.reject = (err) => { + removeItem(); + reject(err); + }; + }); + + return item.promise; }, info({ dispatch }, info) { - dispatch('showItem', { + return dispatch('showItem', { type: 'info', content: info, }); }, + confirm({ dispatch }, question) { + return dispatch('showItem', { + type: 'confirm', + content: question, + timeout: 10000, // 10 sec + }); + }, error({ dispatch, rootState }, error) { const item = { type: 'error' }; if (error) { @@ -48,7 +78,7 @@ export default { if (!item.content || item.content === '[object Object]') { item.content = 'Unknown error.'; } - dispatch('showItem', item); + return dispatch('showItem', item); }, }, };