├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .firebaserc ├── .gitignore ├── .netlify ├── README.md ├── _redirects ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── utils.js ├── vue-loader.conf.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js └── prod.env.js ├── extension ├── background.js └── manifest.json ├── firebase.json ├── functions ├── index.js └── package.json ├── index.html ├── package.json ├── server ├── index.js ├── package.json ├── routes │ ├── mal.js │ └── opening.js └── yarn.lock ├── src ├── App.css ├── App.vue ├── assets │ ├── emoji │ │ ├── FrostyFridays.png │ │ ├── TsuyuW.png │ │ ├── UmiDorito.png │ │ └── Yousoro.png │ ├── umi-login.png │ └── umi.png ├── components │ ├── Collection.vue │ ├── DashboardLoadingSection.vue │ ├── EpisodeScroller.vue │ ├── Header.vue │ ├── LoadingMediaItem.vue │ ├── MediaItem.vue │ ├── QueueButton.vue │ ├── QueueItem.vue │ ├── Reactotron.vue │ ├── Search.vue │ ├── SeriesItem.vue │ └── Video.vue ├── lib │ ├── api.js │ ├── auth.js │ ├── bif.js │ ├── cdnRewrite.js │ ├── clappr-level-selector.js │ ├── emoji.js │ ├── firebase.js │ └── prettyTime.js ├── main.js ├── pages │ ├── Changelog.vue │ ├── Dashboard.vue │ ├── History.vue │ ├── Login.vue │ ├── Media.vue │ ├── Migrate.vue │ ├── Notice.vue │ ├── Queue.vue │ ├── Room.vue │ ├── Search.vue │ ├── Series.vue │ └── Settings.vue ├── router │ └── index.js └── store │ └── index.js ├── static └── .gitkeep └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "modules": false, 5 | "targets": { 6 | "chrome": 62, 7 | "firefox": 56, 8 | "ios": 11, 9 | "safari": 11, 10 | "edge": 15 11 | }, 12 | "loose": true, 13 | "useBuiltIns": true 14 | }] 15 | ], 16 | "comments": false 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/user-guide/configuring 2 | 3 | module.exports = { 4 | root: true, 5 | parserOptions: { 6 | parser: 'babel-eslint', 7 | sourceType: 'module' 8 | }, 9 | env: { 10 | browser: true, 11 | }, 12 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 13 | extends: ['plugin:vue/essential', 'standard'], 14 | // required to lint *.vue files 15 | plugins: [ 16 | 'vue' 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "umi-player" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | firebase-debug.log 6 | extension.zip 7 | -------------------------------------------------------------------------------- /.netlify: -------------------------------------------------------------------------------- 1 | {"site_id":"21b5bf8b-865b-459a-ad16-dcdf89e82d67","path":"dist"} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # umi 2 | 3 | > Better Crunchyroll 4 | 5 | ## Build Setup 6 | 7 | ``` bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # build for production and view the bundle analyzer report 18 | npm run build --report 19 | ``` 20 | 21 | For detailed explanation on how things work, checkout the [guide](http://vuejs-templates.github.io/webpack/) and [docs for vue-loader](http://vuejs.github.io/vue-loader). 22 | -------------------------------------------------------------------------------- /_redirects: -------------------------------------------------------------------------------- 1 | /cdn/* http://img1.ak.crunchyroll.com/:splat 200 2 | /pl-proxy/* https://pl.crunchyroll.com/:splat 200 3 | /appcache/* /index.html 404 4 | /* /index.html 200 5 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | // https://github.com/shelljs/shelljs 2 | require('./check-versions')() 3 | 4 | process.env.NODE_ENV = 'production' 5 | 6 | var ora = require('ora') 7 | var path = require('path') 8 | var chalk = require('chalk') 9 | var shell = require('shelljs') 10 | var webpack = require('webpack') 11 | var config = require('../config') 12 | var webpackConfig = require('./webpack.prod.conf') 13 | 14 | var spinner = ora('building for production...') 15 | spinner.start() 16 | 17 | var assetsPath = path.join(config.build.assetsRoot, config.build.assetsSubDirectory) 18 | shell.rm('-rf', assetsPath) 19 | shell.mkdir('-p', assetsPath) 20 | shell.config.silent = true 21 | shell.cp('-R', 'static/*', assetsPath) 22 | shell.config.silent = false 23 | 24 | webpack(webpackConfig, function (err, stats) { 25 | spinner.stop() 26 | if (err) throw err 27 | process.stdout.write(stats.toString({ 28 | colors: true, 29 | modules: false, 30 | children: false, 31 | chunks: false, 32 | chunkModules: false 33 | }) + '\n\n') 34 | 35 | console.log(chalk.cyan(' Build complete.\n')) 36 | console.log(chalk.yellow( 37 | ' Tip: built files are meant to be served over an HTTP server.\n' + 38 | ' Opening index.html over file:// won\'t work.\n' 39 | )) 40 | }) 41 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var chalk = require('chalk') 2 | var semver = require('semver') 3 | var packageConfig = require('../package.json') 4 | 5 | function exec (cmd) { 6 | return require('child_process').execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | 3 | var config = require('../config') 4 | if (!process.env.NODE_ENV) { 5 | process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 6 | } 7 | 8 | var opn = require('opn') 9 | var path = require('path') 10 | var express = require('express') 11 | var webpack = require('webpack') 12 | var proxyMiddleware = require('http-proxy-middleware') 13 | var webpackConfig = require('./webpack.dev.conf') 14 | 15 | // default port where dev server listens for incoming traffic 16 | var port = process.env.PORT || config.dev.port 17 | // automatically open browser, if not set will be false 18 | var autoOpenBrowser = !!config.dev.autoOpenBrowser 19 | // Define HTTP proxies to your custom API backend 20 | // https://github.com/chimurai/http-proxy-middleware 21 | var proxyTable = config.dev.proxyTable 22 | 23 | var app = express() 24 | var compiler = webpack(webpackConfig) 25 | 26 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 27 | publicPath: webpackConfig.output.publicPath, 28 | quiet: true 29 | }) 30 | 31 | var hotMiddleware = require('webpack-hot-middleware')(compiler, { 32 | log: () => {} 33 | }) 34 | 35 | // @TODO - fix this for webpack 3 36 | // force page reload when html-webpack-plugin template changes 37 | // compiler.plugin('compilation', function (compilation) { 38 | // compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 39 | // hotMiddleware.publish({ action: 'reload' }) 40 | // cb() 41 | // }) 42 | // }) 43 | 44 | // proxy api requests 45 | Object.keys(proxyTable).forEach(function (context) { 46 | var options = proxyTable[context] 47 | if (typeof options === 'string') { 48 | options = { target: options } 49 | } 50 | app.use(proxyMiddleware(options.filter || context, options)) 51 | }) 52 | 53 | // handle fallback for HTML5 history API 54 | app.use(require('connect-history-api-fallback')()) 55 | 56 | // serve webpack bundle output 57 | app.use(devMiddleware) 58 | 59 | // enable hot-reload and state-preserving 60 | // compilation error display 61 | app.use(hotMiddleware) 62 | 63 | // serve pure static assets 64 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 65 | app.use(staticPath, express.static('./static')) 66 | 67 | var uri = 'http://localhost:' + port 68 | 69 | devMiddleware.waitUntilValid(function () { 70 | console.log('> Listening at ' + uri + '\n') 71 | }) 72 | 73 | module.exports = app.listen(port, function (err) { 74 | if (err) { 75 | console.log(err) 76 | return 77 | } 78 | 79 | // when env is testing, don't need open it 80 | if (autoOpenBrowser && process.env.NODE_ENV !== 'testing') { 81 | opn(uri) 82 | } 83 | }) 84 | -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract({ 32 | use: sourceLoader, 33 | fallback: 'vue-style-loader' 34 | }) 35 | } else { 36 | return ['vue-style-loader', sourceLoader].join('!') 37 | } 38 | } 39 | 40 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 41 | return { 42 | css: generateLoaders(['css']), 43 | postcss: generateLoaders(['css']), 44 | less: generateLoaders(['css', 'less']), 45 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 46 | scss: generateLoaders(['css', 'sass']), 47 | stylus: generateLoaders(['css', 'stylus']), 48 | styl: generateLoaders(['css', 'stylus']) 49 | } 50 | } 51 | 52 | // Generate loaders for standalone style files (outside of .vue) 53 | exports.styleLoaders = function (options) { 54 | var output = [] 55 | var loaders = exports.cssLoaders(options) 56 | for (var extension in loaders) { 57 | var loader = loaders[extension] 58 | output.push({ 59 | test: new RegExp('\\.' + extension + '$'), 60 | loader: loader 61 | }) 62 | } 63 | return output 64 | } 65 | -------------------------------------------------------------------------------- /build/vue-loader.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var config = require('../config') 3 | var isProduction = process.env.NODE_ENV === 'production' 4 | 5 | module.exports = { 6 | loaders: utils.cssLoaders({ 7 | sourceMap: isProduction 8 | ? config.build.productionSourceMap 9 | : config.dev.cssSourceMap, 10 | extract: isProduction 11 | }), 12 | postcss: [ 13 | require('autoprefixer')({ 14 | browsers: ['last 2 versions'] 15 | }) 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var utils = require('./utils') 3 | var config = require('../config') 4 | var vueLoaderConfig = require('./vue-loader.conf') 5 | 6 | function resolve (dir) { 7 | return path.join(__dirname, '..', dir) 8 | } 9 | 10 | module.exports = { 11 | entry: { 12 | app: './src/main.js' 13 | }, 14 | output: { 15 | path: config.build.assetsRoot, 16 | filename: '[name].js', 17 | publicPath: process.env.NODE_ENV === 'production' 18 | ? config.build.assetsPublicPath 19 | : config.dev.assetsPublicPath 20 | }, 21 | node: { 22 | Buffer: false, 23 | // prevent webpack from injecting useless setImmediate polyfill because Vue 24 | // source contains it (although only uses it if it's native). 25 | setImmediate: false, 26 | // prevent webpack from injecting mocks to Node native modules 27 | // that does not make sense for the client 28 | dgram: 'empty', 29 | fs: 'empty', 30 | net: 'empty', 31 | tls: 'empty', 32 | child_process: 'empty' 33 | }, 34 | resolve: { 35 | extensions: ['.js', '.vue', '.json'], 36 | modules: [ 37 | resolve('src'), 38 | resolve('node_modules') 39 | ], 40 | alias: { 41 | 'src': resolve('src'), 42 | 'assets': resolve('src/assets'), 43 | 'emoji': resolve('src/assets/emoji'), 44 | 'components': resolve('src/components'), 45 | 'pages': resolve('src/pages'), 46 | 'lib': resolve('src/lib'), 47 | 'Clappr': 'clappr' 48 | } 49 | }, 50 | module: { 51 | rules: [ 52 | // { 53 | // test: /\.(js|vue)$/, 54 | // loader: 'eslint-loader', 55 | // enforce: "pre", 56 | // include: [resolve('src'), resolve('test')], 57 | // options: { 58 | // formatter: require('eslint-friendly-formatter') 59 | // } 60 | // }, 61 | { 62 | test: /\.vue$/, 63 | loader: 'vue-loader', 64 | options: vueLoaderConfig 65 | }, 66 | { 67 | test: /\.js$/, 68 | loader: 'babel-loader', 69 | include: [resolve('src')] 70 | }, 71 | { 72 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 73 | loader: 'file-loader', 74 | query: { 75 | name: utils.assetsPath('img/[name].[hash:7].[ext]') 76 | } 77 | }, 78 | { 79 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 80 | loader: 'url-loader', 81 | query: { 82 | limit: 10000, 83 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]') 84 | } 85 | } 86 | ] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils') 2 | var webpack = require('webpack') 3 | var config = require('../config') 4 | var merge = require('webpack-merge') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var HtmlWebpackPlugin = require('html-webpack-plugin') 7 | var FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // cheap-module-eval-source-map is faster for development 19 | devtool: '#cheap-module-eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.NoEmitOnErrorsPlugin(), 27 | // https://github.com/ampedandwired/html-webpack-plugin 28 | new HtmlWebpackPlugin({ 29 | filename: 'index.html', 30 | template: 'index.html', 31 | inject: true 32 | }), 33 | new FriendlyErrorsPlugin() 34 | ] 35 | }) 36 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const utils = require('./utils') 3 | const webpack = require('webpack') 4 | const config = require('../config') 5 | const merge = require('webpack-merge') 6 | const baseWebpackConfig = require('./webpack.base.conf') 7 | const HtmlWebpackPlugin = require('html-webpack-plugin') 8 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 9 | const OfflinePlugin = require('offline-plugin') 10 | const PurifyCSSPlugin = require('purifycss-webpack') 11 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 12 | const glob = require('glob') 13 | const env = config.build.env 14 | 15 | const webpackConfig = merge(baseWebpackConfig, { 16 | module: { 17 | rules: utils.styleLoaders({ 18 | sourceMap: config.build.productionSourceMap, 19 | extract: true 20 | }) 21 | }, 22 | devtool: config.build.productionSourceMap ? '#source-map' : false, 23 | output: { 24 | path: config.build.assetsRoot, 25 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 26 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 27 | }, 28 | plugins: [ 29 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 30 | new webpack.DefinePlugin({ 31 | 'process.env': env 32 | }), 33 | new UglifyJsPlugin({ 34 | uglifyOptions: { 35 | compress: { 36 | warnings: false 37 | } 38 | }, 39 | sourceMap: config.build.productionSourceMap, 40 | parallel: true 41 | }), 42 | // extract css into its own file 43 | new ExtractTextPlugin({ 44 | filename: utils.assetsPath('css/[name].[contenthash].css') 45 | }), 46 | new PurifyCSSPlugin({ 47 | paths: glob.sync(path.join(__dirname, '../src/**/*.vue')), 48 | minimize: true, 49 | purifyOptions: { 50 | whitelist: ['*data-v-*', 'sans-serif', '*tooltip*'] 51 | } 52 | }), 53 | // generate dist index.html with correct asset hash for caching. 54 | // you can customize output by editing /index.html 55 | // see https://github.com/ampedandwired/html-webpack-plugin 56 | new HtmlWebpackPlugin({ 57 | filename: config.build.index, 58 | template: 'index.html', 59 | inject: true, 60 | minify: { 61 | removeComments: true, 62 | collapseWhitespace: true, 63 | removeAttributeQuotes: true 64 | // more options: 65 | // https://github.com/kangax/html-minifier#options-quick-reference 66 | }, 67 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 68 | chunksSortMode: 'dependency' 69 | }), 70 | // enable scope hoisting 71 | new webpack.optimize.ModuleConcatenationPlugin(), 72 | // split vendor js into its own file 73 | new webpack.optimize.CommonsChunkPlugin({ 74 | name: 'vendor', 75 | minChunks: function (module, count) { 76 | // any required modules inside node_modules are extracted to vendor 77 | return ( 78 | module.resource && 79 | /\.js$/.test(module.resource) && 80 | module.resource.indexOf( 81 | path.join(__dirname, '../node_modules') 82 | ) === 0 83 | ) 84 | } 85 | }), 86 | // extract webpack runtime and module manifest to its own file in order to 87 | // prevent vendor hash from being updated whenever app bundle is updated 88 | new webpack.optimize.CommonsChunkPlugin({ 89 | name: 'manifest', 90 | chunks: ['vendor'] 91 | }), 92 | new webpack.LoaderOptionsPlugin({ 93 | minimize: true 94 | }), 95 | new OfflinePlugin({ 96 | ServiceWorker: { 97 | events: true, 98 | navigateFallbackURL: '/' 99 | }, 100 | AppCache: false, 101 | cacheMaps: [{ 102 | match: function (url) { 103 | if (url.pathname.indexOf('/cdn/') > -1) { 104 | return 105 | } 106 | 107 | return new URL('/', location) 108 | }, 109 | requestTypes: ['navigate'] 110 | }] 111 | }) 112 | ] 113 | }) 114 | 115 | if (config.build.productionGzip) { 116 | const CompressionWebpackPlugin = require('compression-webpack-plugin') 117 | 118 | webpackConfig.plugins.push( 119 | new CompressionWebpackPlugin({ 120 | asset: '[path].gz[query]', 121 | algorithm: 'gzip', 122 | test: new RegExp( 123 | '\\.(' + 124 | config.build.productionGzipExtensions.join('|') + 125 | ')$' 126 | ), 127 | threshold: 10240, 128 | minRatio: 0.8 129 | }) 130 | ) 131 | } 132 | 133 | if (config.build.bundleAnalyzerReport) { 134 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin 135 | webpackConfig.plugins.push(new BundleAnalyzerPlugin()) 136 | } 137 | 138 | module.exports = webpackConfig 139 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../dist/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../dist'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '/', 11 | productionSourceMap: false, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'], 18 | // Run the build command with an extra argument to 19 | // View the bundle analyzer report after build finishes: 20 | // `npm run build --report` 21 | // Set to `true` or `false` to always turn it on or off 22 | bundleAnalyzerReport: process.env.npm_config_report 23 | }, 24 | dev: { 25 | env: require('./dev.env'), 26 | port: 8080, 27 | autoOpenBrowser: true, 28 | assetsSubDirectory: 'static', 29 | assetsPublicPath: '/', 30 | proxyTable: { 31 | '/cdn': { 32 | target: 'http://img1.ak.crunchyroll.com', 33 | changeOrigin: true, 34 | pathRewrite: { 35 | '^/cdn': '' 36 | } 37 | }, 38 | '/pl-proxy': { 39 | target: 'https://pl.crunchyroll.com', 40 | changeOrigin: true, 41 | pathRewrite: { 42 | '^/pl-proxy': '' 43 | } 44 | } 45 | }, 46 | // CSS Sourcemaps off by default because relative paths are "buggy" 47 | // with this option, according to the CSS-Loader README 48 | // (https://github.com/webpack/css-loader#sourcemaps) 49 | // In our experience, they generally work as expected, 50 | // just be aware of this issue when enabling this option. 51 | cssSourceMap: false 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /extension/background.js: -------------------------------------------------------------------------------- 1 | /* global chrome */ 2 | 3 | chrome.webRequest.onHeadersReceived.addListener(({responseHeaders}) => { 4 | const corsHeader = responseHeaders.findIndex(({name}) => name.toLowerCase() === 'access-control-allow-origin') 5 | if (corsHeader > -1) { 6 | responseHeaders[corsHeader].value = '*' 7 | } 8 | 9 | return {responseHeaders} 10 | }, {urls: ['https://*.vrv.co/*', 'https://*.dlvr1.net/*']}, ['blocking', 'responseHeaders']) 11 | -------------------------------------------------------------------------------- /extension/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | 4 | "name": "Umi Enabler", 5 | "description": "Allows Umi player to work on all series", 6 | "version": "1.0.0", 7 | 8 | "permissions": [ 9 | "webRequest", 10 | "webRequestBlocking", 11 | "https://umi.party/*", 12 | "https://*.vrv.co/*", 13 | "https://*.dlvr1.net/*" 14 | ], 15 | 16 | "background": { 17 | "scripts": ["background.js"], 18 | "persistent": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /functions/index.js: -------------------------------------------------------------------------------- 1 | const functions = require('firebase-functions') 2 | const admin = require('firebase-admin') 3 | admin.initializeApp(functions.config().firebase) 4 | 5 | exports.cleanRooms = functions.database.ref('roomUsers/{roomId}') 6 | .onDelete((event) => { 7 | const root = event.data.adminRef.root 8 | return Promise.all([ 9 | root.child(`rooms/${event.params.roomId}`).remove(), 10 | root.child(`roomEmoji/${event.params.roomId}`).remove() 11 | ]) 12 | }) 13 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "description": "Cloud Functions for Firebase", 4 | "scripts": { 5 | "serve": "firebase serve --only functions", 6 | "shell": "firebase experimental:functions:shell", 7 | "start": "npm run shell", 8 | "deploy": "firebase deploy --only functions", 9 | "logs": "firebase functions:log" 10 | }, 11 | "dependencies": { 12 | "firebase-admin": "~5.4.2", 13 | "firebase-functions": "^0.7.1" 14 | }, 15 | "private": true 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Watch anime together - Umi 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "umi", 3 | "version": "1.0.0", 4 | "description": "Better Crunchyroll", 5 | "author": "Zach Bruggeman ", 6 | "private": true, 7 | "scripts": { 8 | "dev": "node build/dev-server.js", 9 | "build": "node build/build.js && cp _redirects dist/_redirects", 10 | "lint": "eslint --ext .js,.vue src" 11 | }, 12 | "dependencies": { 13 | "animejs": "^2.0.1", 14 | "axios": "^0.16.0", 15 | "base64-js": "^1.2.0", 16 | "clappr": "0.2.77", 17 | "clappr-thumbnails-plugin": "^3.6.0", 18 | "date-fns": "^1.27.2", 19 | "debounce": "^1.0.2", 20 | "firebase": "^4.6.2", 21 | "font-awesome": "^4.7.0", 22 | "tachyons": "^4.9.0", 23 | "uuid": "^3.0.1", 24 | "v-tooltip": "^1.1.6", 25 | "vue": "^2.2.4", 26 | "vue-analytics": "^4.1.3", 27 | "vue-clickaway": "^2.1.0", 28 | "vue-meta": "^1.0.4", 29 | "vue-router": "^2.3.0", 30 | "vuex": "^2.2.1", 31 | "vuex-router-sync": "^4.1.2" 32 | }, 33 | "devDependencies": { 34 | "autoprefixer": "^7.2.5", 35 | "babel-core": "^6.26.0", 36 | "babel-eslint": "^7.1.1", 37 | "babel-loader": "^7.0.0", 38 | "babel-polyfill": "^6.23.0", 39 | "babel-preset-env": "^1.6.1", 40 | "babel-register": "^6.22.0", 41 | "chalk": "^1.1.3", 42 | "connect-history-api-fallback": "^1.3.0", 43 | "css-loader": "^0.28.0", 44 | "eslint": "^3.14.1", 45 | "eslint-config-standard": "^10.0.0", 46 | "eslint-friendly-formatter": "^2.0.7", 47 | "eslint-loader": "^1.6.1", 48 | "eslint-plugin-import": "^2.2.0", 49 | "eslint-plugin-node": "^4.2.2", 50 | "eslint-plugin-promise": "^3.4.0", 51 | "eslint-plugin-standard": "^3.0.1", 52 | "eslint-plugin-vue": "^4.0.1", 53 | "eventsource-polyfill": "^0.9.6", 54 | "express": "^4.14.1", 55 | "extract-text-webpack-plugin": "^3.0.0", 56 | "file-loader": "^0.11.1", 57 | "friendly-errors-webpack-plugin": "^1.1.3", 58 | "function-bind": "^1.1.0", 59 | "html-webpack-plugin": "^2.28.0", 60 | "http-proxy-middleware": "^0.17.3", 61 | "node": "^8.9.4", 62 | "offline-plugin": "^4.6.2", 63 | "opn": "^5.0.0", 64 | "ora": "^1.1.0", 65 | "purify-css": "^1.2.5", 66 | "purifycss-webpack": "^0.7.0", 67 | "semver": "^5.3.0", 68 | "shelljs": "^0.7.6", 69 | "uglifyjs-webpack-plugin": "^1.1.2", 70 | "url-loader": "^0.5.7", 71 | "vue-loader": "^13.0.4", 72 | "vue-style-loader": "^3.0.1", 73 | "vue-template-compiler": "^2.2.4", 74 | "webpack": "^3.3.0", 75 | "webpack-bundle-analyzer": "^2.2.1", 76 | "webpack-dev-middleware": "^1.10.0", 77 | "webpack-hot-middleware": "^2.16.1", 78 | "webpack-merge": "^4.0.0", 79 | "workerize-loader": "^1.0.1" 80 | }, 81 | "engines": { 82 | "node": ">= 4.0.0", 83 | "npm": ">= 3.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const compression = require('compression') 3 | const cors = require('cors') 4 | 5 | const opening = require('./routes/opening') 6 | const mal = require('./routes/mal') 7 | 8 | // create app 9 | const app = express() 10 | const srv = require('http').Server(app) 11 | app.use(compression()) 12 | app.use(cors()) 13 | 14 | // setup routes 15 | app.get('/', (req, res) => res.send({status: 'ok'})) 16 | app.get('/opening', opening) 17 | app.use('/mal', mal) 18 | 19 | // listen 20 | srv.listen(3001, () => { 21 | console.log('listening on 3001') 22 | }) 23 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "server", 3 | "version": "1.0.0", 4 | "description": "umi watch together server", 5 | "main": "index.js", 6 | "author": "Zach Bruggeman", 7 | "license": "MIT", 8 | "dependencies": { 9 | "axios": "^0.16.2", 10 | "body-parser": "^1.17.1", 11 | "cheerio": "^0.22.0", 12 | "compression": "^1.6.2", 13 | "cors": "^2.8.3", 14 | "express": "^4.15.2", 15 | "iron": "^4.0.4", 16 | "popura": "^1.2.5" 17 | }, 18 | "scripts": { 19 | "start": "node index.js", 20 | "dev": "cross-env IRON_TOKEN=EXAMPLE_TOKEN_PLEASE_DONT_USE_IN_PRODUCTION nodemon index.js" 21 | }, 22 | "now": { 23 | "alias": "umi-watch-api", 24 | "env": { 25 | "IRON_TOKEN": "@iron-token", 26 | "NODE_ENV": "production" 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /server/routes/mal.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const axios = require('axios') 3 | const {json} = require('body-parser') 4 | const popura = require('popura') 5 | const Iron = require('iron') 6 | 7 | const {IRON_TOKEN} = process.env 8 | 9 | const router = express.Router() 10 | router.use(json()) 11 | 12 | router.post('/login', function malLogin (req, res) { 13 | const {username, password} = req.body 14 | if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'}) 15 | 16 | const client = popura(username, password) 17 | 18 | client.verifyAuth() 19 | .then((obj) => { 20 | Iron.seal(password, IRON_TOKEN, Iron.defaults, (err, sealed) => { 21 | if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'}) 22 | 23 | res.send({ 24 | status: 'ok', 25 | username: obj.username, 26 | password: sealed 27 | }) 28 | }) 29 | }) 30 | .catch((err) => { 31 | res.status(err.statusCode).send({ 32 | status: 'not ok', 33 | error: err.message 34 | }) 35 | }) 36 | }) 37 | 38 | router.get('/series', function malSeries (req, res) { 39 | const {name} = req.query 40 | if (!name) return res.status(400).send({status: 'not ok', error: 'Invalid payload'}) 41 | 42 | axios({ 43 | method: 'GET', 44 | url: 'https://myanimelist.net/search/prefix.json', 45 | params: { 46 | type: 'anime', 47 | keyword: name, 48 | v: 1 49 | } 50 | }) 51 | .then(({data}) => { 52 | const {items} = data.categories[0] 53 | 54 | let item = items.find((i) => i.name.toLowerCase() === name.toLowerCase()) || items[0] 55 | if (name === 'My Hero Academia Season 2') { 56 | item = items.find((i) => i.name === 'Boku no Hero Academia 2nd Season') || item 57 | } 58 | if (!item) return res.status(404).send({status: 'not ok', error: `Couldn't find anime for "${name}"`}) 59 | 60 | res.send({status: 'ok', item}) 61 | }) 62 | }) 63 | 64 | router.post('/update', function malUpdate (req, res) { 65 | const {username, password, id, episode, status} = req.body 66 | if (!username || !password) return res.status(400).send({status: 'not ok', error: 'Invalid login'}) 67 | if (!id || !episode || !status) return res.status(400).send({status: 'not ok', error: 'Invalid payload'}) 68 | 69 | Iron.unseal(password, IRON_TOKEN, Iron.defaults, (err, unsealed) => { 70 | if (err) return res.status(500).send({status: 'not ok', error: 'Internal error'}) 71 | 72 | const client = popura(username, unsealed) 73 | client.updateAnime(id, {episode, status}) 74 | .then(() => { 75 | res.send({status: 'ok'}) 76 | }) 77 | .catch((err) => { 78 | console.error(err) 79 | res.status(500).send({status: 'not ok', error: 'Internal error'}) 80 | }) 81 | }) 82 | }) 83 | 84 | module.exports = router 85 | -------------------------------------------------------------------------------- /server/routes/opening.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios') 2 | const cheerio = require('cheerio') 3 | const cache = {} 4 | 5 | async function openingHandler (req, res) { 6 | const {search} = req.query 7 | if (!search) return res.send({result: null}) 8 | if (cache[search]) { 9 | const openings = cache[search] 10 | const result = openings[Math.floor(Math.random() * openings.length)].url 11 | return res.send({result}) 12 | } 13 | 14 | try { 15 | const {data: results} = await axios.get(`https://openings.ninja/core/getAnime.php?term=${search}`, {timeout: 3000}) 16 | if (!results || results.length === 0) return res.send({result: null}) 17 | 18 | const {data: html} = await axios.get(`https://openings.ninja/${results[0]}/op/1`) 19 | const $ = cheerio.load(html) 20 | const openings = [] 21 | $('#mirror > li[data-theme^="op"]').toArray() 22 | .forEach((el) => { 23 | const index = openings.findIndex((op) => op.theme === el.attribs['data-theme']) 24 | if (index < 0) { 25 | openings.push({theme: el.attribs['data-theme'], url: el.attribs['data-url']}) 26 | } 27 | }) 28 | 29 | cache[search] = openings 30 | const result = openings[Math.floor(Math.random() * openings.length)].url 31 | res.send({result}) 32 | } catch (err) { 33 | console.error(err.message) 34 | res.send({result: null}) 35 | } 36 | } 37 | 38 | module.exports = openingHandler 39 | -------------------------------------------------------------------------------- /server/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | accepts@~1.3.3: 6 | version "1.3.3" 7 | resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" 8 | dependencies: 9 | mime-types "~2.1.11" 10 | negotiator "0.6.1" 11 | 12 | array-flatten@1.1.1: 13 | version "1.1.1" 14 | resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" 15 | 16 | axios@^0.16.2: 17 | version "0.16.2" 18 | resolved "https://registry.yarnpkg.com/axios/-/axios-0.16.2.tgz#ba4f92f17167dfbab40983785454b9ac149c3c6d" 19 | dependencies: 20 | follow-redirects "^1.2.3" 21 | is-buffer "^1.1.5" 22 | 23 | body-parser@^1.17.1: 24 | version "1.17.2" 25 | resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee" 26 | dependencies: 27 | bytes "2.4.0" 28 | content-type "~1.0.2" 29 | debug "2.6.7" 30 | depd "~1.1.0" 31 | http-errors "~1.6.1" 32 | iconv-lite "0.4.15" 33 | on-finished "~2.3.0" 34 | qs "6.4.0" 35 | raw-body "~2.2.0" 36 | type-is "~1.6.15" 37 | 38 | boolbase@~1.0.0: 39 | version "1.0.0" 40 | resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" 41 | 42 | boom@5.x.x: 43 | version "5.1.0" 44 | resolved "https://registry.yarnpkg.com/boom/-/boom-5.1.0.tgz#0308fa8e924cd6d42d9c3bf4883bdc98f0e71df8" 45 | dependencies: 46 | hoek "4.x.x" 47 | 48 | bytes@2.3.0: 49 | version "2.3.0" 50 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.3.0.tgz#d5b680a165b6201739acb611542aabc2d8ceb070" 51 | 52 | bytes@2.4.0: 53 | version "2.4.0" 54 | resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339" 55 | 56 | capture-stack-trace@^1.0.0: 57 | version "1.0.0" 58 | resolved "https://registry.yarnpkg.com/capture-stack-trace/-/capture-stack-trace-1.0.0.tgz#4a6fa07399c26bba47f0b2496b4d0fb408c5550d" 59 | 60 | cheerio@^0.22.0: 61 | version "0.22.0" 62 | resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-0.22.0.tgz#a9baa860a3f9b595a6b81b1a86873121ed3a269e" 63 | dependencies: 64 | css-select "~1.2.0" 65 | dom-serializer "~0.1.0" 66 | entities "~1.1.1" 67 | htmlparser2 "^3.9.1" 68 | lodash.assignin "^4.0.9" 69 | lodash.bind "^4.1.4" 70 | lodash.defaults "^4.0.1" 71 | lodash.filter "^4.4.0" 72 | lodash.flatten "^4.2.0" 73 | lodash.foreach "^4.3.0" 74 | lodash.map "^4.4.0" 75 | lodash.merge "^4.4.0" 76 | lodash.pick "^4.2.1" 77 | lodash.reduce "^4.4.0" 78 | lodash.reject "^4.4.0" 79 | lodash.some "^4.4.0" 80 | 81 | compressible@~2.0.8: 82 | version "2.0.10" 83 | resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.10.tgz#feda1c7f7617912732b29bf8cf26252a20b9eecd" 84 | dependencies: 85 | mime-db ">= 1.27.0 < 2" 86 | 87 | compression@^1.6.2: 88 | version "1.6.2" 89 | resolved "https://registry.yarnpkg.com/compression/-/compression-1.6.2.tgz#cceb121ecc9d09c52d7ad0c3350ea93ddd402bc3" 90 | dependencies: 91 | accepts "~1.3.3" 92 | bytes "2.3.0" 93 | compressible "~2.0.8" 94 | debug "~2.2.0" 95 | on-headers "~1.0.1" 96 | vary "~1.1.0" 97 | 98 | content-disposition@0.5.2: 99 | version "0.5.2" 100 | resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" 101 | 102 | content-type@~1.0.2: 103 | version "1.0.2" 104 | resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.2.tgz#b7d113aee7a8dd27bd21133c4dc2529df1721eed" 105 | 106 | cookie-signature@1.0.6: 107 | version "1.0.6" 108 | resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" 109 | 110 | cookie@0.3.1: 111 | version "0.3.1" 112 | resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" 113 | 114 | core-util-is@~1.0.0: 115 | version "1.0.2" 116 | resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" 117 | 118 | cors@^2.8.3: 119 | version "2.8.3" 120 | resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.3.tgz#4cf78e1d23329a7496b2fc2225b77ca5bb5eb802" 121 | dependencies: 122 | object-assign "^4" 123 | vary "^1" 124 | 125 | create-error-class@^3.0.0: 126 | version "3.0.2" 127 | resolved "https://registry.yarnpkg.com/create-error-class/-/create-error-class-3.0.2.tgz#06be7abef947a3f14a30fd610671d401bca8b7b6" 128 | dependencies: 129 | capture-stack-trace "^1.0.0" 130 | 131 | cryptiles@3.x.x: 132 | version "3.1.2" 133 | resolved "https://registry.yarnpkg.com/cryptiles/-/cryptiles-3.1.2.tgz#a89fbb220f5ce25ec56e8c4aa8a4fd7b5b0d29fe" 134 | dependencies: 135 | boom "5.x.x" 136 | 137 | css-select@~1.2.0: 138 | version "1.2.0" 139 | resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" 140 | dependencies: 141 | boolbase "~1.0.0" 142 | css-what "2.1" 143 | domutils "1.5.1" 144 | nth-check "~1.0.1" 145 | 146 | css-what@2.1: 147 | version "2.1.0" 148 | resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.0.tgz#9467d032c38cfaefb9f2d79501253062f87fa1bd" 149 | 150 | debug@2.6.7, debug@^2.2.0, debug@^2.4.5: 151 | version "2.6.7" 152 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.7.tgz#92bad1f6d05bbb6bba22cca88bcd0ec894c2861e" 153 | dependencies: 154 | ms "2.0.0" 155 | 156 | debug@~2.2.0: 157 | version "2.2.0" 158 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" 159 | dependencies: 160 | ms "0.7.1" 161 | 162 | depd@1.1.0, depd@~1.1.0: 163 | version "1.1.0" 164 | resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.0.tgz#e1bd82c6aab6ced965b97b88b17ed3e528ca18c3" 165 | 166 | destroy@~1.0.4: 167 | version "1.0.4" 168 | resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" 169 | 170 | dom-serializer@0, dom-serializer@~0.1.0: 171 | version "0.1.0" 172 | resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" 173 | dependencies: 174 | domelementtype "~1.1.1" 175 | entities "~1.1.1" 176 | 177 | domelementtype@1, domelementtype@^1.3.0: 178 | version "1.3.0" 179 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.0.tgz#b17aed82e8ab59e52dd9c19b1756e0fc187204c2" 180 | 181 | domelementtype@~1.1.1: 182 | version "1.1.3" 183 | resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.1.3.tgz#bd28773e2642881aec51544924299c5cd822185b" 184 | 185 | domhandler@^2.3.0: 186 | version "2.4.1" 187 | resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.1.tgz#892e47000a99be55bbf3774ffea0561d8879c259" 188 | dependencies: 189 | domelementtype "1" 190 | 191 | domutils@1.5.1, domutils@^1.5.1: 192 | version "1.5.1" 193 | resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" 194 | dependencies: 195 | dom-serializer "0" 196 | domelementtype "1" 197 | 198 | duplexer3@^0.1.4: 199 | version "0.1.4" 200 | resolved "https://registry.yarnpkg.com/duplexer3/-/duplexer3-0.1.4.tgz#ee01dd1cac0ed3cbc7fdbea37dc0a8f1ce002ce2" 201 | 202 | ee-first@1.1.1: 203 | version "1.1.1" 204 | resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" 205 | 206 | encodeurl@~1.0.1: 207 | version "1.0.1" 208 | resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" 209 | 210 | entities@^1.1.1, entities@~1.1.1: 211 | version "1.1.1" 212 | resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.1.tgz#6e5c2d0a5621b5dadaecef80b90edfb5cd7772f0" 213 | 214 | escape-html@~1.0.3: 215 | version "1.0.3" 216 | resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" 217 | 218 | etag@~1.8.0: 219 | version "1.8.0" 220 | resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.0.tgz#6f631aef336d6c46362b51764044ce216be3c051" 221 | 222 | express@^4.15.2: 223 | version "4.15.3" 224 | resolved "https://registry.yarnpkg.com/express/-/express-4.15.3.tgz#bab65d0f03aa80c358408972fc700f916944b662" 225 | dependencies: 226 | accepts "~1.3.3" 227 | array-flatten "1.1.1" 228 | content-disposition "0.5.2" 229 | content-type "~1.0.2" 230 | cookie "0.3.1" 231 | cookie-signature "1.0.6" 232 | debug "2.6.7" 233 | depd "~1.1.0" 234 | encodeurl "~1.0.1" 235 | escape-html "~1.0.3" 236 | etag "~1.8.0" 237 | finalhandler "~1.0.3" 238 | fresh "0.5.0" 239 | merge-descriptors "1.0.1" 240 | methods "~1.1.2" 241 | on-finished "~2.3.0" 242 | parseurl "~1.3.1" 243 | path-to-regexp "0.1.7" 244 | proxy-addr "~1.1.4" 245 | qs "6.4.0" 246 | range-parser "~1.2.0" 247 | send "0.15.3" 248 | serve-static "1.12.3" 249 | setprototypeof "1.0.3" 250 | statuses "~1.3.1" 251 | type-is "~1.6.15" 252 | utils-merge "1.0.0" 253 | vary "~1.1.1" 254 | 255 | finalhandler@~1.0.3: 256 | version "1.0.3" 257 | resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.0.3.tgz#ef47e77950e999780e86022a560e3217e0d0cc89" 258 | dependencies: 259 | debug "2.6.7" 260 | encodeurl "~1.0.1" 261 | escape-html "~1.0.3" 262 | on-finished "~2.3.0" 263 | parseurl "~1.3.1" 264 | statuses "~1.3.1" 265 | unpipe "~1.0.0" 266 | 267 | follow-redirects@^1.2.3: 268 | version "1.2.4" 269 | resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.4.tgz#355e8f4d16876b43f577b0d5ce2668b9723214ea" 270 | dependencies: 271 | debug "^2.4.5" 272 | 273 | forwarded@~0.1.0: 274 | version "0.1.0" 275 | resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.0.tgz#19ef9874c4ae1c297bcf078fde63a09b66a84363" 276 | 277 | fresh@0.5.0: 278 | version "0.5.0" 279 | resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.0.tgz#f474ca5e6a9246d6fd8e0953cfa9b9c805afa78e" 280 | 281 | get-stream@^3.0.0: 282 | version "3.0.0" 283 | resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" 284 | 285 | got@^6.3.0: 286 | version "6.7.1" 287 | resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0" 288 | dependencies: 289 | create-error-class "^3.0.0" 290 | duplexer3 "^0.1.4" 291 | get-stream "^3.0.0" 292 | is-redirect "^1.0.0" 293 | is-retry-allowed "^1.0.0" 294 | is-stream "^1.0.0" 295 | lowercase-keys "^1.0.0" 296 | safe-buffer "^5.0.1" 297 | timed-out "^4.0.0" 298 | unzip-response "^2.0.1" 299 | url-parse-lax "^1.0.0" 300 | 301 | hoek@4.x.x: 302 | version "4.1.1" 303 | resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.1.1.tgz#9cc573ffba2b7b408fb5e9c2a13796be94cddce9" 304 | 305 | htmlparser2@^3.9.1: 306 | version "3.9.2" 307 | resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338" 308 | dependencies: 309 | domelementtype "^1.3.0" 310 | domhandler "^2.3.0" 311 | domutils "^1.5.1" 312 | entities "^1.1.1" 313 | inherits "^2.0.1" 314 | readable-stream "^2.0.2" 315 | 316 | http-errors@~1.6.1: 317 | version "1.6.1" 318 | resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.1.tgz#5f8b8ed98aca545656bf572997387f904a722257" 319 | dependencies: 320 | depd "1.1.0" 321 | inherits "2.0.3" 322 | setprototypeof "1.0.3" 323 | statuses ">= 1.3.1 < 2" 324 | 325 | iconv-lite@0.4.15: 326 | version "0.4.15" 327 | resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" 328 | 329 | inherits@2.0.3, inherits@^2.0.1, inherits@~2.0.3: 330 | version "2.0.3" 331 | resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" 332 | 333 | ipaddr.js@1.3.0: 334 | version "1.3.0" 335 | resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.3.0.tgz#1e03a52fdad83a8bbb2b25cbf4998b4cffcd3dec" 336 | 337 | iron@^4.0.4: 338 | version "4.0.5" 339 | resolved "https://registry.yarnpkg.com/iron/-/iron-4.0.5.tgz#4f042cceb8b9738f346b59aa734c83a89bc31428" 340 | dependencies: 341 | boom "5.x.x" 342 | cryptiles "3.x.x" 343 | hoek "4.x.x" 344 | 345 | is-buffer@^1.1.5: 346 | version "1.1.5" 347 | resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.5.tgz#1f3b26ef613b214b88cbca23cc6c01d87961eecc" 348 | 349 | is-redirect@^1.0.0: 350 | version "1.0.0" 351 | resolved "https://registry.yarnpkg.com/is-redirect/-/is-redirect-1.0.0.tgz#1d03dded53bd8db0f30c26e4f95d36fc7c87dc24" 352 | 353 | is-retry-allowed@^1.0.0: 354 | version "1.1.0" 355 | resolved "https://registry.yarnpkg.com/is-retry-allowed/-/is-retry-allowed-1.1.0.tgz#11a060568b67339444033d0125a61a20d564fb34" 356 | 357 | is-stream@^1.0.0: 358 | version "1.1.0" 359 | resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" 360 | 361 | isarray@~1.0.0: 362 | version "1.0.0" 363 | resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" 364 | 365 | lodash.assignin@^4.0.9: 366 | version "4.2.0" 367 | resolved "https://registry.yarnpkg.com/lodash.assignin/-/lodash.assignin-4.2.0.tgz#ba8df5fb841eb0a3e8044232b0e263a8dc6a28a2" 368 | 369 | lodash.bind@^4.1.4: 370 | version "4.2.1" 371 | resolved "https://registry.yarnpkg.com/lodash.bind/-/lodash.bind-4.2.1.tgz#7ae3017e939622ac31b7d7d7dcb1b34db1690d35" 372 | 373 | lodash.defaults@^4.0.1: 374 | version "4.2.0" 375 | resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" 376 | 377 | lodash.filter@^4.4.0: 378 | version "4.6.0" 379 | resolved "https://registry.yarnpkg.com/lodash.filter/-/lodash.filter-4.6.0.tgz#668b1d4981603ae1cc5a6fa760143e480b4c4ace" 380 | 381 | lodash.flatten@^4.2.0: 382 | version "4.4.0" 383 | resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" 384 | 385 | lodash.foreach@^4.3.0: 386 | version "4.5.0" 387 | resolved "https://registry.yarnpkg.com/lodash.foreach/-/lodash.foreach-4.5.0.tgz#1a6a35eace401280c7f06dddec35165ab27e3e53" 388 | 389 | lodash.map@^4.4.0: 390 | version "4.6.0" 391 | resolved "https://registry.yarnpkg.com/lodash.map/-/lodash.map-4.6.0.tgz#771ec7839e3473d9c4cde28b19394c3562f4f6d3" 392 | 393 | lodash.merge@^4.4.0: 394 | version "4.6.0" 395 | resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.0.tgz#69884ba144ac33fe699737a6086deffadd0f89c5" 396 | 397 | lodash.pick@^4.2.1: 398 | version "4.4.0" 399 | resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" 400 | 401 | lodash.reduce@^4.4.0: 402 | version "4.6.0" 403 | resolved "https://registry.yarnpkg.com/lodash.reduce/-/lodash.reduce-4.6.0.tgz#f1ab6b839299ad48f784abbf476596f03b914d3b" 404 | 405 | lodash.reject@^4.4.0: 406 | version "4.6.0" 407 | resolved "https://registry.yarnpkg.com/lodash.reject/-/lodash.reject-4.6.0.tgz#80d6492dc1470864bbf583533b651f42a9f52415" 408 | 409 | lodash.some@^4.4.0: 410 | version "4.6.0" 411 | resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" 412 | 413 | lodash@^4.0.0: 414 | version "4.17.4" 415 | resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" 416 | 417 | lowercase-keys@^1.0.0: 418 | version "1.0.0" 419 | resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" 420 | 421 | media-typer@0.3.0: 422 | version "0.3.0" 423 | resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" 424 | 425 | merge-descriptors@1.0.1: 426 | version "1.0.1" 427 | resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" 428 | 429 | methods@~1.1.2: 430 | version "1.1.2" 431 | resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" 432 | 433 | "mime-db@>= 1.27.0 < 2": 434 | version "1.28.0" 435 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.28.0.tgz#fedd349be06d2865b7fc57d837c6de4f17d7ac3c" 436 | 437 | mime-db@~1.27.0: 438 | version "1.27.0" 439 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" 440 | 441 | mime-types@~2.1.11, mime-types@~2.1.15: 442 | version "2.1.15" 443 | resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" 444 | dependencies: 445 | mime-db "~1.27.0" 446 | 447 | mime@1.3.4: 448 | version "1.3.4" 449 | resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" 450 | 451 | ms@0.7.1: 452 | version "0.7.1" 453 | resolved "https://registry.yarnpkg.com/ms/-/ms-0.7.1.tgz#9cd13c03adbff25b65effde7ce864ee952017098" 454 | 455 | ms@2.0.0: 456 | version "2.0.0" 457 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 458 | 459 | negotiator@0.6.1: 460 | version "0.6.1" 461 | resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" 462 | 463 | nth-check@~1.0.1: 464 | version "1.0.1" 465 | resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.1.tgz#9929acdf628fc2c41098deab82ac580cf149aae4" 466 | dependencies: 467 | boolbase "~1.0.0" 468 | 469 | object-assign@^4: 470 | version "4.1.1" 471 | resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" 472 | 473 | on-finished@~2.3.0: 474 | version "2.3.0" 475 | resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" 476 | dependencies: 477 | ee-first "1.1.1" 478 | 479 | on-headers@~1.0.1: 480 | version "1.0.1" 481 | resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" 482 | 483 | parseurl@~1.3.1: 484 | version "1.3.1" 485 | resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.1.tgz#c8ab8c9223ba34888aa64a297b28853bec18da56" 486 | 487 | path-to-regexp@0.1.7: 488 | version "0.1.7" 489 | resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" 490 | 491 | popura@^1.2.5: 492 | version "1.2.5" 493 | resolved "https://registry.yarnpkg.com/popura/-/popura-1.2.5.tgz#a92cd25fb940c2d6a5854029b825798a95c1901a" 494 | dependencies: 495 | debug "^2.2.0" 496 | got "^6.3.0" 497 | xml2js "^0.4.17" 498 | 499 | prepend-http@^1.0.1: 500 | version "1.0.4" 501 | resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" 502 | 503 | process-nextick-args@~1.0.6: 504 | version "1.0.7" 505 | resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" 506 | 507 | proxy-addr@~1.1.4: 508 | version "1.1.4" 509 | resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-1.1.4.tgz#27e545f6960a44a627d9b44467e35c1b6b4ce2f3" 510 | dependencies: 511 | forwarded "~0.1.0" 512 | ipaddr.js "1.3.0" 513 | 514 | qs@6.4.0: 515 | version "6.4.0" 516 | resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233" 517 | 518 | range-parser@~1.2.0: 519 | version "1.2.0" 520 | resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" 521 | 522 | raw-body@~2.2.0: 523 | version "2.2.0" 524 | resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96" 525 | dependencies: 526 | bytes "2.4.0" 527 | iconv-lite "0.4.15" 528 | unpipe "1.0.0" 529 | 530 | readable-stream@^2.0.2: 531 | version "2.3.2" 532 | resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.2.tgz#5a04df05e4f57fe3f0dc68fdd11dc5c97c7e6f4d" 533 | dependencies: 534 | core-util-is "~1.0.0" 535 | inherits "~2.0.3" 536 | isarray "~1.0.0" 537 | process-nextick-args "~1.0.6" 538 | safe-buffer "~5.1.0" 539 | string_decoder "~1.0.0" 540 | util-deprecate "~1.0.1" 541 | 542 | safe-buffer@^5.0.1, safe-buffer@~5.1.0: 543 | version "5.1.1" 544 | resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" 545 | 546 | sax@>=0.6.0: 547 | version "1.2.4" 548 | resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" 549 | 550 | send@0.15.3: 551 | version "0.15.3" 552 | resolved "https://registry.yarnpkg.com/send/-/send-0.15.3.tgz#5013f9f99023df50d1bd9892c19e3defd1d53309" 553 | dependencies: 554 | debug "2.6.7" 555 | depd "~1.1.0" 556 | destroy "~1.0.4" 557 | encodeurl "~1.0.1" 558 | escape-html "~1.0.3" 559 | etag "~1.8.0" 560 | fresh "0.5.0" 561 | http-errors "~1.6.1" 562 | mime "1.3.4" 563 | ms "2.0.0" 564 | on-finished "~2.3.0" 565 | range-parser "~1.2.0" 566 | statuses "~1.3.1" 567 | 568 | serve-static@1.12.3: 569 | version "1.12.3" 570 | resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.12.3.tgz#9f4ba19e2f3030c547f8af99107838ec38d5b1e2" 571 | dependencies: 572 | encodeurl "~1.0.1" 573 | escape-html "~1.0.3" 574 | parseurl "~1.3.1" 575 | send "0.15.3" 576 | 577 | setprototypeof@1.0.3: 578 | version "1.0.3" 579 | resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" 580 | 581 | "statuses@>= 1.3.1 < 2", statuses@~1.3.1: 582 | version "1.3.1" 583 | resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" 584 | 585 | string_decoder@~1.0.0: 586 | version "1.0.3" 587 | resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.0.3.tgz#0fc67d7c141825de94282dd536bec6b9bce860ab" 588 | dependencies: 589 | safe-buffer "~5.1.0" 590 | 591 | timed-out@^4.0.0: 592 | version "4.0.1" 593 | resolved "https://registry.yarnpkg.com/timed-out/-/timed-out-4.0.1.tgz#f32eacac5a175bea25d7fab565ab3ed8741ef56f" 594 | 595 | type-is@~1.6.15: 596 | version "1.6.15" 597 | resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" 598 | dependencies: 599 | media-typer "0.3.0" 600 | mime-types "~2.1.15" 601 | 602 | unpipe@1.0.0, unpipe@~1.0.0: 603 | version "1.0.0" 604 | resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" 605 | 606 | unzip-response@^2.0.1: 607 | version "2.0.1" 608 | resolved "https://registry.yarnpkg.com/unzip-response/-/unzip-response-2.0.1.tgz#d2f0f737d16b0615e72a6935ed04214572d56f97" 609 | 610 | url-parse-lax@^1.0.0: 611 | version "1.0.0" 612 | resolved "https://registry.yarnpkg.com/url-parse-lax/-/url-parse-lax-1.0.0.tgz#7af8f303645e9bd79a272e7a14ac68bc0609da73" 613 | dependencies: 614 | prepend-http "^1.0.1" 615 | 616 | util-deprecate@~1.0.1: 617 | version "1.0.2" 618 | resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" 619 | 620 | utils-merge@1.0.0: 621 | version "1.0.0" 622 | resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.0.tgz#0294fb922bb9375153541c4f7096231f287c8af8" 623 | 624 | vary@^1, vary@~1.1.0, vary@~1.1.1: 625 | version "1.1.1" 626 | resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.1.tgz#67535ebb694c1d52257457984665323f587e8d37" 627 | 628 | xml2js@^0.4.17: 629 | version "0.4.17" 630 | resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.17.tgz#17be93eaae3f3b779359c795b419705a8817e868" 631 | dependencies: 632 | sax ">=0.6.0" 633 | xmlbuilder "^4.1.0" 634 | 635 | xmlbuilder@^4.1.0: 636 | version "4.2.1" 637 | resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-4.2.1.tgz#aa58a3041a066f90eaa16c2f5389ff19f3f461a5" 638 | dependencies: 639 | lodash "^4.0.0" 640 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | @import '~tachyons'; 2 | @import '~font-awesome/css/font-awesome.css'; 3 | 4 | @keyframes loading-shimmer { 5 | 0% { 6 | background-position: -992px 0 7 | } 8 | 100% { 9 | background-position: 992px 0 10 | } 11 | } 12 | 13 | .sans-serif { 14 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; 15 | } 16 | 17 | .transparent { 18 | color: transparent; 19 | } 20 | 21 | .box-shadow-umi { 22 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); 23 | } 24 | 25 | .emoji { 26 | height: 28px; 27 | user-select: none; 28 | } 29 | 30 | .tooltip { 31 | display: none; 32 | opacity: 0; 33 | transition: opacity .15s; 34 | pointer-events: none; 35 | padding: 4px; 36 | z-index: 2147483647; 37 | } 38 | 39 | .tooltip .tooltip-content { 40 | background: rgba(0, 0, 0, 0.6); 41 | color: white; 42 | padding: 5px 10px 4px; 43 | } 44 | 45 | .tooltip.tooltip-open-transitionend { 46 | display: block; 47 | } 48 | 49 | .tooltip.tooltip-after-open { 50 | opacity: 1; 51 | } 52 | 53 | .mal-icon { 54 | display: inline-block; 55 | width: 24px; 56 | padding: 2px; 57 | box-sizing: border-box; 58 | color: #004175; 59 | border: 0.125rem solid #004175; 60 | border-radius: 0.25rem; 61 | font-size: 12px; 62 | font-weight: bold; 63 | } 64 | 65 | .mal-icon:after { 66 | content: 'AL'; 67 | } 68 | 69 | .mal-icon.watched { 70 | color: #19a974; 71 | border-color: #19a974; 72 | width: 44px; 73 | } 74 | 75 | .mal-icon.watched:after { 76 | content: 'AL ✔'; 77 | } 78 | 79 | .player-top-offset { 80 | top: 64px; 81 | } 82 | 83 | .player-width { 84 | width: 1024px; 85 | } 86 | 87 | .player-height { 88 | height: 576px; 89 | } 90 | 91 | .container-width { 92 | width: 948px; 93 | } 94 | 95 | 96 | .pointer-events-none { 97 | pointer-events: none; 98 | } 99 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 149 | 150 | 151 | 152 | 187 | -------------------------------------------------------------------------------- /src/assets/emoji/FrostyFridays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/FrostyFridays.png -------------------------------------------------------------------------------- /src/assets/emoji/TsuyuW.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/TsuyuW.png -------------------------------------------------------------------------------- /src/assets/emoji/UmiDorito.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/UmiDorito.png -------------------------------------------------------------------------------- /src/assets/emoji/Yousoro.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/emoji/Yousoro.png -------------------------------------------------------------------------------- /src/assets/umi-login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/umi-login.png -------------------------------------------------------------------------------- /src/assets/umi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/src/assets/umi.png -------------------------------------------------------------------------------- /src/components/Collection.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 53 | -------------------------------------------------------------------------------- /src/components/DashboardLoadingSection.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 18 | -------------------------------------------------------------------------------- /src/components/EpisodeScroller.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /src/components/Header.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 184 | 185 | 371 | -------------------------------------------------------------------------------- /src/components/LoadingMediaItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 58 | -------------------------------------------------------------------------------- /src/components/MediaItem.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 51 | 52 | 144 | -------------------------------------------------------------------------------- /src/components/QueueButton.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 25 | -------------------------------------------------------------------------------- /src/components/QueueItem.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 54 | 55 | 72 | -------------------------------------------------------------------------------- /src/components/Reactotron.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | -------------------------------------------------------------------------------- /src/components/Search.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 148 | 149 | 192 | -------------------------------------------------------------------------------- /src/components/SeriesItem.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /src/components/Video.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 373 | 374 | 404 | 405 | 427 | -------------------------------------------------------------------------------- /src/lib/api.js: -------------------------------------------------------------------------------- 1 | import axios, {CancelToken} from 'axios' 2 | 3 | export const ACCESS_TOKEN = 'LNDJgOit5yaRIWN' 4 | export const DEVICE_TYPE = 'com.crunchyroll.windows.desktop' 5 | export const LOCALE = () => localStorage.getItem('locale') || 'enUS' 6 | export const VERSION = '1.1.20.0' 7 | export const CONNECTIVITY_TYPE = 'ethernet' 8 | export const UMI_SERVER = process.env.NODE_ENV === 'production' ? 'https://umi-watch-api.now.sh' : 'http://localhost:3001' 9 | 10 | let source = CancelToken.source() 11 | 12 | export default function api (opts) { 13 | const config = { 14 | method: opts.method || 'get', 15 | url: `https://api.crunchyroll.com/${opts.route}.${opts.version || '0'}.json`, 16 | params: !opts.data ? Object.assign({}, opts.params, { 17 | locale: LOCALE(), 18 | version: VERSION, 19 | connectivity_type: CONNECTIVITY_TYPE 20 | }) : null, 21 | data: opts.data, 22 | cancelToken: !opts.noCancel ? source.token : null 23 | } 24 | 25 | return axios(config) 26 | } 27 | 28 | export function cancelCurrentRequests () { 29 | source.cancel('User changed page') 30 | source = CancelToken.source() 31 | } 32 | -------------------------------------------------------------------------------- /src/lib/auth.js: -------------------------------------------------------------------------------- 1 | import uuid from 'uuid/v4' 2 | import store from '../store' 3 | 4 | export function authGuard (to, from, next) { 5 | const loggedIn = !!store.state.auth.username 6 | if (loggedIn) { 7 | next() 8 | } else { 9 | next(`/login?next=${encodeURIComponent(to.fullPath)}`) 10 | } 11 | } 12 | 13 | export function loginGuard (to, from, next) { 14 | const noAuth = !store.state.auth.username 15 | if (noAuth) { 16 | next() 17 | } else { 18 | next('/') 19 | } 20 | } 21 | 22 | let localId = localStorage.getItem('umi-uuid') 23 | export function getUuid () { 24 | if (!localId) { 25 | localId = uuid().toUpperCase() 26 | localStorage.setItem('umi-uuid', localId) 27 | } 28 | 29 | return localId 30 | } 31 | -------------------------------------------------------------------------------- /src/lib/bif.js: -------------------------------------------------------------------------------- 1 | // Based on https://github.com/chemoish/videojs-bif/blob/c6fdc0c2cfc9446927062995b7e8830ae45fff0d/src/parser.js 2 | import { fromByteArray } from 'base64-js' 3 | 4 | const BIF_INDEX_OFFSET = 64 5 | const FRAMEWISE_SEPARATION_OFFSET = 16 6 | const NUMBER_OF_BIF_IMAGES_OFFSET = 12 7 | const BIF_INDEX_ENTRY_LENGTH = 8 8 | const MAGIC_NUMBER = new Uint8Array([ 9 | '0x89', 10 | '0x42', 11 | '0x49', 12 | '0x46', 13 | '0x0d', 14 | '0x0a', 15 | '0x1a', 16 | '0x0a' 17 | ]) 18 | 19 | function validate (magicNumber) { 20 | return MAGIC_NUMBER.every((byte, i) => magicNumber[i] === byte) 21 | } 22 | 23 | export async function parse (url) { 24 | const res = await fetch(url.replace('https://img1.ak.crunchyroll.com/', '/cdn/')) 25 | const buf = await res.arrayBuffer() 26 | 27 | const magicNumber = new Uint8Array(buf).slice(0, 8) 28 | if (!validate(magicNumber)) { 29 | return [] 30 | } 31 | 32 | const data = new DataView(buf) 33 | const framewiseSeparation = data.getUint32(FRAMEWISE_SEPARATION_OFFSET, true) || 1000 34 | const numberOfBIFImages = data.getUint32(NUMBER_OF_BIF_IMAGES_OFFSET, true) 35 | 36 | const bifData = [] 37 | for (let i = 0, bifIndexEntryOffset = BIF_INDEX_OFFSET; i < numberOfBIFImages; i += 1, bifIndexEntryOffset += BIF_INDEX_ENTRY_LENGTH) { 38 | const bifIndexEntryTimestampOffset = bifIndexEntryOffset 39 | const bifIndexEntryAbsoluteOffset = bifIndexEntryOffset + 4 40 | const nextBifIndexEntryAbsoluteOffset = bifIndexEntryAbsoluteOffset + BIF_INDEX_ENTRY_LENGTH 41 | 42 | const offset = data.getUint32(bifIndexEntryAbsoluteOffset, true) 43 | const nextOffset = data.getUint32(nextBifIndexEntryAbsoluteOffset, true) 44 | const length = nextOffset - offset 45 | 46 | bifData.push({ 47 | time: ((data.getUint32(bifIndexEntryTimestampOffset, true) * framewiseSeparation) / 1000) - 15, 48 | url: `data:image/jpeg;base64,${fromByteArray(new Uint8Array(buf.slice(offset, offset + length)))}` 49 | }) 50 | } 51 | 52 | return bifData 53 | } 54 | -------------------------------------------------------------------------------- /src/lib/cdnRewrite.js: -------------------------------------------------------------------------------- 1 | const CRUNCHYROLL_CDN = 'http://img1.ak.crunchyroll.com/' 2 | 3 | export default function cdnRewrite (url) { 4 | if (typeof url !== 'string') return url 5 | 6 | if (url.indexOf(CRUNCHYROLL_CDN) > -1) { 7 | return url.replace(CRUNCHYROLL_CDN, '/cdn/') 8 | } else { 9 | return url 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/clappr-level-selector.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("Clappr")):"function"==typeof define&&define.amd?define(["Clappr"],t):"object"==typeof exports?exports.LevelSelector=t(require("Clappr")):e.LevelSelector=t(e.Clappr)}(this,function(e){return function(e){function t(n){if(l[n])return l[n].exports;var o=l[n]={exports:{},id:n,loaded:!1};return e[n].call(o.exports,o,o.exports,t),o.loaded=!0,o.exports}var l={};return t.m=e,t.c=l,t.p="<%=baseUrl%>/",t(0)}([/*!******************!*\ 3 | !*** ./index.js ***! 4 | \******************/ 5 | function(e,t,l){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t["default"]=l(/*! ./src/main.js */1),e.exports=t["default"]},/*!*********************!*\ 6 | !*** ./src/main.js ***! 7 | \*********************/ 8 | function(e,t,l){"use strict";function n(e){return e&&e.__esModule?e:{"default":e}}function o(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function r(e,t){if("function"!=typeof t&&null!==t)throw new TypeError("Super expression must either be null or a function, not "+typeof t);e.prototype=Object.create(t&&t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}),t&&(Object.setPrototypeOf?Object.setPrototypeOf(e,t):e.__proto__=t)}Object.defineProperty(t,"__esModule",{value:!0});var i=function(){function e(e,t){for(var l=0;l0;t&&this.fillLevels(e.levels)}},{key:"reload",value:function(){this.unBindEvents(),this.bindEvents(),this.bindPlaybackEvents()}},{key:"shouldRender",value:function(){if(!this.core.getCurrentContainer())return!1;var e=this.core.getCurrentPlayback();if(!e)return!1;var t=void 0!==e.currentLevel,l=!!(this.levels&&this.levels.length>1);return t&&l}},{key:"render",value:function(){if(this.shouldRender()){var e=a.Styler.getStyleFor(h["default"],{baseUrl:this.core.options.baseUrl});this.$el.html(this.template({levels:this.levels,title:this.getTitle()})),this.$el.append(e),this.core.mediaControl.$(".media-control-right-panel").append(this.el),this.highlightCurrentLevel()}return this}},{key:"fillLevels",value:function(e){var t=arguments.length<=1||void 0===arguments[1]?f:arguments[1];void 0===this.selectedLevelId&&(this.selectedLevelId=t),this.levels=e,this.configureLevelsLabels(),this.render()}},{key:"configureLevelsLabels",value:function(){if(void 0!==this.core.options.levelSelectorConfig){var e=this.core.options.levelSelectorConfig.labelCallback;if(e&&"function"!=typeof e)throw new TypeError("labelCallback must be a function");var t=this.core.options.levelSelectorConfig.labels,l=t?this.core.options.levelSelectorConfig.labels:{};if(e||t){var n,o;for(var r in this.levels)n=this.levels[r],o=l[n.id],e?n.label=e(n,o):o&&(n.label=o)}}}},{key:"findLevelBy",value:function(e){var t;return this.levels.forEach(function(l){l.id===e&&(t=l)}),t}},{key:"onLevelSelect",value:function(e){return this.selectedLevelId=parseInt(e.target.dataset.levelSelectorSelect,10),this.core.getCurrentPlayback().currentLevel!=this.selectedLevelId&&(this.core.getCurrentPlayback().currentLevel=this.selectedLevelId,this.toggleContextMenu(),e.stopPropagation(),!1)}},{key:"onShowLevelSelectMenu",value:function(e){this.toggleContextMenu()}},{key:"hideSelectLevelMenu",value:function(){this.$(".level_selector ul").hide()}},{key:"toggleContextMenu",value:function(){this.$(".level_selector ul").toggle()}},{key:"buttonElement",value:function(){return this.$(".level_selector button")}},{key:"levelElement",value:function(e){return this.$(".level_selector ul a"+(isNaN(e)?"":'[data-level-selector-select="'+e+'"]')).parent()}},{key:"getTitle",value:function(){return(this.core.options.levelSelectorConfig||{}).title}},{key:"startLevelSwitch",value:function(){this.buttonElement().addClass("changing")}},{key:"stopLevelSwitch",value:function(){this.buttonElement().removeClass("changing")}},{key:"updateText",value:function(e){e===f?this.buttonElement().text(this.currentLevel?"AUTO ("+this.currentLevel.label+")":"AUTO"):this.buttonElement().text(this.findLevelBy(e).label)}},{key:"updateCurrentLevel",value:function(e){var t=this.findLevelBy(e.level);this.currentLevel=t?t:null,this.highlightCurrentLevel()}},{key:"highlightCurrentLevel",value:function(){this.levelElement().removeClass("current"),this.currentLevel&&this.levelElement(this.currentLevel.id).addClass("current"),this.updateText(this.selectedLevelId)}},{key:"name",get:function(){return"level_selector"}},{key:"template",get:function(){return(0,a.template)(u["default"])}},{key:"attributes",get:function(){return{"class":this.name,"data-level-selector":""}}},{key:"events",get:function(){return{"click [data-level-selector-select]":"onLevelSelect","click [data-level-selector-button]":"onShowLevelSelectMenu"}}}],[{key:"version",get:function(){return VERSION}}]),t}(a.UICorePlugin);t["default"]=d,e.exports=t["default"]},/*!*************************!*\ 9 | !*** external "Clappr" ***! 10 | \*************************/ 11 | function(t,l){t.exports=e},/*!****************************************!*\ 12 | !*** ./src/public/level-selector.html ***! 13 | \****************************************/ 14 | function(e,t){e.exports='\n\n'},/*!*******************************!*\ 15 | !*** ./src/public/style.scss ***! 16 | \*******************************/ 17 | function(e,t,l){t=e.exports=l(/*! ./../../~/css-loader/lib/css-base.js */5)(),t.push([e.id,".level_selector[data-level-selector]{float:right;margin-top:5px;position:relative}.level_selector[data-level-selector] button{background-color:transparent;color:#fff;font-family:Roboto,Open Sans,Arial,sans-serif;-webkit-font-smoothing:antialiased;border:none;font-size:10px}.level_selector[data-level-selector] button:hover{color:#c9c9c9}.level_selector[data-level-selector] button.changing{-webkit-animation:pulse .5s infinite alternate}.level_selector[data-level-selector]>ul{list-style-type:none;position:absolute;bottom:25px;border:1px solid #000;display:none;background-color:#e6e6e6}.level_selector[data-level-selector] li{font-size:10px}.level_selector[data-level-selector] li[data-title]{background-color:#c3c2c2;padding:5px}.level_selector[data-level-selector] li a{color:#444;padding:2px 10px;display:block;text-decoration:none}.level_selector[data-level-selector] li a:hover{background-color:#555;color:#fff}.level_selector[data-level-selector] li a:hover a{color:#fff;text-decoration:none}.level_selector[data-level-selector] li.current a{color:red}@-webkit-keyframes pulse{0%{color:#fff}50%{color:#ff0101}to{color:#b80000}}",""])},/*!**************************************!*\ 18 | !*** ./~/css-loader/lib/css-base.js ***! 19 | \**************************************/ 20 | function(e,t){"use strict";e.exports=function(){var e=[];return e.toString=function(){for(var e=[],t=0;t { 22 | try { 23 | if (!this.app) this.app = firebase.initializeApp(config) 24 | await this.app.auth().signInAnonymously() 25 | this.connected = true 26 | resolve() 27 | } catch (err) { 28 | reject(err) 29 | } 30 | }) 31 | }, 32 | 33 | getRef (str) { 34 | if (!this.refs[str]) { 35 | this.refs[str] = this.app.database().ref(str) 36 | } 37 | 38 | return this.refs[str] 39 | }, 40 | 41 | install (Vue) { 42 | Object.defineProperty(Vue.prototype, '$firebase', { 43 | get () { 44 | return Firebase 45 | } 46 | }) 47 | } 48 | } 49 | 50 | export default Firebase 51 | -------------------------------------------------------------------------------- /src/lib/prettyTime.js: -------------------------------------------------------------------------------- 1 | export default function prettyTime (time) { 2 | const mins = Math.floor(time / 60) 3 | const secs = time - (mins * 60) 4 | 5 | return `${mins}:${secs < 10 ? '0' : ''}${secs}` 6 | } 7 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import Vue from 'vue' 3 | import App from './App' 4 | import router from './router' 5 | import store from './store' 6 | import { sync } from 'vuex-router-sync' 7 | import Firebase from 'lib/firebase' 8 | import VTooltip from 'v-tooltip' 9 | import cdnRewrite from 'lib/cdnRewrite' 10 | 11 | Vue.use(VTooltip) 12 | Vue.use(Firebase) 13 | sync(store, router) 14 | Vue.filter('cdnRewrite', cdnRewrite) 15 | 16 | /* eslint-disable no-new */ 17 | new Vue({ 18 | el: '#app', 19 | router, 20 | store, 21 | render: h => h(App) 22 | }) 23 | 24 | if (process.env.NODE_ENV === 'production') { 25 | const runtime = require('offline-plugin/runtime') 26 | runtime.install({ 27 | onUpdateReady () { 28 | runtime.applyUpdate() 29 | }, 30 | onUpdated () { 31 | localStorage.setItem('updated', Date.now()) 32 | location.reload() 33 | } 34 | }) 35 | } 36 | -------------------------------------------------------------------------------- /src/pages/Changelog.vue: -------------------------------------------------------------------------------- 1 | 149 | 150 | 163 | 164 | 197 | -------------------------------------------------------------------------------- /src/pages/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 124 | 125 | 130 | -------------------------------------------------------------------------------- /src/pages/History.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 54 | 55 | 60 | -------------------------------------------------------------------------------- /src/pages/Login.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 74 | 75 | 102 | -------------------------------------------------------------------------------- /src/pages/Media.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 333 | 334 | 376 | -------------------------------------------------------------------------------- /src/pages/Migrate.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 33 | -------------------------------------------------------------------------------- /src/pages/Notice.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 45 | -------------------------------------------------------------------------------- /src/pages/Queue.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 67 | 68 | 104 | -------------------------------------------------------------------------------- /src/pages/Room.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | -------------------------------------------------------------------------------- /src/pages/Search.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 72 | 73 | 78 | -------------------------------------------------------------------------------- /src/pages/Series.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 185 | 186 | 223 | -------------------------------------------------------------------------------- /src/pages/Settings.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 123 | 124 | 129 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Meta from 'vue-meta' 4 | import Analytics from 'vue-analytics' 5 | import store from '../store' 6 | import {authGuard, loginGuard} from 'lib/auth' 7 | import {cancelCurrentRequests} from 'lib/api' 8 | 9 | import Dashboard from 'pages/Dashboard' 10 | import Migrate from 'pages/Migrate' 11 | import Queue from 'pages/Queue' 12 | import Settings from 'pages/Settings' 13 | import History from 'pages/History' 14 | import Search from 'pages/Search' 15 | import Login from 'pages/Login' 16 | import Series from 'pages/Series' 17 | import Media from 'pages/Media' 18 | import Room from 'pages/Room' 19 | import Changelog from 'pages/Changelog' 20 | import Notice from 'pages/Notice' 21 | 22 | Vue.use(Router) 23 | Vue.use(Meta) 24 | 25 | const router = new Router({ 26 | mode: 'history', 27 | scrollBehavior (to, from, savedPosition) { 28 | if (savedPosition) { 29 | return savedPosition 30 | } else { 31 | return { x: 0, y: 0 } 32 | } 33 | }, 34 | routes: [ 35 | { 36 | path: '/', 37 | name: 'dashboard', 38 | component: Dashboard, 39 | beforeEnter: authGuard 40 | }, 41 | { 42 | path: '/migrate', 43 | name: 'migrate', 44 | component: Migrate 45 | }, 46 | { 47 | path: '/queue', 48 | name: 'queue', 49 | component: Queue, 50 | beforeEnter: authGuard 51 | }, 52 | { 53 | path: '/settings', 54 | name: 'settings', 55 | component: Settings, 56 | beforeEnter: authGuard 57 | }, 58 | { 59 | path: '/history', 60 | name: 'history', 61 | component: History, 62 | beforeEnter: authGuard 63 | }, 64 | { 65 | path: '/login', 66 | name: 'login', 67 | component: Login, 68 | beforeEnter: loginGuard 69 | }, 70 | { 71 | path: '/search', 72 | name: 'search', 73 | component: Search, 74 | beforeEnter: authGuard 75 | }, 76 | { 77 | path: '/series/:id', 78 | name: 'series', 79 | component: Series, 80 | beforeEnter: authGuard 81 | }, 82 | { 83 | path: '/series/:seriesId/:id', 84 | name: 'media', 85 | component: Media, 86 | beforeEnter: authGuard 87 | }, 88 | { 89 | path: '/room/:id', 90 | name: 'room', 91 | component: Room, 92 | beforeEnter: authGuard 93 | }, 94 | { 95 | path: '/changelog', 96 | name: 'changelog', 97 | component: Changelog, 98 | beforeEnter: authGuard 99 | }, 100 | { 101 | path: '/future-of-umi', 102 | name: 'future-of-umi', 103 | component: Notice 104 | } 105 | ] 106 | }) 107 | 108 | router.beforeEach((to, from, next) => { 109 | // cancel navigation when in host-only mode and client is not the host 110 | if (store.state.roomConnected && store.state.roomData.hostOnly && !store.getters.isRoomHost && to.name !== from.name) { 111 | store.dispatch('flashGuestMessage') 112 | return next(false) 113 | } 114 | 115 | // on all page changes aside from the initial load, we cancel in progress requests 116 | if (from.name && from.name !== to.name) { 117 | cancelCurrentRequests() 118 | } 119 | next() 120 | }) 121 | 122 | if (process.env.NODE_ENV === 'production') { 123 | Vue.use(Analytics, {id: 'UA-46859303-4', router}) 124 | } 125 | 126 | export default router 127 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | import uuid from 'uuid/v4' 4 | import axios, {isCancel} from 'axios' 5 | 6 | import api, {ACCESS_TOKEN, DEVICE_TYPE, LOCALE, VERSION, UMI_SERVER} from 'lib/api' 7 | import {getUuid} from 'lib/auth' 8 | import Firebase from 'lib/firebase' 9 | 10 | const MEDIA_FIELDS = 'media.media_id,media.available,media.available_time,media.collection_id,media.collection_name,media.series_id,media.type,media.episode_number,media.name,media.description,media.screenshot_image,media.created,media.duration,media.playhead,media.bif_url' 11 | const SERIES_FIELDS = 'series.series_id,series.name,series.portrait_image,series.landscape_image,series.description,series.in_queue' 12 | 13 | Vue.use(Vuex) 14 | 15 | function handleError (err, reject) { 16 | if (!isCancel(err)) { 17 | store.commit('SET_ERROR', true) 18 | reject(err) 19 | } 20 | } 21 | 22 | const ANILIST_AUTH_QUERY = ` 23 | query { 24 | Viewer { 25 | name 26 | } 27 | } 28 | ` 29 | 30 | const store = new Vuex.Store({ 31 | state: { 32 | auth: localStorage.getItem('auth') ? ( 33 | JSON.parse(localStorage.getItem('auth')) 34 | ) : ( 35 | {} 36 | ), 37 | malAuth: localStorage.getItem('malAuth') ? ( 38 | JSON.parse(localStorage.getItem('malAuth')) 39 | ) : ( 40 | {} 41 | ), 42 | alAuth: localStorage.getItem('alAuth') ? ( 43 | JSON.parse(localStorage.getItem('alAuth')) 44 | ) : ( 45 | {} 46 | ), 47 | locales: [], 48 | series: {}, 49 | seriesCollections: {}, 50 | collections: {}, 51 | collectionMedia: {}, 52 | media: {}, 53 | searchIds: [], 54 | searchQuery: '', 55 | queueData: [], 56 | initialHistory: [], 57 | recent: [], 58 | roomId: '', 59 | roomConnected: false, 60 | roomMenu: false, 61 | roomData: {}, 62 | connectedCount: 0, 63 | lights: false, 64 | error: false, 65 | expiredSession: '', 66 | guestMessage: false, 67 | readExtension: localStorage.getItem('readExtension') ? true : false 68 | }, 69 | 70 | actions: { 71 | startSession ({commit, state}) { 72 | const params = { 73 | access_token: ACCESS_TOKEN, 74 | device_type: DEVICE_TYPE, 75 | device_id: getUuid() 76 | } 77 | 78 | if (state.auth.token) { 79 | params.auth = state.auth.token 80 | } 81 | 82 | return new Promise(async (resolve, reject) => { 83 | try { 84 | const resp = await api({route: 'start_session', params, noCancel: true}) 85 | const data = resp.data.data 86 | commit('UPDATE_AUTH', { 87 | session_id: data.session_id, 88 | country: data.country_code.toLowerCase(), 89 | token: data.auth, 90 | expires: data.expires 91 | }) 92 | resolve() 93 | // fetch locales in the background 94 | const localeResp = await api({ 95 | route: 'list_locales', 96 | version: '1', 97 | params: {session_id: data.session_id}, 98 | noCancel: true 99 | }) 100 | commit('UPDATE_LOCALES', localeResp.data.data.locales) 101 | } catch (err) { 102 | reject(err) 103 | } 104 | }) 105 | }, 106 | 107 | login ({commit, state, dispatch}, {username, password}) { 108 | const form = new FormData() 109 | form.append('account', username) 110 | form.append('password', password) 111 | form.append('session_id', state.auth.session_id) 112 | form.append('locale', LOCALE()) 113 | form.append('version', VERSION) 114 | 115 | return new Promise(async (resolve, reject) => { 116 | try { 117 | const resp = await api({method: 'post', route: 'login', data: form}) 118 | if (resp.data.error) throw resp 119 | 120 | const data = resp.data.data 121 | if (data.user.premium.indexOf('anime') === -1) { 122 | return reject(new Error('You must have a premium Crunchyroll account to use Umi.')) 123 | } 124 | commit('UPDATE_AUTH', { 125 | token: data.auth, 126 | expires: data.expires, 127 | username: data.user.username 128 | }) 129 | commit('SET_EXPIRED_SESSION', '') 130 | resolve() 131 | } catch (err) { 132 | reject(err) 133 | } 134 | }) 135 | }, 136 | 137 | logout ({commit, dispatch, state}, didExpire) { 138 | return new Promise(async (resolve, reject) => { 139 | try { 140 | if (didExpire) { 141 | commit('SET_EXPIRED_SESSION', state.auth.username) 142 | } 143 | commit('REMOVE_AUTH') 144 | await dispatch('startSession') 145 | resolve() 146 | commit('SET_INITIAL_HISTORY', []) 147 | commit('SET_QUEUE_DATA', []) 148 | } catch (err) { 149 | handleError(err, reject) 150 | } 151 | }) 152 | }, 153 | 154 | authenticateMal ({commit, state}, {username, password}) { 155 | return new Promise(async (resolve, reject) => { 156 | try { 157 | const {data} = await axios.post(`${UMI_SERVER}/mal/login`, {username, password}) 158 | if (data.status === 'ok') { 159 | commit('UPDATE_MAL', data) 160 | resolve() 161 | } else { 162 | reject(new Error(data.error)) 163 | } 164 | } catch (err) { 165 | reject(err) 166 | } 167 | }) 168 | }, 169 | 170 | authenticateAniList ({commit}, {token}) { 171 | return new Promise(async (resolve, reject) => { 172 | try { 173 | const {data: {data, errors}} = await axios({ 174 | method: 'post', 175 | url: 'https://graphql.anilist.co', 176 | headers: { 177 | Authorization: `Bearer ${token}` 178 | }, 179 | data: { 180 | query: ANILIST_AUTH_QUERY 181 | } 182 | }) 183 | if (errors && errors.length > 0) { 184 | reject(new Error(errors[0])) 185 | } else { 186 | console.log(data) 187 | commit('UPDATE_AL', { 188 | token, 189 | name: data.Viewer.name 190 | }) 191 | resolve() 192 | } 193 | } catch (err) { 194 | reject(err) 195 | } 196 | }) 197 | }, 198 | 199 | getQueueInfo ({commit, state}, force) { 200 | const params = { 201 | session_id: state.auth.session_id, 202 | media_types: 'anime|drama', 203 | fields: [MEDIA_FIELDS, SERIES_FIELDS].join(',') 204 | } 205 | 206 | if (state.queueData.length > 0 && !force) return Promise.resolve() 207 | 208 | return new Promise(async (resolve, reject) => { 209 | try { 210 | const resp = await api({route: 'queue', params}) 211 | if (resp.data.error) throw resp 212 | 213 | const data = resp.data.data 214 | commit('SET_QUEUE_DATA', data) 215 | data.forEach((d) => { 216 | commit('ADD_SERIES', d.series) 217 | commit('ADD_MEDIA', d.most_likely_media) 218 | commit('ADD_MEDIA', d.last_watched_media) 219 | }) 220 | resolve() 221 | } catch (err) { 222 | handleError(err, reject) 223 | } 224 | }) 225 | }, 226 | 227 | getHistoryInfo ({commit, state}, {limit = 24, offset = 0} = {}) { 228 | const params = { 229 | session_id: state.auth.session_id, 230 | media_types: 'anime|drama', 231 | fields: [MEDIA_FIELDS, SERIES_FIELDS].join(','), 232 | limit, 233 | offset 234 | } 235 | 236 | return new Promise(async (resolve, reject) => { 237 | try { 238 | const resp = await api({route: 'recently_watched', params}) 239 | if (resp.data.error) throw resp 240 | 241 | const data = resp.data.data 242 | if (offset === 0) { 243 | commit('SET_INITIAL_HISTORY', data) 244 | } 245 | data.forEach((d) => { 246 | commit('ADD_SERIES', d.series) 247 | commit('ADD_COLLECTION', d.collection) 248 | commit('ADD_MEDIA', d.media) 249 | }) 250 | resolve(data) 251 | } catch (err) { 252 | handleError(err, reject) 253 | } 254 | }) 255 | }, 256 | 257 | getRecentInfo ({commit, state}) { 258 | const params = { 259 | session_id: state.auth.session_id, 260 | media_type: 'anime', 261 | fields: [MEDIA_FIELDS, 'media.series_name', 'series.most_recent_media'].join(','), 262 | limit: 50, 263 | offset: 0, 264 | filter: 'updated' 265 | } 266 | 267 | if (state.recent.length > 0) return Promise.resolve(state.recent) 268 | 269 | return new Promise(async (resolve, reject) => { 270 | try { 271 | const resp = await api({route: 'list_series', params}) 272 | if (resp.data.error) throw resp 273 | 274 | const data = resp.data.data 275 | data.forEach((d) => { 276 | commit('ADD_MEDIA', d.most_recent_media) 277 | }) 278 | commit('SET_RECENT', data) 279 | resolve(data) 280 | } catch (err) { 281 | handleError(err, reject) 282 | } 283 | }) 284 | }, 285 | 286 | search ({commit, state}, q) { 287 | const params = { 288 | session_id: state.auth.session_id, 289 | classes: 'series', 290 | limit: 999, 291 | offset: 0, 292 | media_types: 'anime|drama', 293 | fields: SERIES_FIELDS, 294 | q 295 | } 296 | 297 | return new Promise(async (resolve, reject) => { 298 | try { 299 | const resp = await api({route: 'autocomplete', params}) 300 | if (resp.data.error) throw resp 301 | 302 | const data = resp.data.data 303 | data.forEach((d) => { 304 | commit('ADD_SERIES', d) 305 | }) 306 | commit('SET_SEARCH_IDS', data.map((d) => d.series_id)) 307 | resolve() 308 | } catch (err) { 309 | handleError(err, reject) 310 | } 311 | }) 312 | }, 313 | 314 | getSeriesInfo ({commit, state}, id) { 315 | const params = { 316 | session_id: state.auth.session_id, 317 | series_id: id, 318 | fields: SERIES_FIELDS 319 | } 320 | 321 | if (state.series[id]) return Promise.resolve() 322 | 323 | return new Promise(async (resolve, reject) => { 324 | try { 325 | const resp = await api({route: 'info', params}) 326 | if (resp.data.error) throw resp 327 | 328 | const data = resp.data.data 329 | commit('ADD_SERIES', data) 330 | resolve() 331 | } catch (err) { 332 | handleError(err, reject) 333 | } 334 | }) 335 | }, 336 | 337 | updateSeriesQueue ({commit, state, dispatch}, {id, queueStatus}) { 338 | const form = new FormData() 339 | form.append('session_id', state.auth.session_id) 340 | form.append('locale', LOCALE()) 341 | form.append('version', VERSION) 342 | form.append('series_id', id) 343 | 344 | commit('UPDATE_SERIES_QUEUE', {id, queueStatus}) 345 | 346 | return new Promise(async (resolve, reject) => { 347 | try { 348 | const resp = await api({method: 'post', route: queueStatus ? 'add_to_queue' : 'remove_from_queue', data: form}) 349 | if (resp.data.error) throw resp 350 | 351 | dispatch('getQueueInfo', true) 352 | resolve() 353 | } catch (err) { 354 | handleError(err, reject) 355 | } 356 | }) 357 | }, 358 | 359 | getCollectionsForSeries ({commit, state}, id) { 360 | const params = { 361 | session_id: state.auth.session_id, 362 | series_id: id, 363 | limit: 5000, 364 | offset: 0 365 | } 366 | 367 | if (state.seriesCollections[id]) return Promise.resolve() 368 | 369 | return new Promise(async (resolve, reject) => { 370 | try { 371 | const resp = await api({route: 'list_collections', params}) 372 | if (resp.data.error) throw resp 373 | 374 | const data = resp.data.data 375 | data.forEach((d) => { 376 | commit('ADD_COLLECTION', d) 377 | }) 378 | commit('ADD_SERIES_COLLECTION', {id, arr: data.map((d) => d.collection_id)}) 379 | resolve() 380 | } catch (err) { 381 | handleError(err, reject) 382 | } 383 | }) 384 | }, 385 | 386 | getMediaForCollection ({commit, state}, id) { 387 | const params = { 388 | session_id: state.auth.session_id, 389 | collection_id: id, 390 | include_clips: 1, 391 | limit: 5000, 392 | offset: 0, 393 | fields: MEDIA_FIELDS 394 | } 395 | 396 | if (state.collectionMedia[id]) return Promise.resolve() 397 | 398 | return new Promise(async (resolve, reject) => { 399 | try { 400 | const resp = await api({route: 'list_media', params}) 401 | if (resp.data.error) throw resp 402 | 403 | const data = resp.data.data 404 | data.forEach((d) => { 405 | commit('ADD_MEDIA', d) 406 | }) 407 | commit('ADD_COLLECTION_MEDIA', {id, arr: data.map((d) => d.media_id)}) 408 | resolve() 409 | } catch (err) { 410 | handleError(err, reject) 411 | } 412 | }) 413 | }, 414 | 415 | getCollectionInfo ({commit, state}, id) { 416 | const params = { 417 | session_id: state.auth.session_id, 418 | collection_id: id 419 | } 420 | 421 | if (state.media[id]) return Promise.resolve() 422 | 423 | return new Promise(async (resolve, reject) => { 424 | try { 425 | const resp = await api({route: 'info', params}) 426 | if (resp.data.error) throw resp 427 | 428 | const data = resp.data.data 429 | commit('ADD_COLLECTION', data) 430 | resolve() 431 | } catch (err) { 432 | handleError(err, reject) 433 | } 434 | }) 435 | }, 436 | 437 | getMediaInfo ({commit, state, dispatch}, id) { 438 | const params = { 439 | session_id: state.auth.session_id, 440 | media_id: id, 441 | fields: MEDIA_FIELDS 442 | } 443 | 444 | if (state.media[id]) return Promise.resolve() 445 | 446 | return new Promise(async (resolve, reject) => { 447 | try { 448 | const resp = await api({route: 'info', params}) 449 | if (resp.data.error) throw resp 450 | 451 | const data = resp.data.data 452 | commit('ADD_MEDIA', data) 453 | resolve() 454 | } catch (err) { 455 | handleError(err, reject) 456 | } 457 | }) 458 | }, 459 | 460 | leaveRoom ({state, commit}) { 461 | const roomRef = Firebase.getRef(`/rooms/${state.roomId}`) 462 | const usersRef = Firebase.getRef(`/roomUsers/${state.roomId}`) 463 | const connectedRef = Firebase.getRef(`/roomUsers/${state.roomId}/${Firebase.app.auth().currentUser.uid}`) 464 | 465 | roomRef.off() 466 | usersRef.off() 467 | connectedRef.remove() 468 | 469 | commit('UPDATE_ROOM', '') 470 | commit('UPDATE_CONNECTED', false) 471 | commit('UPDATE_CONNECTED_COUNT', 1) 472 | }, 473 | 474 | enterRoom ({state, commit}, {id = uuid()}) { 475 | commit('UPDATE_ROOM', id) 476 | 477 | return new Promise(async (resolve, reject) => { 478 | try { 479 | await Firebase.init() 480 | const roomRef = Firebase.getRef(`/rooms/${id}`) 481 | const usersRef = Firebase.getRef(`/roomUsers/${id}`) 482 | const connectedRef = Firebase.getRef(`/roomUsers/${id}/${Firebase.app.auth().currentUser.uid}`) 483 | await connectedRef.set(true) 484 | connectedRef.onDisconnect().remove() 485 | 486 | const room = await roomRef.once('value') 487 | const value = room.val() || { 488 | playing: false, 489 | syncedTime: 0, 490 | host: Firebase.app.auth().currentUser.uid, 491 | hostOnly: false, 492 | route: { 493 | path: state.route.path, 494 | name: state.route.name 495 | } 496 | } 497 | 498 | if (!room.val()) { 499 | await roomRef.set(value) 500 | } 501 | 502 | roomRef.on('value', (snapshot) => { 503 | commit('UPDATE_ROOM_DATA', snapshot.val()) 504 | }) 505 | 506 | commit('UPDATE_CONNECTED', true) 507 | usersRef.on('value', (snapshot) => { 508 | if (snapshot.exists()) { 509 | commit('UPDATE_CONNECTED_COUNT', Object.keys(snapshot.val()).length) 510 | } 511 | }) 512 | 513 | resolve(value) 514 | } catch (err) { 515 | reject(err) 516 | } 517 | }) 518 | }, 519 | 520 | updateRoomData ({state, getters}, obj) { 521 | if (state.roomData.hostOnly && !getters.isRoomHost) return Promise.resolve() 522 | 523 | return new Promise(async (resolve, reject) => { 524 | try { 525 | const roomRef = Firebase.getRef(`/rooms/${state.roomId}`) 526 | 527 | await roomRef.update(obj) 528 | resolve() 529 | } catch (err) { 530 | reject(err) 531 | } 532 | }) 533 | }, 534 | 535 | flashGuestMessage ({state, commit}) { 536 | if (state.guestMessage) return 537 | 538 | commit('UPDATE_GUEST_MESSAGE', true) 539 | setTimeout(() => { 540 | commit('UPDATE_GUEST_MESSAGE', false) 541 | }, 5000) 542 | } 543 | }, 544 | 545 | mutations: { 546 | UPDATE_AUTH (state, obj) { 547 | const updated = Object.assign({}, state.auth, obj) 548 | localStorage.setItem('auth', JSON.stringify(updated)) 549 | Vue.set(state, 'auth', updated) 550 | }, 551 | 552 | UPDATE_LOCALES (state, arr) { 553 | state.locales = arr 554 | }, 555 | 556 | REMOVE_AUTH (state) { 557 | localStorage.removeItem('auth') 558 | Vue.set(state, 'auth', {}) 559 | }, 560 | 561 | UPDATE_MAL (state, obj) { 562 | const updated = Object.assign({}, state.malAuth, obj) 563 | localStorage.setItem('malAuth', JSON.stringify(updated)) 564 | Vue.set(state, 'malAuth', updated) 565 | }, 566 | 567 | REMOVE_MAL_AUTH (state) { 568 | localStorage.removeItem('malAuth') 569 | Vue.set(state, 'malAuth', {}) 570 | }, 571 | 572 | UPDATE_AL (state, obj) { 573 | const updated = Object.assign({}, state.alAuth, obj) 574 | localStorage.setItem('alAuth', JSON.stringify(updated)) 575 | Vue.set(state, 'alAuth', updated) 576 | }, 577 | 578 | REMOVE_AL_AUTH (state) { 579 | localStorage.removeItem('alAuth') 580 | Vue.set(state, 'alAuth', {}) 581 | }, 582 | 583 | SET_SEARCH_IDS (state, arr) { 584 | state.searchIds = arr 585 | }, 586 | 587 | SET_SEARCH_QUERY (state, str) { 588 | state.searchQuery = str 589 | }, 590 | 591 | SET_QUEUE_DATA (state, arr) { 592 | Vue.set(state, 'queueData', arr) 593 | }, 594 | 595 | SET_INITIAL_HISTORY (state, arr) { 596 | Vue.set(state, 'initialHistory', arr) 597 | }, 598 | 599 | SET_RECENT (state, arr) { 600 | Vue.set(state, 'recent', arr) 601 | }, 602 | 603 | ADD_SERIES (state, obj) { 604 | if (obj && obj.series_id) Vue.set(state.series, obj.series_id, obj) 605 | }, 606 | 607 | ADD_SERIES_COLLECTION (state, {id, arr}) { 608 | if (arr) Vue.set(state.seriesCollections, id, arr) 609 | }, 610 | 611 | ADD_COLLECTION (state, obj) { 612 | if (obj && obj.collection_id) Vue.set(state.collections, obj.collection_id, obj) 613 | }, 614 | 615 | ADD_COLLECTION_MEDIA (state, {id, arr}) { 616 | if (arr) Vue.set(state.collectionMedia, id, arr) 617 | }, 618 | 619 | ADD_MEDIA (state, obj) { 620 | if (obj && obj.media_id) Vue.set(state.media, obj.media_id, obj) 621 | }, 622 | 623 | UPDATE_ROOM (state, str) { 624 | state.roomId = str 625 | }, 626 | 627 | UPDATE_ROOM_DATA (state, obj) { 628 | Vue.set(state, 'roomData', obj) 629 | }, 630 | 631 | UPDATE_CONNECTED (state, bool) { 632 | state.roomConnected = bool 633 | }, 634 | 635 | UPDATE_CONNECTED_COUNT (state, int) { 636 | state.connectedCount = int 637 | }, 638 | 639 | UPDATE_ROOM_MENU (state, bool) { 640 | state.roomMenu = bool 641 | }, 642 | 643 | UPDATE_SERIES_QUEUE (state, {id, queueStatus}) { 644 | Vue.set(state.series[id], 'in_queue', queueStatus) 645 | }, 646 | 647 | UPDATE_LIGHTS (state, bool) { 648 | state.lights = bool 649 | }, 650 | 651 | SET_ERROR (state, bool) { 652 | state.error = bool 653 | }, 654 | 655 | SET_EXPIRED_SESSION (state, str) { 656 | state.expiredSession = str 657 | }, 658 | 659 | UPDATE_GUEST_MESSAGE (state, bool) { 660 | state.guestMessage = bool 661 | }, 662 | 663 | SET_READ_EXTENSION (state) { 664 | localStorage.setItem('readExtension', 'true') 665 | state.readExtension = true 666 | } 667 | }, 668 | 669 | getters: { 670 | isRoomHost (state) { 671 | return state.roomConnected && state.roomData.host === Firebase.app.auth().currentUser.uid 672 | } 673 | } 674 | }) 675 | 676 | export default store 677 | -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/remixz/umi/188084473e496026f5a996c401bd2599d6476580/static/.gitkeep --------------------------------------------------------------------------------