├── .babelrc ├── .electron-vue ├── build.js ├── dev-client.js ├── dev-runner.js ├── webpack.main.config.js ├── webpack.renderer.config.js └── webpack.web.config.js ├── .gitignore ├── .jsbeautifyrc ├── .prettierrc.js ├── .travis.yml ├── LICENSE ├── README.md ├── appveyor.yml ├── build └── icons │ ├── 256x256.png │ ├── icon old.ico │ ├── icon.icns │ ├── icon.ico │ └── icon.psd ├── dist ├── electron │ └── .gitkeep └── web │ └── .gitkeep ├── github_image ├── 001.png ├── 002.png └── 003.png ├── package-lock.json ├── package.json ├── src ├── config.js ├── index.ejs ├── main │ ├── bean │ │ └── FileItem.ts │ ├── index.dev.js │ ├── index.js │ └── service │ │ ├── AlbumDirSorter.ts │ │ ├── AlbumServiceImpl.ts │ │ └── DirBrowser.ts └── renderer │ ├── App.vue │ ├── CoreApp.vue │ ├── Home.vue │ ├── assets │ └── value │ │ ├── bookInstruction.js │ │ ├── instruction.js │ │ ├── string.js │ │ ├── tags.js │ │ └── version.js │ ├── bean │ ├── DialogBean.ts │ ├── DialogOperation.ts │ ├── ImgPageInfo.ts │ ├── ServerMessage.ts │ └── ThumbInfo.ts │ ├── components │ ├── AlbumBookView.vue │ ├── AlbumScrollView.vue │ ├── LoadingView.vue │ ├── ModalManager.vue │ ├── PageView.vue │ ├── ReaderView.vue │ ├── ThumbScrollView.vue │ ├── TopBar.vue │ ├── base │ │ └── AwesomeScrollView.vue │ └── widget │ │ ├── CircleIconButton.vue │ │ ├── DropOption.vue │ │ ├── FlatButton.vue │ │ ├── Pagination.vue │ │ ├── PopSlider.vue │ │ ├── Popover.vue │ │ ├── SimpleDialog.vue │ │ ├── SimpleSwitch.vue │ │ └── Slider.vue │ ├── main.js │ ├── router │ └── index.js │ ├── service │ ├── AlbumService.ts │ ├── InfoService.ts │ ├── PlatformService.js │ ├── SettingService.ts │ ├── StringService.ts │ ├── request │ │ ├── MultiAsyncReq.ts │ │ ├── ReqQueue.ts │ │ └── TextReq.ts │ └── storage │ │ ├── LocalStorage.ts │ │ ├── SyncStorage.ts │ │ └── base │ │ └── Storage.js │ ├── store │ ├── index.js │ ├── modules │ │ ├── AlbumView.js │ │ ├── Modal.js │ │ ├── String.js │ │ └── index.js │ └── mutation-types.js │ ├── style │ ├── _markdown.scss │ ├── _normalize.scss │ ├── _responsive.scss │ └── _variables.scss │ └── utils │ ├── DateUtil.js │ ├── DateWrapper.js │ ├── Logger.js │ ├── MdRenderer.js │ ├── Utils.ts │ ├── VueUtil.js │ ├── bezier-easing.js │ ├── formatter.js │ └── react-native-storage │ ├── error.js │ └── storage.js ├── static └── .gitkeep ├── test ├── .eslintrc └── unit │ ├── index.js │ ├── karma.conf.js │ └── specs │ └── LandingPage.spec.js ├── tsconfig.json ├── update.json ├── yarn-error.log └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "comments": false, 3 | "env": { 4 | "test": { 5 | "presets": [ 6 | ["env", { 7 | "targets": { "node": 7 } 8 | }], 9 | "stage-0" 10 | ], 11 | "plugins": ["istanbul"] 12 | }, 13 | "main": { 14 | "presets": [ 15 | ["env", { 16 | "targets": { "node": 7 } 17 | }], 18 | "stage-0" 19 | ] 20 | }, 21 | "renderer": { 22 | "presets": [ 23 | ["env", { 24 | "modules": false 25 | }], 26 | "stage-0" 27 | ] 28 | }, 29 | "web": { 30 | "presets": [ 31 | ["env", { 32 | "modules": false 33 | }], 34 | "stage-0" 35 | ] 36 | } 37 | }, 38 | "plugins": ["transform-runtime"] 39 | } 40 | -------------------------------------------------------------------------------- /.electron-vue/build.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.NODE_ENV = 'production' 4 | 5 | const { say } = require('cfonts') 6 | const chalk = require('chalk') 7 | const del = require('del') 8 | const { spawn } = require('child_process') 9 | const webpack = require('webpack') 10 | const Multispinner = require('multispinner') 11 | 12 | 13 | const mainConfig = require('./webpack.main.config') 14 | const rendererConfig = require('./webpack.renderer.config') 15 | const webConfig = require('./webpack.web.config') 16 | 17 | const doneLog = chalk.bgGreen.white(' DONE ') + ' ' 18 | const errorLog = chalk.bgRed.white(' ERROR ') + ' ' 19 | const okayLog = chalk.bgBlue.white(' OKAY ') + ' ' 20 | const isCI = process.env.CI || false 21 | 22 | if (process.env.BUILD_TARGET === 'clean') clean() 23 | else if (process.env.BUILD_TARGET === 'web') web() 24 | else build() 25 | 26 | function clean () { 27 | del.sync(['build/*', '!build/icons', '!build/icons/icon.*']) 28 | console.log(`\n${doneLog}\n`) 29 | process.exit() 30 | } 31 | 32 | function build () { 33 | greeting() 34 | 35 | del.sync(['dist/electron/*', '!.gitkeep']) 36 | 37 | const tasks = ['main', 'renderer'] 38 | const m = new Multispinner(tasks, { 39 | preText: 'building', 40 | postText: 'process' 41 | }) 42 | 43 | let results = '' 44 | 45 | m.on('success', () => { 46 | process.stdout.write('\x1B[2J\x1B[0f') 47 | console.log(`\n\n${results}`) 48 | console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`) 49 | process.exit() 50 | }) 51 | 52 | pack(mainConfig).then(result => { 53 | results += result + '\n\n' 54 | m.success('main') 55 | }).catch(err => { 56 | m.error('main') 57 | console.log(`\n ${errorLog}failed to build main process`) 58 | console.error(`\n${err}\n`) 59 | process.exit(1) 60 | }) 61 | 62 | pack(rendererConfig).then(result => { 63 | results += result + '\n\n' 64 | m.success('renderer') 65 | }).catch(err => { 66 | m.error('renderer') 67 | console.log(`\n ${errorLog}failed to build renderer process`) 68 | console.error(`\n${err}\n`) 69 | process.exit(1) 70 | }) 71 | } 72 | 73 | function pack (config) { 74 | return new Promise((resolve, reject) => { 75 | config.mode = 'production' 76 | webpack(config, (err, stats) => { 77 | if (err) reject(err.stack || err) 78 | else if (stats.hasErrors()) { 79 | let err = '' 80 | 81 | stats.toString({ 82 | chunks: false, 83 | colors: true 84 | }) 85 | .split(/\r?\n/) 86 | .forEach(line => { 87 | err += ` ${line}\n` 88 | }) 89 | 90 | reject(err) 91 | } else { 92 | resolve(stats.toString({ 93 | chunks: false, 94 | colors: true 95 | })) 96 | } 97 | }) 98 | }) 99 | } 100 | 101 | function web () { 102 | del.sync(['dist/web/*', '!.gitkeep']) 103 | webConfig.mode = 'production' 104 | webpack(webConfig, (err, stats) => { 105 | if (err || stats.hasErrors()) console.log(err) 106 | 107 | console.log(stats.toString({ 108 | chunks: false, 109 | colors: true 110 | })) 111 | 112 | process.exit() 113 | }) 114 | } 115 | 116 | function greeting () { 117 | const cols = process.stdout.columns 118 | let text = '' 119 | 120 | if (cols > 85) text = 'lets-build' 121 | else if (cols > 60) text = 'lets-|build' 122 | else text = false 123 | 124 | if (text && !isCI) { 125 | say(text, { 126 | colors: ['yellow'], 127 | font: 'simple3d', 128 | space: false 129 | }) 130 | } else console.log(chalk.yellow.bold('\n lets-build')) 131 | console.log() 132 | } -------------------------------------------------------------------------------- /.electron-vue/dev-client.js: -------------------------------------------------------------------------------- 1 | const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 2 | 3 | hotClient.subscribe(event => { 4 | /** 5 | * Reload browser when HTMLWebpackPlugin emits a new index.html 6 | * 7 | * Currently disabled until jantimon/html-webpack-plugin#680 is resolved. 8 | * https://github.com/SimulatedGREG/electron-vue/issues/437 9 | * https://github.com/jantimon/html-webpack-plugin/issues/680 10 | */ 11 | // if (event.action === 'reload') { 12 | // window.location.reload() 13 | // } 14 | 15 | /** 16 | * Notify `mainWindow` when `main` process is compiling, 17 | * giving notice for an expected reload of the `electron` process 18 | */ 19 | if (event.action === 'compiling') { 20 | document.body.innerHTML += ` 21 | 34 | 35 |
36 | Compiling Main Process... 37 |
38 | ` 39 | } 40 | }) 41 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const chalk = require('chalk') 4 | const electron = require('electron') 5 | const path = require('path') 6 | const { say } = require('cfonts') 7 | const { spawn } = require('child_process') 8 | const webpack = require('webpack') 9 | const WebpackDevServer = require('webpack-dev-server') 10 | const webpackHotMiddleware = require('webpack-hot-middleware') 11 | 12 | const mainConfig = require('./webpack.main.config') 13 | const rendererConfig = require('./webpack.renderer.config') 14 | 15 | let electronProcess = null 16 | let manualRestart = false 17 | let hotMiddleware 18 | 19 | function logStats (proc, data) { 20 | let log = '' 21 | 22 | log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`) 23 | log += '\n\n' 24 | 25 | if (typeof data === 'object') { 26 | data.toString({ 27 | colors: true, 28 | chunks: false 29 | }).split(/\r?\n/).forEach(line => { 30 | log += ' ' + line + '\n' 31 | }) 32 | } else { 33 | log += ` ${data}\n` 34 | } 35 | 36 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 37 | 38 | console.log(log) 39 | } 40 | 41 | function startRenderer () { 42 | return new Promise((resolve, reject) => { 43 | rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer) 44 | rendererConfig.mode = 'development' 45 | const compiler = webpack(rendererConfig) 46 | hotMiddleware = webpackHotMiddleware(compiler, { 47 | log: false, 48 | heartbeat: 2500 49 | }) 50 | 51 | compiler.hooks.compilation.tap('compilation', compilation => { 52 | compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => { 53 | hotMiddleware.publish({ action: 'reload' }) 54 | cb() 55 | }) 56 | }) 57 | 58 | compiler.hooks.done.tap('done', stats => { 59 | logStats('Renderer', stats) 60 | }) 61 | 62 | const server = new WebpackDevServer( 63 | compiler, 64 | { 65 | contentBase: path.join(__dirname, '../'), 66 | quiet: true, 67 | before (app, ctx) { 68 | app.use(hotMiddleware) 69 | ctx.middleware.waitUntilValid(() => { 70 | resolve() 71 | }) 72 | } 73 | } 74 | ) 75 | 76 | server.listen(9080) 77 | }) 78 | } 79 | 80 | function startMain () { 81 | return new Promise((resolve, reject) => { 82 | mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main) 83 | mainConfig.mode = 'development' 84 | const compiler = webpack(mainConfig) 85 | 86 | compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 87 | logStats('Main', chalk.white.bold('compiling...')) 88 | hotMiddleware.publish({ action: 'compiling' }) 89 | done() 90 | }) 91 | 92 | compiler.watch({}, (err, stats) => { 93 | if (err) { 94 | console.log(err) 95 | return 96 | } 97 | 98 | logStats('Main', stats) 99 | 100 | if (electronProcess && electronProcess.kill) { 101 | manualRestart = true 102 | process.kill(electronProcess.pid) 103 | electronProcess = null 104 | startElectron() 105 | 106 | setTimeout(() => { 107 | manualRestart = false 108 | }, 5000) 109 | } 110 | 111 | resolve() 112 | }) 113 | }) 114 | } 115 | 116 | function startElectron () { 117 | var args = [ 118 | '--inspect=5858', 119 | path.join(__dirname, '../dist/electron/main.js') 120 | ] 121 | 122 | // detect yarn or npm and process commandline args accordingly 123 | if (process.env.npm_execpath.endsWith('yarn.js')) { 124 | args = args.concat(process.argv.slice(3)) 125 | } else if (process.env.npm_execpath.endsWith('npm-cli.js')) { 126 | args = args.concat(process.argv.slice(2)) 127 | } 128 | 129 | electronProcess = spawn(electron, args) 130 | 131 | electronProcess.stdout.on('data', data => { 132 | electronLog(data, 'blue') 133 | }) 134 | electronProcess.stderr.on('data', data => { 135 | electronLog(data, 'red') 136 | }) 137 | 138 | electronProcess.on('close', () => { 139 | if (!manualRestart) process.exit() 140 | }) 141 | } 142 | 143 | function electronLog (data, color) { 144 | let log = '' 145 | data = data.toString().split(/\r?\n/) 146 | data.forEach(line => { 147 | log += ` ${line}\n` 148 | }) 149 | if (/[0-9A-z]+/.test(log)) { 150 | console.log( 151 | chalk[color].bold('┏ Electron -------------------') + 152 | '\n\n' + 153 | log + 154 | chalk[color].bold('┗ ----------------------------') + 155 | '\n' 156 | ) 157 | } 158 | } 159 | 160 | function greeting () { 161 | const cols = process.stdout.columns 162 | let text = '' 163 | 164 | if (cols > 104) text = 'electron-vue' 165 | else if (cols > 76) text = 'electron-|vue' 166 | else text = false 167 | 168 | if (text) { 169 | say(text, { 170 | colors: ['yellow'], 171 | font: 'simple3d', 172 | space: false 173 | }) 174 | } else console.log(chalk.yellow.bold('\n electron-vue')) 175 | console.log(chalk.blue(' getting ready...') + '\n') 176 | } 177 | 178 | function init () { 179 | greeting() 180 | 181 | Promise.all([startRenderer(), startMain()]) 182 | .then(() => { 183 | startElectron() 184 | }) 185 | .catch(err => { 186 | console.error(err) 187 | }) 188 | } 189 | 190 | init() 191 | -------------------------------------------------------------------------------- /.electron-vue/webpack.main.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'main' 4 | 5 | const path = require('path') 6 | const { 7 | dependencies 8 | } = require('../package.json') 9 | const webpack = require('webpack') 10 | 11 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 12 | 13 | let mainConfig = { 14 | entry: { 15 | main: path.join(__dirname, '../src/main/index.js') 16 | }, 17 | externals: [ 18 | ...Object.keys(dependencies || {}) 19 | ], 20 | module: { 21 | rules: [{ 22 | test: /\.js$/, 23 | use: 'babel-loader', 24 | exclude: /node_modules/ 25 | }, 26 | { 27 | test: /\.node$/, 28 | use: 'node-loader' 29 | }, 30 | { 31 | test: /\.tsx?$/, 32 | loader: 'ts-loader', 33 | exclude: /node_modules/, 34 | options: { 35 | appendTsSuffixTo: [/\.vue$/] 36 | } 37 | } 38 | ] 39 | }, 40 | node: { 41 | __dirname: process.env.NODE_ENV !== 'production', 42 | __filename: process.env.NODE_ENV !== 'production' 43 | }, 44 | output: { 45 | filename: '[name].js', 46 | libraryTarget: 'commonjs2', 47 | path: path.join(__dirname, '../dist/electron') 48 | }, 49 | plugins: [ 50 | new webpack.NoEmitOnErrorsPlugin() 51 | ], 52 | resolve: { 53 | extensions: ['.ts', '.js', '.json', '.node'] 54 | }, 55 | target: 'electron-main' 56 | } 57 | 58 | /** 59 | * Adjust mainConfig for development settings 60 | */ 61 | if (process.env.NODE_ENV !== 'production') { 62 | mainConfig.plugins.push( 63 | new webpack.DefinePlugin({ 64 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 65 | }) 66 | ) 67 | } 68 | 69 | /** 70 | * Adjust mainConfig for production settings 71 | */ 72 | if (process.env.NODE_ENV === 'production') { 73 | mainConfig.plugins.push( 74 | new BabiliWebpackPlugin(), 75 | new webpack.DefinePlugin({ 76 | 'process.env.NODE_ENV': '"production"' 77 | }) 78 | ) 79 | } 80 | 81 | module.exports = mainConfig -------------------------------------------------------------------------------- /.electron-vue/webpack.renderer.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'renderer' 4 | 5 | const path = require('path') 6 | const { 7 | dependencies 8 | } = require('../package.json') 9 | const webpack = require('webpack') 10 | 11 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 12 | const CopyWebpackPlugin = require('copy-webpack-plugin') 13 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 14 | const HtmlWebpackPlugin = require('html-webpack-plugin') 15 | const { 16 | VueLoaderPlugin 17 | } = require('vue-loader') 18 | 19 | /** 20 | * List of node_modules to include in webpack bundle 21 | * 22 | * Required for specific packages like Vue UI libraries 23 | * that provide pure *.vue files that need compiling 24 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals 25 | */ 26 | let whiteListedModules = ['vue'] 27 | 28 | function resolve(dir) { 29 | return path.join(__dirname, '..', dir) 30 | } 31 | 32 | let rendererConfig = { 33 | devtool: '#cheap-module-eval-source-map', 34 | entry: { 35 | renderer: path.join(__dirname, '../src/renderer/main.js') 36 | }, 37 | externals: [ 38 | ...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d)) 39 | ], 40 | module: { 41 | rules: [{ 42 | test: /\.scss$/, 43 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 44 | }, 45 | { 46 | test: /\.sass$/, 47 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 48 | }, 49 | { 50 | test: /\.less$/, 51 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 52 | }, 53 | { 54 | test: /\.css$/, 55 | use: ['vue-style-loader', 'css-loader'] 56 | }, 57 | { 58 | test: /\.html$/, 59 | use: 'vue-html-loader' 60 | }, 61 | { 62 | test: /\.js$/, 63 | use: 'babel-loader', 64 | exclude: /node_modules/ 65 | }, 66 | { 67 | test: /\.node$/, 68 | use: 'node-loader' 69 | }, 70 | { 71 | test: /\.vue$/, 72 | use: { 73 | loader: 'vue-loader', 74 | options: { 75 | extractCSS: process.env.NODE_ENV === 'production', 76 | loaders: { 77 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 78 | scss: 'vue-style-loader!css-loader!sass-loader', 79 | less: 'vue-style-loader!css-loader!less-loader' 80 | } 81 | } 82 | } 83 | }, 84 | { 85 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 86 | use: { 87 | loader: 'url-loader', 88 | query: { 89 | limit: 10000, 90 | name: 'imgs/[name]--[folder].[ext]' 91 | } 92 | } 93 | }, 94 | { 95 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 96 | loader: 'url-loader', 97 | options: { 98 | limit: 10000, 99 | name: 'media/[name]--[folder].[ext]' 100 | } 101 | }, 102 | { 103 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 104 | use: { 105 | loader: 'url-loader', 106 | query: { 107 | limit: 10000, 108 | name: 'fonts/[name]--[folder].[ext]' 109 | } 110 | } 111 | }, 112 | { 113 | test: /\.tsx?$/, 114 | loader: 'ts-loader', 115 | exclude: /node_modules/, 116 | options: { 117 | appendTsSuffixTo: [/\.vue$/] 118 | } 119 | } 120 | ] 121 | }, 122 | node: { 123 | __dirname: process.env.NODE_ENV !== 'production', 124 | __filename: process.env.NODE_ENV !== 'production' 125 | }, 126 | plugins: [ 127 | new VueLoaderPlugin(), 128 | new MiniCssExtractPlugin({ 129 | filename: 'styles.css' 130 | }), 131 | new HtmlWebpackPlugin({ 132 | filename: 'index.html', 133 | template: path.resolve(__dirname, '../src/index.ejs'), 134 | templateParameters(compilation, assets, options) { 135 | return { 136 | compilation: compilation, 137 | webpack: compilation.getStats().toJson(), 138 | webpackConfig: compilation.options, 139 | htmlWebpackPlugin: { 140 | files: assets, 141 | options: options 142 | }, 143 | process, 144 | }; 145 | }, 146 | minify: { 147 | collapseWhitespace: true, 148 | removeAttributeQuotes: true, 149 | removeComments: true 150 | }, 151 | nodeModules: process.env.NODE_ENV !== 'production' ? 152 | path.resolve(__dirname, '../node_modules') : false 153 | }), 154 | new webpack.HotModuleReplacementPlugin(), 155 | new webpack.NoEmitOnErrorsPlugin() 156 | ], 157 | output: { 158 | filename: '[name].js', 159 | libraryTarget: 'commonjs2', 160 | path: path.join(__dirname, '../dist/electron') 161 | }, 162 | resolve: { 163 | alias: { 164 | '@': path.join(__dirname, '../src/renderer'), 165 | 'vue$': 'vue/dist/vue.esm.js' 166 | }, 167 | extensions: ['.ts', '.js', '.vue', '.json', '.css', '.node'] 168 | }, 169 | target: 'electron-renderer' 170 | } 171 | 172 | /** 173 | * Adjust rendererConfig for development settings 174 | */ 175 | if (process.env.NODE_ENV !== 'production') { 176 | rendererConfig.plugins.push( 177 | new webpack.DefinePlugin({ 178 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"` 179 | }) 180 | ) 181 | } 182 | 183 | /** 184 | * Adjust rendererConfig for production settings 185 | */ 186 | if (process.env.NODE_ENV === 'production') { 187 | rendererConfig.devtool = '' 188 | 189 | rendererConfig.plugins.push( 190 | new BabiliWebpackPlugin(), 191 | new CopyWebpackPlugin([{ 192 | from: path.join(__dirname, '../static'), 193 | to: path.join(__dirname, '../dist/electron/static'), 194 | ignore: ['.*'] 195 | }]), 196 | new webpack.DefinePlugin({ 197 | 'process.env.NODE_ENV': '"production"' 198 | }), 199 | new webpack.LoaderOptionsPlugin({ 200 | minimize: true 201 | }) 202 | ) 203 | } 204 | 205 | module.exports = rendererConfig -------------------------------------------------------------------------------- /.electron-vue/webpack.web.config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | process.env.BABEL_ENV = 'web' 4 | 5 | const path = require('path') 6 | const webpack = require('webpack') 7 | 8 | const BabiliWebpackPlugin = require('babili-webpack-plugin') 9 | const CopyWebpackPlugin = require('copy-webpack-plugin') 10 | const MiniCssExtractPlugin = require('mini-css-extract-plugin') 11 | const HtmlWebpackPlugin = require('html-webpack-plugin') 12 | const { VueLoaderPlugin } = require('vue-loader') 13 | 14 | let webConfig = { 15 | devtool: '#cheap-module-eval-source-map', 16 | entry: { 17 | web: path.join(__dirname, '../src/renderer/main.js') 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.scss$/, 23 | use: ['vue-style-loader', 'css-loader', 'sass-loader'] 24 | }, 25 | { 26 | test: /\.sass$/, 27 | use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax'] 28 | }, 29 | { 30 | test: /\.less$/, 31 | use: ['vue-style-loader', 'css-loader', 'less-loader'] 32 | }, 33 | { 34 | test: /\.css$/, 35 | use: ['vue-style-loader', 'css-loader'] 36 | }, 37 | { 38 | test: /\.html$/, 39 | use: 'vue-html-loader' 40 | }, 41 | { 42 | test: /\.js$/, 43 | use: 'babel-loader', 44 | include: [ path.resolve(__dirname, '../src/renderer') ], 45 | exclude: /node_modules/ 46 | }, 47 | { 48 | test: /\.vue$/, 49 | use: { 50 | loader: 'vue-loader', 51 | options: { 52 | extractCSS: true, 53 | loaders: { 54 | sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1', 55 | scss: 'vue-style-loader!css-loader!sass-loader', 56 | less: 'vue-style-loader!css-loader!less-loader' 57 | } 58 | } 59 | } 60 | }, 61 | { 62 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 63 | use: { 64 | loader: 'url-loader', 65 | query: { 66 | limit: 10000, 67 | name: 'imgs/[name].[ext]' 68 | } 69 | } 70 | }, 71 | { 72 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 73 | use: { 74 | loader: 'url-loader', 75 | query: { 76 | limit: 10000, 77 | name: 'fonts/[name].[ext]' 78 | } 79 | } 80 | } 81 | ] 82 | }, 83 | plugins: [ 84 | new VueLoaderPlugin(), 85 | new MiniCssExtractPlugin({filename: 'styles.css'}), 86 | new HtmlWebpackPlugin({ 87 | filename: 'index.html', 88 | template: path.resolve(__dirname, '../src/index.ejs'), 89 | templateParameters(compilation, assets, options) { 90 | return { 91 | compilation: compilation, 92 | webpack: compilation.getStats().toJson(), 93 | webpackConfig: compilation.options, 94 | htmlWebpackPlugin: { 95 | files: assets, 96 | options: options 97 | }, 98 | process, 99 | }; 100 | }, 101 | minify: { 102 | collapseWhitespace: true, 103 | removeAttributeQuotes: true, 104 | removeComments: true 105 | }, 106 | nodeModules: false 107 | }), 108 | new webpack.DefinePlugin({ 109 | 'process.env.IS_WEB': 'true' 110 | }), 111 | new webpack.HotModuleReplacementPlugin(), 112 | new webpack.NoEmitOnErrorsPlugin() 113 | ], 114 | output: { 115 | filename: '[name].js', 116 | path: path.join(__dirname, '../dist/web') 117 | }, 118 | resolve: { 119 | alias: { 120 | '@': path.join(__dirname, '../src/renderer'), 121 | 'vue$': 'vue/dist/vue.esm.js' 122 | }, 123 | extensions: ['.js', '.vue', '.json', '.css'] 124 | }, 125 | target: 'web' 126 | } 127 | 128 | /** 129 | * Adjust webConfig for production settings 130 | */ 131 | if (process.env.NODE_ENV === 'production') { 132 | webConfig.devtool = '' 133 | 134 | webConfig.plugins.push( 135 | new BabiliWebpackPlugin(), 136 | new CopyWebpackPlugin([ 137 | { 138 | from: path.join(__dirname, '../static'), 139 | to: path.join(__dirname, '../dist/web/static'), 140 | ignore: ['.*'] 141 | } 142 | ]), 143 | new webpack.DefinePlugin({ 144 | 'process.env.NODE_ENV': '"production"' 145 | }), 146 | new webpack.LoaderOptionsPlugin({ 147 | minimize: true 148 | }) 149 | ) 150 | } 151 | 152 | module.exports = webConfig 153 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dist/electron/* 3 | dist/web/* 4 | build/* 5 | !build/icons 6 | coverage 7 | node_modules/ 8 | npm-debug.log 9 | npm-debug.log.* 10 | thumbs.db 11 | !.gitkeep 12 | -------------------------------------------------------------------------------- /.jsbeautifyrc: -------------------------------------------------------------------------------- 1 | { 2 | "indent_with_tabs": false, 3 | "max_preserve_newlines": 2, 4 | "preserve_newlines": true, 5 | "keep_array_indentation": false, 6 | "break_chained_methods": false, 7 | "wrap_line_length": 200, 8 | "end_with_newline": true, 9 | "brace_style": "collapse,preserve-inline", 10 | "unformatted": ["a", "abbr", "area", "audio", "b", "bdi", "bdo", "br", "button", "canvas", "cite", "code", "data", 11 | "datalist", "del", "dfn", "em", "embed", "i", "iframe", "img", "input", "ins", "kbd", "keygen", "label", "map", 12 | "mark", "math", "meter", "noscript", "object", "output", "progress", "q", "ruby", "s", "samp", "select", "small", 13 | "span", "strong", "sub", "sup", "template", "textarea", "time", "u", "var", "video", "wbr", "text", "acronym", 14 | "address", "big", "dt", "ins", "small", "strike", "tt", "pre", "h1", "h2", "h3", "h4", "h5", "h6" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 120, 3 | parser: 'flow', 4 | tabWidth: 4, 5 | singleQuote: true 6 | }; 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | osx_image: xcode8.3 4 | sudo: required 5 | dist: trusty 6 | language: c 7 | matrix: 8 | include: 9 | - os: osx 10 | - os: linux 11 | env: CC=clang CXX=clang++ npm_config_clang=1 12 | compiler: clang 13 | cache: 14 | directories: 15 | - node_modules 16 | - "$HOME/.electron" 17 | - "$HOME/.cache" 18 | addons: 19 | apt: 20 | packages: 21 | - libgnome-keyring-dev 22 | - icnsutils 23 | #- xvfb 24 | before_install: 25 | - mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([ 26 | "$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz 27 | | tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull 28 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi 29 | install: 30 | #- export DISPLAY=':99.0' 31 | #- Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & 32 | - nvm install 7 33 | - curl -o- -L https://yarnpkg.com/install.sh | bash 34 | - source ~/.bashrc 35 | - npm install -g xvfb-maybe 36 | - yarn 37 | script: 38 | #- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js 39 | - yarn run build 40 | branches: 41 | only: 42 | - master 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Alex 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eHunter_local 2 | eHunter_local is local version of [eHunter](https://github.com/hanFengSan/eHunter),supporting for Windows and MacOS. It's a lightweight image reader for reading manga. 3 | 4 | ### Preview 5 | ![avatar](https://github.com/hanFengSan/eHunter_local/blob/master/github_image/001.png?raw=true) 6 | 7 | ![avatar](https://github.com/hanFengSan/eHunter_local/blob/master/github_image/002.png?raw=true) 8 | 9 | ![avatar](https://github.com/hanFengSan/eHunter_local/blob/master/github_image/003.png?raw=true) 10 | 11 | ### Download 12 | [Github Release](https://github.com/hanFengSan/eHunter_local/releases) 13 | 14 | [百度网盘](https://pan.baidu.com/s/1wEnBe9uGoBKzNd4DCfbuAg) 提取码: czft 15 | 16 | ### Build Setup 17 | 18 | ``` bash 19 | # install dependencies 20 | npm install 21 | 22 | # serve with hot reload at localhost:9080 23 | npm run dev 24 | 25 | # build electron application for production 26 | npm run build 27 | 28 | ``` 29 | 30 | --- 31 | 32 | This project was generated with [electron-vue](https://github.com/SimulatedGREG/electron-vue) using [vue-cli](https://github.com/vuejs/vue-cli). Documentation about the original structure can be found [here](https://simulatedgreg.gitbooks.io/electron-vue/content/index.html). 33 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Commented sections below can be used to run tests on the CI server 2 | # https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing 3 | version: 0.1.{build} 4 | 5 | branches: 6 | only: 7 | - master 8 | 9 | image: Visual Studio 2017 10 | platform: 11 | - x64 12 | 13 | cache: 14 | - node_modules 15 | - '%APPDATA%\npm-cache' 16 | - '%USERPROFILE%\.electron' 17 | - '%USERPROFILE%\AppData\Local\Yarn\cache' 18 | 19 | init: 20 | - git config --global core.autocrlf input 21 | 22 | install: 23 | - ps: Install-Product node 8 x64 24 | - git reset --hard HEAD 25 | - yarn 26 | - node --version 27 | 28 | build_script: 29 | #- yarn test 30 | - yarn build 31 | 32 | test: off 33 | -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon old.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/build/icons/icon old.ico -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/build/icons/icon.ico -------------------------------------------------------------------------------- /build/icons/icon.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/build/icons/icon.psd -------------------------------------------------------------------------------- /dist/electron/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/dist/electron/.gitkeep -------------------------------------------------------------------------------- /dist/web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/dist/web/.gitkeep -------------------------------------------------------------------------------- /github_image/001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/github_image/001.png -------------------------------------------------------------------------------- /github_image/002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/github_image/002.png -------------------------------------------------------------------------------- /github_image/003.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/github_image/003.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ehunter_local", 3 | "version": "1.1.0", 4 | "author": "Alex Chen ", 5 | "description": "ehunter", 6 | "license": "MIT", 7 | "main": "./dist/electron/main.js", 8 | "scripts": { 9 | "build": "node .electron-vue/build.js && electron-builder", 10 | "build:dir": "node .electron-vue/build.js && electron-builder --dir", 11 | "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", 12 | "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", 13 | "dev": "node .electron-vue/dev-runner.js", 14 | "pack": "npm run pack:main && npm run pack:renderer", 15 | "pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js", 16 | "pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js", 17 | "test": "npm run unit", 18 | "unit": "karma start test/unit/karma.conf.js", 19 | "postinstall": "" 20 | }, 21 | "build": { 22 | "productName": "eHunter-local", 23 | "appId": "info.alexskye.ehunter", 24 | "directories": { 25 | "output": "build" 26 | }, 27 | "files": [ 28 | "dist/electron/**/*" 29 | ], 30 | "dmg": { 31 | "contents": [ 32 | { 33 | "x": 410, 34 | "y": 150, 35 | "type": "link", 36 | "path": "/Applications" 37 | }, 38 | { 39 | "x": 130, 40 | "y": 150, 41 | "type": "file" 42 | } 43 | ] 44 | }, 45 | "mac": { 46 | "icon": "build/icons/icon.icns" 47 | }, 48 | "win": { 49 | "target": "portable", 50 | "icon": "build/icons/icon.ico" 51 | }, 52 | "linux": { 53 | "icon": "build/icons" 54 | } 55 | }, 56 | "dependencies": { 57 | "@babel/polyfill": "^7.4.3", 58 | "axios": "^0.18.0", 59 | "css.escape": "^1.5.1", 60 | "file-url": "^3.0.0", 61 | "image-size": "^0.7.3", 62 | "markdown-it": "^8.4.2", 63 | "markdown-it-emoji": "^1.4.0", 64 | "react-native-storage": "0.2.2", 65 | "ts-loader": "^5.3.3", 66 | "twemoji": "^12.0.1", 67 | "typescript": "^3.4.4", 68 | "vue": "^2.5.16", 69 | "vue-electron": "^1.0.6", 70 | "vue-router": "^3.0.1", 71 | "vuex": "^3.0.1", 72 | "vuex-electron": "^1.0.0" 73 | }, 74 | "devDependencies": { 75 | "ajv": "^6.5.0", 76 | "babel-core": "^6.26.3", 77 | "babel-loader": "^7.1.4", 78 | "babel-plugin-istanbul": "^4.1.6", 79 | "babel-plugin-transform-runtime": "^6.23.0", 80 | "babel-preset-env": "^1.7.0", 81 | "babel-preset-stage-0": "^6.24.1", 82 | "babel-register": "^6.26.0", 83 | "babili-webpack-plugin": "^0.1.2", 84 | "cfonts": "^2.1.2", 85 | "chai": "^4.1.2", 86 | "chalk": "^2.4.1", 87 | "copy-webpack-plugin": "^4.5.1", 88 | "cross-env": "^5.1.6", 89 | "css-loader": "^0.28.11", 90 | "del": "^3.0.0", 91 | "devtron": "^1.4.0", 92 | "electron": "^2.0.4", 93 | "electron-builder": "^21.2.0", 94 | "electron-debug": "^1.5.0", 95 | "electron-devtools-installer": "^2.2.4", 96 | "file-loader": "^1.1.11", 97 | "html-webpack-plugin": "^3.2.0", 98 | "inject-loader": "^4.0.1", 99 | "karma": "^2.0.2", 100 | "karma-chai": "^0.1.0", 101 | "karma-coverage": "^1.1.2", 102 | "karma-electron": "^6.0.0", 103 | "karma-mocha": "^1.3.0", 104 | "karma-sourcemap-loader": "^0.3.7", 105 | "karma-spec-reporter": "^0.0.32", 106 | "karma-webpack": "^3.0.0", 107 | "mini-css-extract-plugin": "0.4.0", 108 | "mocha": "^5.2.0", 109 | "multispinner": "^0.2.1", 110 | "node-loader": "^0.6.0", 111 | "node-sass": "^4.12.0", 112 | "sass-loader": "^7.1.0", 113 | "style-loader": "^0.21.0", 114 | "url-loader": "^1.0.1", 115 | "vue-html-loader": "^1.2.4", 116 | "vue-loader": "^15.2.4", 117 | "vue-style-loader": "^4.1.0", 118 | "vue-template-compiler": "^2.5.16", 119 | "webpack": "^4.15.1", 120 | "webpack-cli": "^3.0.8", 121 | "webpack-dev-server": "^3.1.4", 122 | "webpack-hot-middleware": "^2.22.2", 123 | "webpack-merge": "^4.1.3" 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | version: '1.1.0', 3 | homePage: 'https://github.com/hanFengSan/eHunter_local', 4 | email: 'c360785655@gmail.com', 5 | updateServer1: 'https://raw.githubusercontent.com/hanFengSan/eHunter_local/master/update.json', 6 | updateServer2: 'https://raw.githubusercontent.com/hanFengSan/eHunter_local/master/update.json' 7 | }; -------------------------------------------------------------------------------- /src/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | eHunter-local 6 | 17 | <% if (htmlWebpackPlugin.options.nodeModules) { %> 18 | 19 | 22 | 32 | <% } %> 33 | 34 | 35 |
36 | 37 | <% if (!process.browser) { %> 38 | 41 | <% } %> 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/bean/FileItem.ts: -------------------------------------------------------------------------------- 1 | export interface FileItem { 2 | name: string; 3 | path: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/main/index.dev.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is used specifically and only for development. It installs 3 | * `electron-debug` & `vue-devtools`. There shouldn't be any need to 4 | * modify this file, but it can be used to extend your development 5 | * environment. 6 | */ 7 | 8 | /* eslint-disable */ 9 | 10 | // Install `electron-debug` with `devtron` 11 | require('electron-debug')({ showDevTools: true }) 12 | 13 | // Install `vue-devtools` 14 | require('electron').app.on('ready', () => { 15 | let installExtension = require('electron-devtools-installer') 16 | installExtension.default(installExtension.VUEJS_DEVTOOLS) 17 | .then(() => {}) 18 | .catch(err => { 19 | console.log('Unable to install `vue-devtools`: \n', err) 20 | }) 21 | }) 22 | 23 | // Require `main` process to boot app 24 | require('./index') -------------------------------------------------------------------------------- /src/main/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | app, 3 | BrowserWindow, 4 | ipcMain, 5 | shell 6 | } from 'electron' 7 | 8 | import { 9 | AlbumDirSorter 10 | } from './service/AlbumDirSorter.ts' 11 | 12 | import { 13 | AlbumServiceImpl 14 | } from './service/AlbumServiceImpl.ts' 15 | 16 | /** 17 | * Set `__static` path to static files in production 18 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html 19 | */ 20 | if (process.env.NODE_ENV !== 'development') { 21 | global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') 22 | } 23 | 24 | let mainWindow 25 | const winURL = process.env.NODE_ENV === 'development' ? 26 | `http://localhost:9080` : 27 | `file://${__dirname}/index.html` 28 | 29 | function createWindow() { 30 | /** 31 | * Initial window options 32 | */ 33 | mainWindow = new BrowserWindow({ 34 | title: 'eHunter-local', 35 | height: 763, 36 | useContentSize: true, 37 | width: 1200, 38 | minWidth: 400, 39 | minHeight: 300, 40 | backgroundColor: '#333333', 41 | webPreferences: { 42 | webSecurity: false 43 | } 44 | }) 45 | 46 | mainWindow.loadURL(winURL) 47 | 48 | mainWindow.on('closed', () => { 49 | mainWindow = null 50 | }) 51 | 52 | // Hook new window, and open url in Browser 53 | mainWindow.webContents.on('new-window', function(event, url) { 54 | event.preventDefault(); 55 | shell.openExternal(url); 56 | }); 57 | } 58 | 59 | app.on('ready', createWindow) 60 | 61 | app.on('window-all-closed', () => { 62 | if (process.platform !== 'darwin') { 63 | app.quit() 64 | } 65 | }) 66 | 67 | app.on('activate', () => { 68 | if (mainWindow === null) { 69 | createWindow() 70 | } 71 | }) 72 | 73 | ipcMain.on('SELECT_ALBUM_DIR', async (event, path) => { 74 | try { 75 | let fileItems = await (new AlbumDirSorter(path)).sort(); 76 | let albumServiceImpl = new AlbumServiceImpl(); 77 | await albumServiceImpl.parseFileItems(path, fileItems); 78 | event.sender.send('ALBUM_DATA', albumServiceImpl); 79 | } catch (err) { 80 | event.sender.send('ERROR', err.message); 81 | } 82 | }); 83 | 84 | ipcMain.on('OPEN_HOME', async (event, path) => { 85 | try { 86 | let fileItems = await (new AlbumDirSorter(path)).sort(); 87 | let albumServiceImpl = new AlbumServiceImpl(); 88 | await albumServiceImpl.parseFileItems(path, fileItems); 89 | event.sender.send('ALBUM_DATA', albumServiceImpl); 90 | } catch (err) { 91 | event.sender.send('ERROR', err.message); 92 | } 93 | }); 94 | 95 | /** 96 | * Auto Updater 97 | * 98 | * Uncomment the following code below and install `electron-updater` to 99 | * support auto updating. Code Signing with a valid certificate is required. 100 | * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating 101 | */ 102 | 103 | /* 104 | import { autoUpdater } from 'electron-updater' 105 | 106 | autoUpdater.on('update-downloaded', () => { 107 | autoUpdater.quitAndInstall() 108 | }) 109 | 110 | app.on('ready', () => { 111 | if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates() 112 | }) 113 | */ 114 | -------------------------------------------------------------------------------- /src/main/service/AlbumDirSorter.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "fs"; 2 | import { FileItem } from "../bean/FileItem"; 3 | 4 | const util = require('util'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const stat = util.promisify(fs.stat); 9 | const readdir = util.promisify(fs.readdir); 10 | 11 | export class AlbumDirSorter { 12 | private dirPath: string; 13 | private stats: Stats | undefined; 14 | 15 | constructor(path: string) { 16 | this.dirPath = path; 17 | } 18 | 19 | async sort(): Promise> { 20 | this.stats = await stat(this.dirPath); 21 | if (this.noDir()) 22 | throw new Error('ERROR_NO_DIR'); 23 | let fileItems = await this.getImgsFromDir(); 24 | if (fileItems.length === 0) 25 | throw new Error('NO_IMG'); 26 | var collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }); 27 | fileItems.sort((a, b) => { 28 | return collator.compare(a.name, b.name); 29 | }); 30 | return fileItems; 31 | } 32 | 33 | 34 | private noDir(): boolean { 35 | return !this.stats!.isDirectory(); 36 | } 37 | 38 | private async getImgsFromDir(): Promise> { 39 | let files = await readdir(this.dirPath); 40 | let fileItems = files.filter(i => /(\.jpg|\.png)$/i.test(i)).map(i => { 41 | return { 42 | name: i, 43 | path: path.join(this.dirPath, i) 44 | } 45 | }) 46 | return fileItems; 47 | } 48 | } -------------------------------------------------------------------------------- /src/main/service/AlbumServiceImpl.ts: -------------------------------------------------------------------------------- 1 | import { AlbumService, PreviewThumbnailStyle } from '../../renderer/service/AlbumService'; 2 | import { FileItem } from '../bean/FileItem'; 3 | import { ImgPageInfo } from '../../renderer/bean/ImgPageInfo'; 4 | import { ThumbInfo, ThumbMode } from '../../renderer/bean/ThumbInfo'; 5 | 6 | const util = require('util'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | const ImageSize = require('image-size'); 10 | 11 | const stat = util.promisify(fs.stat); 12 | const readdir = util.promisify(fs.readdir); 13 | const sizeOf = util.promisify(ImageSize); 14 | const fileUrl = require('file-url'); 15 | const CSSEscape = require('css.escape'); 16 | 17 | export class AlbumServiceImpl extends AlbumService { 18 | private imgPageInfos: Array = []; 19 | private thumbInfos: Array = []; 20 | private title: string = ''; 21 | 22 | constructor() { 23 | super(); 24 | } 25 | 26 | static fromJSON(o: Object): AlbumServiceImpl { 27 | return Object.assign(new AlbumServiceImpl(), o); 28 | } 29 | 30 | async parseFileItems(dirPath: string, fileItems: Array): Promise { 31 | this.imgPageInfos = []; 32 | this.thumbInfos = []; 33 | for (let i = 0; i < fileItems.length; i++) { 34 | let id = fileItems[i].name; 35 | let index = i; 36 | let pageUrl = fileItems[i].path; 37 | let src = fileUrl(fileItems[i].path); 38 | let dimensions = await sizeOf(fileItems[i].path); 39 | let heightOfWidth = dimensions.height / dimensions.width; 40 | this.imgPageInfos.push({ id, index, pageUrl, src, heightOfWidth }); 41 | this.thumbInfos.push({ id, src: CSSEscape(src), mode: ThumbMode.IMG }); 42 | } 43 | this.title = path.basename(dirPath); 44 | } 45 | 46 | async getPageCount(): Promise { 47 | return this.imgPageInfos.length; 48 | } 49 | 50 | async getCurPageNum(): Promise { 51 | return 0; 52 | } 53 | 54 | async getTitle(): Promise { 55 | return this.title; 56 | } 57 | 58 | async getImgPageInfos(): Promise> { 59 | return this.imgPageInfos; 60 | } 61 | 62 | async getImgPageInfo(index: number): Promise { 63 | return this.imgPageInfos[index]; 64 | } 65 | 66 | async getImgSrc(index: number, mode): Promise { 67 | return this.imgPageInfos[index].src; 68 | } 69 | 70 | async getNewImgSrc(index: number, mode): Promise { 71 | return this.imgPageInfos[index].src; 72 | } 73 | async getThumbInfos(noCache?: boolean): Promise> { 74 | return this.thumbInfos; 75 | } 76 | 77 | async getThumbInfo(index: number): Promise { 78 | return this.thumbInfos[index]; 79 | } 80 | 81 | async getAlbumId(): Promise { 82 | return await this.getTitle(); 83 | } 84 | 85 | async getPreviewThumbnailStyle(index: number, imgPageInfo: ImgPageInfo, thumbInfo: ThumbInfo): Promise { 86 | return { 87 | 'background-image': '', 88 | 'background-position': '', 89 | 'background-size': '' 90 | }; 91 | } 92 | 93 | supportOriginImg(): boolean { 94 | return false; 95 | } 96 | supportImgChangeSource(): boolean { 97 | return false; 98 | } 99 | 100 | supportThumbView(): boolean { 101 | return true; 102 | } 103 | } -------------------------------------------------------------------------------- /src/main/service/DirBrowser.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "fs"; 2 | import { FileItem } from "../bean/FileItem"; 3 | 4 | const util = require('util'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | const stat = util.promisify(fs.stat); 9 | const readdir = util.promisify(fs.readdir); 10 | 11 | export class DirBrowser { 12 | private dirPath: string; 13 | private stats: Stats | undefined; 14 | 15 | constructor(path: string) { 16 | this.dirPath = path; 17 | } 18 | 19 | async sort(): Promise> { 20 | this.stats = await stat(this.dirPath); 21 | if (this.noDir()) 22 | throw new Error('ERROR_NO_DIR'); 23 | let fileItems = await this.getImgsFromDir(); 24 | if (fileItems.length === 0) 25 | throw new Error('NO_IMG'); 26 | fileItems = this.sortByNum(fileItems); 27 | return fileItems; 28 | } 29 | 30 | 31 | private noDir(): boolean { 32 | return !this.stats!.isDirectory(); 33 | } 34 | 35 | private async getImgsFromDir(): Promise> { 36 | let files = await readdir(this.dirPath); 37 | let fileItems = files.filter(i => /(\.jpg|\.png)$/i.test(i)).map(i => { 38 | return { 39 | name: i, 40 | path: path.join(this.dirPath, i) 41 | } 42 | }) 43 | return fileItems; 44 | } 45 | 46 | private sortByNum(fileItems: Array): Array { 47 | let numTailList: Array = []; 48 | let numHeadList: Array = []; 49 | let otherList: Array = []; 50 | fileItems.forEach(i => { 51 | if (/^.*?\d+?(\.jpg|\.png)$/i.test(i.name)) { 52 | numTailList.push(i); 53 | } else if (/^\d+?.*?(\.jpg|\.png)$/i.test(i.name)) { 54 | numHeadList.push(i); 55 | } else { 56 | otherList.push(i); 57 | } 58 | }); 59 | numTailList.sort((a, b) => { 60 | let reg = /^.*?(\d+?)(\.jpg|\.png)$/i; 61 | a.name.match(reg) 62 | let num1 = Number(RegExp.$1); 63 | b.name.match(reg) 64 | let num2 = Number(RegExp.$1); 65 | return num1 - num2; 66 | }); 67 | numHeadList.sort((a, b) => { 68 | let reg = /^(\d+?).*?(\.jpg|\.png)$/i; 69 | a.name.match(reg) 70 | let num1 = Number(RegExp.$1); 71 | b.name.match(reg) 72 | let num2 = Number(RegExp.$1); 73 | return num1 - num2; 74 | }); 75 | return numTailList.concat(numHeadList, otherList); 76 | } 77 | } -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 28 | -------------------------------------------------------------------------------- /src/renderer/Home.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 118 | 119 | 224 | -------------------------------------------------------------------------------- /src/renderer/assets/value/bookInstruction.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cn: ` 3 | 支持\`A\`. \`D\`, \`Left(左)\`, \`Right(右)\`和\`Space(空格)\`键翻页. 4 | `, 5 | en: ` 6 | You can use the keyboard's \`A\`, \`D\`, \`Left\`, \`Right\` and \`Space\` keys to page. 7 | `, 8 | jp: ` 9 | You can use the keyboard's \`A\`, \`D\`, \`Left\`, \`Right\` and \`Space\` keys to page. 10 | ` 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/assets/value/instruction.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cn: ` 3 | 1.Change language/切换语言/言語を変更 4 | ![image-language](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/language.jpg) 5 | 6 | 2.显示/隐藏顶栏和关闭eHunter 7 | ![image-topbar_close](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/topbar_close.jpg) 8 | 9 | 3.\`滚动\`模式下, 支持\`A\`. \`D\`, \`Left(左)\`和\`Right(右)\`键翻页. 10 | 11 | 4.\`书页\`模式下, 支持\`A\`. \`D\`, \`Left(左)\`, \`Right(右)\`和\`Space(空格)\`键翻页. 12 | 13 | 5.\`分卷页数\`对性能要求较高,请不要设置过高,可能会导致卡顿. 14 | 15 | 6.如果图片很多, 可以关闭\`缩略图\`以提升性能. 16 | 17 | 7.有更多想要的功能, 可以反馈给我, 如果该功能可以有的话, 我有空的时候会支持的. 18 | 19 | ### eHunter 20 | eHunter-local是eHunter的本地版本. eHunter本身是浏览器插件, 支持ehentai和nhentai. [项目主页](https://github.com/hanfengSan/eHunter) 21 | 22 | [chrome商店安装](https://chrome.google.com/webstore/detail/ehunter-more-powerful-e-h/dnnicnedpmjkbkdeijccbjkkcpcbmdoo) [油猴安装](https://greasyfork.org/zh-CN/scripts/39198-ehunter) [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/ehunter/) [crx/xpi安装](https://pan.baidu.com/s/1YTONumJwAAHMPpUfcyqkhg) 23 | 24 | ### 反馈和建议 25 | * 可在[Github]($$HOME_PAGE$$)上开issue给我. 26 | * 可发邮件到我邮箱: c360785655@gmail.com 27 | 28 | ### 关于 29 | * 版本: $$VERSION$$ 30 | * 作者: Alex Chen (hanFeng) 31 | * 项目开源地址: [Github]($$HOME_PAGE$$) 32 | `, 33 | en: ` 34 | 1.Change language/切换语言/言語を変更 35 | ![image-language](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/language.jpg) 36 | 37 | 2.Show/hide top bar and close the eHunter 38 | ![image-topbar_close](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/topbar_close.jpg) 39 | 40 | 3.When the \`Mode\` is \`Scroll\`, you can use the keyboard's \`A\`, \`D\`, \`Left\` and \`Right\` keys to page. 41 | 42 | 4.When the \`Mode\` is \`Book\`, you can use the keyboard's \`A\`, \`D\`, \`Left\`, \`Right\` and \`Space\` keys to page. 43 | 44 | 5.This is a high performance requirements on \`Volume size\`. If too big, the program will be slow. 45 | 46 | 6.If there are many images, you can turn off \`Thumbnail\` to improve performance. 47 | 48 | 7.If you want EHunter to support more features, you can give me feedback. 49 | 50 | ### eHunter 51 | The eHunter-local is local version of eHunter. The eHunter is browser plugin, supporting ehentai and nhentai. [Home Page](https://github.com/hanfengSan/eHunter) 52 | 53 | [Chrome Web Store](https://chrome.google.com/webstore/detail/ehunter-more-powerful-e-h/dnnicnedpmjkbkdeijccbjkkcpcbmdoo) [Tampermonkey](https://greasyfork.org/zh-CN/scripts/39198-ehunter) [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/ehunter/) 54 | 55 | ### Feedback & Suggestion 56 | * Create issue on [Github]($$HOME_PAGE$$) to me. 57 | * Send email to c360785655@gmail.com 58 | 59 | ### About 60 | * Version: $$VERSION$$ 61 | * Author: Alex Chen (hanFeng) 62 | * Home page of this project: [Github]($$HOME_PAGE$$) 63 | `, 64 | jp: ` 65 | 1.Change language/切换语言/言語を変更 66 | ![image-language](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/language.jpg) 67 | 68 | 2.トップバーを表示/非表示にしてeHunterを閉じる 69 | ![image-topbar_close](https://raw.githubusercontent.com/hanFengSan/eHunter/master/github_image/topbar_close.jpg) 70 | 71 | 3.When the \`Mode\` is \`Scroll\`, you can use the keyboard's \`A\`, \`D\`, \`Left\` and \`Right\` keys to page. 72 | 73 | 4.When the \`Mode\` is \`Book\`, you can use the keyboard's \`A\`, \`D\`, \`Left\`, \`Right\` and \`Space\` keys to page. 74 | 75 | 5.これは\`ボリュームサイズ\`の高性能要件です。 大きすぎるとプログラムが遅くなります。 76 | 77 | 6.画像が多い場合は、パフォーマンスを向上させるために "サムネイル"をオフにすることができます。 78 | 79 | 7.あなたがEHunterにもっと多くの機能をサポートさせたいならば、あなたは私にフィードバックを与えることができます。 80 | 81 | ### eHunter 82 | eHunter-localはeHunterのローカル版です。eHunterは、ehentaiとnhentaiをサポートするブラウザプラグインです。 [Home Page](https://github.com/hanfengSan/eHunter) 83 | 84 | [Chrome Web Store](https://chrome.google.com/webstore/detail/ehunter-more-powerful-e-h/dnnicnedpmjkbkdeijccbjkkcpcbmdoo) [Tampermonkey](https://greasyfork.org/zh-CN/scripts/39198-ehunter) [Firefox](https://addons.mozilla.org/zh-CN/firefox/addon/ehunter/) 85 | 86 | ### フィードバックと提案 87 | * 私にGITHUBのオープンな問題 [Github]($$HOME_PAGE$$) 88 | * c360785655@gmail.comにメールを送信する 89 | 90 | ### 〜について 91 | * バージョン: $$VERSION$$ 92 | * 著者: Alex Chen (hanFeng) 93 | * このプロジェクトのホームページ: [Github]($$HOME_PAGE$$) 94 | ` 95 | } 96 | -------------------------------------------------------------------------------- /src/renderer/assets/value/string.js: -------------------------------------------------------------------------------- 1 | export default { 2 | lang: { 3 | cn: 'CN', 4 | en: 'EN', 5 | jp: 'JP' 6 | }, 7 | dropDir: { 8 | cn: '拖拽文件夹到此处', 9 | en: 'Drop folder to here', 10 | jp: 'ここにフォルダをドロップ' 11 | }, 12 | openDir: { 13 | cn: '选择文件夹', 14 | en: 'OPEN FOLDER', 15 | jp: '開いたフォルダ' 16 | }, 17 | noDir: { 18 | cn: '不是文件夹', 19 | en: 'It isn\'t folder', 20 | jp: 'フォルダではありません' 21 | }, 22 | noImg: { 23 | cn: '文件夹中不存在图片(*.jpg, *.png)', 24 | en: 'No image exists in the folder(*.jpg, *.png)', 25 | jp: 'フォルダに画像が存在しません' 26 | }, 27 | error: { 28 | cn: '错误', 29 | en: 'Error', 30 | jp: 'エラー' 31 | }, 32 | readingMode: { 33 | cn: '阅读模式', 34 | en: 'Mode', 35 | jp: 'モード' 36 | }, 37 | readingModeTip: { 38 | cn: '设置阅读模式', 39 | en: 'Change reading mode', 40 | jp: '読むモードを変更する' 41 | }, 42 | scrollMode: { 43 | cn: '滚动', 44 | en: 'Scroll', 45 | jp: 'スクロール' 46 | }, 47 | bookMode: { 48 | cn: '书页', 49 | en: 'Book', 50 | jp: 'ページ' 51 | }, 52 | widthScale: { 53 | cn: '页面比例', 54 | en: 'Page scale', 55 | jp: 'ページの割合' 56 | }, 57 | widthScaleTip: { 58 | cn: '设置页面比例', 59 | en: 'Change page scale', 60 | jp: 'ページの割合を設定' 61 | }, 62 | custom: { 63 | cn: '自定义', 64 | en: 'Custom', 65 | jp: 'カスタム' 66 | }, 67 | loadNum: { 68 | cn: '加载页数', 69 | en: 'Loading quantity', 70 | jp: '積載量' 71 | }, 72 | loadNumTip: { 73 | cn: '越大则内存占用越高', 74 | en: 'The greater quantity, the higher usage of Memory', 75 | jp: '量が多いほど、メモリの使用量は多くなります' 76 | }, 77 | volSize: { 78 | cn: '分卷页数', 79 | en: 'Volume size', 80 | jp: 'ボリュームサイズ' 81 | }, 82 | volSizeTip: { 83 | cn: '设置过大会导致卡顿', 84 | en: 'If too big, the program will be slow', 85 | jp: '大きすぎると、プログラムは遅くなります' 86 | }, 87 | thumbView: { 88 | cn: '缩略图栏', 89 | en: 'Thumbnail', 90 | jp: 'サムネイル' 91 | }, 92 | thumbViewTip: { 93 | cn: '开启/关闭缩略图栏', 94 | en: 'Show/hide the column of thumbnail', 95 | jp: 'サムネイルの列を表示または非表示' 96 | }, 97 | screenSize: { 98 | cn: '同屏页数', 99 | en: 'Pages/screen', 100 | jp: 'ページ/画面' 101 | }, 102 | screenSizeTip: { 103 | cn: '一个屏幕下的页数', 104 | en: 'The number of pages on the screen', 105 | jp: '画面上のページ数' 106 | }, 107 | bookDirection: { 108 | cn: '阅读方向', 109 | en: 'Direction', 110 | jp: '読み取り方向' 111 | }, 112 | bookDirectionTip: { 113 | cn: '阅读方向', 114 | en: 'Reading direction', 115 | jp: '読み取り方向' 116 | }, 117 | rtl: { 118 | cn: 'RTL (从右到左)', 119 | en: 'RTL (Right To Left)', 120 | jp: 'RTL (右から左に)' 121 | }, 122 | ltr: { 123 | cn: 'LTR (从左到右)', 124 | en: 'LTR (Left to Right)', 125 | jp: 'LTR (左から右へ)' 126 | }, 127 | pagination: { 128 | cn: '页目录', 129 | en: 'Pagination', 130 | jp: 'ページネーション' 131 | }, 132 | paginationTip: { 133 | cn: '显示/隐藏底部悬浮页目录', 134 | en: 'Show/hide the bottom floating pagination', 135 | jp: 'ボトムフローティングページネーションの表示/非表示' 136 | }, 137 | bookAnimation: { 138 | cn: '换页动画', 139 | en: 'Sliding animation', 140 | jp: 'アニメーション' 141 | }, 142 | bookAnimationTip: { 143 | cn: '开启/关闭换页时的滑动动画(测试中)', 144 | en: 'show/hide the sliding animation when changing location(Beta)', 145 | jp: '場所を変更するときのスライドアニメーションの表示/非表示(测试中)' 146 | }, 147 | reverseFlip: { 148 | cn: '反转翻页', 149 | en: 'Reverse flip', 150 | jp: '反転フリップ' 151 | }, 152 | reverseFlipTip: { 153 | cn: '反转翻页方向', 154 | en: 'Reverse page turning direction', 155 | jp: 'ページめくり方向を逆にする' 156 | }, 157 | autoFlip: { 158 | cn: '自动翻页', 159 | en: 'Auto', 160 | jp: '自動ページめくり' 161 | }, 162 | autoFlipTip: { 163 | cn: '自动翻页', 164 | en: 'Automatic page turning', 165 | jp: '自動ページめくり' 166 | }, 167 | autoFlipFrequency: { 168 | cn: '翻页频率', 169 | en: 'Frequency', 170 | jp: '頻度' 171 | }, 172 | autoFlipFrequencyTip: { 173 | cn: '自动翻页的频率', 174 | en: 'Automatic page turning frequency', 175 | jp: '自動ページめくり頻度' 176 | }, 177 | refresh: { 178 | cn: '刷新', 179 | en: 'Refresh', 180 | jp: 'リフレッシュ' 181 | }, 182 | refreshTip: { 183 | cn: '再次获取普通图片', 184 | en: 'Refresh to load normal image', 185 | jp: 'リフレッシュ; 通常の画像を読み込みます' 186 | }, 187 | originImg: { 188 | cn: '原图', 189 | en: 'Original', 190 | jp: '元画像' 191 | }, 192 | originImgTip: { 193 | cn: '加载原图', 194 | en: 'Load original image', 195 | jp: '元画像を読み込む' 196 | }, 197 | refreshByOtherSource: { 198 | cn: '换源刷新', 199 | en: 'Other source', 200 | jp: '他のサーバー' 201 | }, 202 | refreshByOtherSourceTip: { 203 | cn: '从其他服务器获取普通图片', 204 | en: 'Load normal image from other server', 205 | jp: '他のサーバーから通常の画像を取得する' 206 | }, 207 | loadingImg: { 208 | cn: '加载图片中...', 209 | en: 'Loading image...', 210 | jp: '画像を読み込む..' 211 | }, 212 | loadingImgUrl: { 213 | cn: '加载图片地址中...', 214 | en: 'Loading image url..', 215 | jp: '画像URLを読み込む..' 216 | }, 217 | reload: { 218 | cn: '重载', 219 | en: 'Reload', 220 | jp: 'リロード' 221 | }, 222 | loadingImgFailed: { 223 | cn: '加载图片失败, 请刷新', 224 | en: 'Loading failed, please refresh', 225 | jp: '読み込みに失敗しました。更新してください' 226 | }, 227 | noOriginalImg: { 228 | cn: '无原图, 请刷新', 229 | en: 'No original Image, please refresh', 230 | jp: 'オリジナルイメージはありません。リフレッシュしてください' 231 | }, 232 | loadingFailed: { 233 | cn: '加载错误', 234 | en: 'Loading failed', 235 | jp: '読み込み失敗' 236 | }, 237 | imgLoaded: { 238 | cn: '图片加载完成', 239 | en: 'Image loaded', 240 | jp: '画像が読み込まれた' 241 | }, 242 | waiting: { 243 | cn: '等待中..', 244 | en: 'Waiting..', 245 | jp: '待っている..' 246 | }, 247 | fullScreen: { 248 | cn: '全屏', 249 | en: 'Full screen', 250 | jp: '全画面表示' 251 | }, 252 | closeEHunter: { 253 | cn: '返回 [Q]', 254 | en: 'Return [Q]', 255 | jp: '返す [Q]' 256 | }, 257 | toggleTopBar: { 258 | cn: '显示/隐藏顶栏 [Esc]', 259 | en: 'Show/hide top bar [Esc]', 260 | jp: 'トップバーの表示/非表示 [Esc]' 261 | }, 262 | toggleMoreSettings: { 263 | cn: '显示/隐藏更多设置 [Shift]', 264 | en: 'Show/hide more settings [Shift]', 265 | jp: '他の設定を表示/隠す [Shift]' 266 | }, 267 | confirm: { 268 | cn: '确定', 269 | en: 'CONFIRM', 270 | jp: '確認' 271 | }, 272 | cancel: { 273 | cn: '取消', 274 | en: 'CANCEL', 275 | jp: '取り消し' 276 | }, 277 | infoTip: { 278 | cn: '查看说明和关于', 279 | en: 'Look the Instructions and About', 280 | jp: '指示と情報を見てください' 281 | }, 282 | resetTip: { 283 | cn: '重置缓存和数据', 284 | en: 'Reset cache and data', 285 | jp: 'Reset cache and data' 286 | }, 287 | githubTip: { 288 | cn: '前往项目主页(Github)', 289 | en: 'Go to the project home page(Github)', 290 | jp: 'プロジェクトのホームページに行く(Github)' 291 | }, 292 | instructionsAndAbouts: { 293 | cn: '说明和关于', 294 | en: 'Instructions & About', 295 | jp: '説明と概要' 296 | }, 297 | instructions: { 298 | cn: '说明', 299 | en: 'Instructions', 300 | jp: '説明' 301 | }, 302 | later: { 303 | cn: '以后再说', 304 | en: 'LATER', 305 | jp: '後で' 306 | }, 307 | changingToSmallFailed: { 308 | cn: '无缝切换至`"Normal"`模式失败,可能是网络错误,可刷新重试或者返回前一页将预览图的大小模式切换为`"Normal"`。', 309 | en: 'Changing to `"Normal"` mode failed, because of poor network. You can reload this page or go back to previous page and change the mode of thumbnails to `"Normal"`', 310 | jp: 'ネットワークが不十分であるため、`「Normal」`モードに変更できませんでした。 このページをリロードするか、前のページに戻ってサムネイルのモードを`「Normal」`に変更することができます' 311 | }, 312 | loadingTip: { 313 | cn: '在前页采用Normal模式查看缩略图可加速加载', 314 | en: 'You can use "Normal" mode of thumbnail in previous page to accelerate the load.', 315 | jp: '前のページでサムネイルの「Normal」モードを使用して、読み込みを高速化できます。' 316 | }, 317 | versionUpdate: { 318 | cn: '版本更新说明', 319 | en: 'The update of this version', 320 | jp: 'このバージョンの更新' 321 | }, 322 | loadingFailedAndRefresh: { 323 | cn: '加载错误, 刷新重试', 324 | en: 'Loading failed, please refresh to retry', 325 | jp: '読み込みに失敗しました。もう一度試してください' 326 | }, 327 | failedMsg: { 328 | cn: '错误信息', 329 | en: 'Error message', 330 | jp: 'エラーメッセージ' 331 | }, 332 | version: { 333 | cn: '版本', 334 | en: 'Version', 335 | jp: 'Version' 336 | }, 337 | ContractAuthor: { 338 | cn: '联系作者', 339 | en: 'Contact author', 340 | jp: '作者に連絡する' 341 | }, 342 | wheelSensitivity: { 343 | cn: '滚轮翻页', 344 | en: 'Wheel flip', 345 | jp: 'ホイール' 346 | }, 347 | wheelSensitivityTip: { 348 | cn: '鼠标滚轮翻页灵敏度', 349 | en: 'Wheel sensitivity', 350 | jp: 'ホイール感度' 351 | }, 352 | wheelDirection: { 353 | cn: '滚轮方向', 354 | en: 'Wheel Direction', 355 | jp: 'ホイール方向' 356 | }, 357 | wheelDirectionTip: { 358 | cn: '反转滚轮翻页方向', 359 | en: 'Reverse Wheel Direction to flip', 360 | jp: 'リバースホイール方向' 361 | }, 362 | tips: { 363 | cn: '提示', 364 | en: 'TIPS', 365 | jp: 'ヒント' 366 | }, 367 | numberInputTip: { 368 | cn: '最小值为`{{min}}`, 最大值为`{{max}}`', 369 | en: 'The minimum is `{{min}}` and the maximum is `{{max}}`', 370 | jp: '最小は`{{min}}`, 最大は`{{max}}`です' 371 | }, 372 | pageMargin: { 373 | cn: '页间隔', 374 | en: 'Page spacing', 375 | jp: 'ページ間隔' 376 | }, 377 | pageMarginTip: { 378 | cn: '页间隔', 379 | en: 'Page spacing', 380 | jp: 'ページ間隔' 381 | }, 382 | oddEven: { 383 | cn: '奇偶切换', 384 | en: 'Odd/Even', 385 | jp: '奇/偶' 386 | }, 387 | oddEvenTip: { 388 | cn: '切换奇偶页拼接', 389 | en: 'Switching odd or even page stitching', 390 | jp: '奇数または偶数ページステッチの切り替え' 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/renderer/assets/value/tags.js: -------------------------------------------------------------------------------- 1 | export const SCROLL_VIEW = 'SCROLL_VIEW'; 2 | export const SCROLL_VIEW_VOL = 'SCROLL_VIEW_VOL'; 3 | export const BOOK_VIEW = 'BOOK_VIEW'; 4 | export const THUMB_VIEW = 'THUMB_VIEW'; 5 | export const READER_VIEW = 'READER_VIEW'; 6 | export const TOP_BAR = 'TOP_BAR'; 7 | 8 | export const MODE_FAST = 'MODE_FAST'; 9 | export const MODE_ORIGIN = 'MODE_ORIGIN'; 10 | export const MODE_CHANGE_SOURCE = 'MODE_CHANGE_SOURCE'; 11 | 12 | export const ERROR_NO_ORIGIN = 'ERROR_NO_ORIGIN'; 13 | 14 | export const ID_START = 'ID_START'; 15 | export const ID_END = 'ID_END'; 16 | export const TYPE_NORMAL = 'TYPE_NORMAL'; 17 | export const TYPE_START = 'TYPE_START'; 18 | export const TYPE_END = 'TYPE_END'; 19 | 20 | export const LANG_EN = 'LANG_EN'; 21 | export const LANG_CN = 'LANG_CN'; 22 | export const LANG_JP = 'LANG_JP'; 23 | 24 | export const STATE_WAITING = 'STATE_WAITING'; 25 | export const STATE_LOADING = 'STATE_LOADING'; 26 | export const STATE_ERROR = 'STATE_ERROR'; 27 | export const STATE_LOADED = 'STATE_LOADED'; 28 | 29 | export const DIALOG_NORMAL = 'DIALOG_NORMAL'; 30 | export const DIALOG_COMPULSIVE = 'DIALOG_COMPULSIVE'; 31 | export const DIALOG_OPERATION_TYPE_PLAIN = 'DIALOG_OPERATION_PLAIN'; 32 | export const DIALOG_OPERATION_TYPE_NEGATIVE = 'DIALOG_OPERATION_TYPE_NEGATIVE'; 33 | export const DIALOG_OPERATION_TYPE_POSITIVE = 'DIALOG_OPERATION_TYPE_POSITIVE'; 34 | export const DIALOG_OPERATION_TYPE_WARNING = 'DIALOG_OPERATION_TYPE_WARNING'; 35 | 36 | export const TYPE_PROXY = 'TYPE_PROXY'; 37 | 38 | export const KEYBOARD = 'KEYBOARD'; 39 | -------------------------------------------------------------------------------- /src/renderer/assets/value/version.js: -------------------------------------------------------------------------------- 1 | export default { 2 | cn: ` 3 | * \`书页模式\`下支持奇偶页切换 4 | * 支持回车关闭弹窗 5 | * 迁移老的订阅数据 6 | * \`滚动模式\`下支持调整页间距 7 | * 支持手动修改配置值 8 | * 在\`书页模式\`下, 支持鼠标滚轮(触控板)翻页. 9 | * 添加\`滚轮方向\`设置, 用于设置鼠标滚动翻页方向. 10 | `, 11 | en: ` 12 | * In \`Book\` mode, you can switch the \`Odd/Even\` to see the big image that crossing screens. 13 | * Support using \`Enter\` key to close the dialog. 14 | * In \`Scroll\` mode, you can change the space between pages. 15 | * Support manually change the value of the configuration. 16 | * Support mouse wheel to flip pages in \`Book\` mode. 17 | * Add \`Wheel Direction\` for customizing the direction of flipping. 18 | `, 19 | jp: ` 20 | * In \`Book\` mode, you can switch the \`Odd/Even\` to see the big image that crossing screens. 21 | * Support using \`Enter\` key to close the dialog. 22 | * In \`Scroll\` mode, you can change the space between pages. 23 | * Support manually change the value of the configuration. 24 | * Support mouse wheel to flip pages in \`Book\` mode. 25 | * Add \`Wheel Direction\` for customizing the direction of flipping. 26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/bean/DialogBean.ts: -------------------------------------------------------------------------------- 1 | import { DialogOperation } from './DialogOperation' 2 | 3 | export default class DialogBean { 4 | readonly id: number; 5 | type: string; 6 | title: string; 7 | text: string; 8 | operations: Array; 9 | constructor(type: string, title: string, text: string, ...operations: Array) { 10 | this.id = new Date().getTime(); 11 | this.type = type; 12 | this.title = title; 13 | this.text = text; 14 | this.operations = operations; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/renderer/bean/DialogOperation.ts: -------------------------------------------------------------------------------- 1 | export interface DOClick { (): void }; 2 | export class DialogOperation { 3 | name: string; 4 | type: string; 5 | onClick: DOClick; 6 | constructor(name: string, type: string, onClick: DOClick) { 7 | this.name = name; 8 | this.type = type; 9 | this.onClick = onClick; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/bean/ImgPageInfo.ts: -------------------------------------------------------------------------------- 1 | export interface ImgPageInfo { 2 | id: number | string, 3 | index: number, 4 | pageUrl: string, 5 | src: string, 6 | heightOfWidth: number, 7 | thumbHeight?: number, 8 | thumbWidth?: number 9 | } -------------------------------------------------------------------------------- /src/renderer/bean/ServerMessage.ts: -------------------------------------------------------------------------------- 1 | import store from '../store/index' 2 | 3 | interface MsgOperation { 4 | name: string; 5 | url: string; 6 | } 7 | 8 | interface UpdateMsg { 9 | title: string; 10 | version: string; 11 | text: string; 12 | operations: Array; 13 | time: number; 14 | always: boolean; 15 | duration: number; 16 | } 17 | 18 | interface I18nUpdateMsg { 19 | cn: UpdateMsg; 20 | en: UpdateMsg; 21 | jp: UpdateMsg; 22 | } 23 | 24 | export default class ServerMessage { 25 | title: string; 26 | version: string; 27 | text: string; 28 | operations: Array; 29 | time: number; 30 | always: boolean; 31 | duration: number; 32 | 33 | constructor(data: I18nUpdateMsg) { 34 | let message; 35 | switch (store.getters.string.lang) { 36 | case 'CN': 37 | message = data.cn; 38 | break; 39 | case 'JP': 40 | message = data.jp; 41 | break; 42 | case 'EN': 43 | default: 44 | message = data.en; 45 | } 46 | this.title = message.title; 47 | this.version = message.version; 48 | this.text = message.text; 49 | this.operations = message.operations; 50 | this.time = message.time; 51 | this.always = message.always; 52 | this.duration = message.duration; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/renderer/bean/ThumbInfo.ts: -------------------------------------------------------------------------------- 1 | export enum ThumbMode { 2 | SPIRIT = 0, 3 | IMG 4 | } 5 | 6 | export interface ThumbInfo { 7 | id: string | number, 8 | src: string; 9 | mode: ThumbMode, 10 | offset?: number; 11 | } -------------------------------------------------------------------------------- /src/renderer/components/LoadingView.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 32 | 33 | -------------------------------------------------------------------------------- /src/renderer/components/ModalManager.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 43 | 44 | -------------------------------------------------------------------------------- /src/renderer/components/PageView.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 197 | 198 | -------------------------------------------------------------------------------- /src/renderer/components/ReaderView.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | 119 | 120 | -------------------------------------------------------------------------------- /src/renderer/components/ThumbScrollView.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 122 | 123 | 252 | -------------------------------------------------------------------------------- /src/renderer/components/base/AwesomeScrollView.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 98 | 99 | -------------------------------------------------------------------------------- /src/renderer/components/widget/CircleIconButton.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | 41 | -------------------------------------------------------------------------------- /src/renderer/components/widget/DropOption.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 53 | 54 | -------------------------------------------------------------------------------- /src/renderer/components/widget/FlatButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 38 | 39 | -------------------------------------------------------------------------------- /src/renderer/components/widget/Pagination.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 97 | 98 | -------------------------------------------------------------------------------- /src/renderer/components/widget/PopSlider.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 97 | 98 | -------------------------------------------------------------------------------- /src/renderer/components/widget/Popover.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 55 | 56 | -------------------------------------------------------------------------------- /src/renderer/components/widget/SimpleDialog.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 87 | 88 | -------------------------------------------------------------------------------- /src/renderer/components/widget/SimpleSwitch.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 34 | 35 | -------------------------------------------------------------------------------- /src/renderer/components/widget/Slider.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 104 | 105 | -------------------------------------------------------------------------------- /src/renderer/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | import VueUtil from './utils/VueUtil' 4 | 5 | import App from './App' 6 | import router from './router' 7 | import store from './store' 8 | 9 | if (!process.env.IS_WEB) Vue.use(require('vue-electron')) 10 | Vue.http = Vue.prototype.$http = axios 11 | Vue.config.productionTip = false 12 | Vue.mixin(VueUtil) 13 | 14 | /* eslint-disable no-new */ 15 | new Vue({ 16 | components: { App }, 17 | router, 18 | store, 19 | template: '' 20 | }).$mount('#app') 21 | -------------------------------------------------------------------------------- /src/renderer/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Router from 'vue-router' 3 | import Home from '../Home' 4 | import CoreApp from '../CoreApp' 5 | 6 | Vue.use(Router) 7 | 8 | export default new Router({ 9 | routes: [{ 10 | path: '/', 11 | name: 'Home', 12 | component: Home 13 | }, 14 | { 15 | path: '*', 16 | redirect: '/' 17 | }, 18 | { 19 | path: '/CoreApp', 20 | name: 'CoreApp', 21 | component: CoreApp 22 | } 23 | ] 24 | }) -------------------------------------------------------------------------------- /src/renderer/service/AlbumService.ts: -------------------------------------------------------------------------------- 1 | import { ImgPageInfo } from "../bean/ImgPageInfo"; 2 | import { ThumbInfo } from "../bean/ThumbInfo"; 3 | 4 | export interface PreviewThumbnailStyle { 5 | 'background-image': string; 6 | 'background-position': string; 7 | 'background-size': string; 8 | } 9 | 10 | export interface IndexInfo { 11 | val: number; 12 | updater: string; 13 | } 14 | 15 | export abstract class AlbumService { 16 | abstract async getPageCount(): Promise; 17 | abstract async getCurPageNum(): Promise; 18 | abstract async getTitle(): Promise; 19 | abstract async getImgPageInfos(): Promise>; 20 | abstract async getImgPageInfo(index: number): Promise; 21 | abstract async getImgSrc(index: number, mode): Promise; 22 | abstract async getNewImgSrc(index: number, mode): Promise; 23 | abstract async getThumbInfos(noCache?: boolean): Promise>; 24 | abstract async getThumbInfo(index: number): Promise; 25 | abstract async getAlbumId(): Promise; 26 | abstract async getPreviewThumbnailStyle(index: number, imgPageInfo: ImgPageInfo, thumbInfo: ThumbInfo): Promise; 27 | abstract supportOriginImg(): boolean; 28 | abstract supportImgChangeSource(): boolean; 29 | abstract supportThumbView(): boolean; 30 | 31 | getBookScreenCount(pageCount: number, screenSize: number): number { 32 | // 2 is start page and end page 33 | return Math.ceil((pageCount + 2) / screenSize); 34 | } 35 | 36 | getRealCurIndexInfo(pageCount: number, curIndex: IndexInfo): IndexInfo { 37 | let index = curIndex.val; 38 | index = index >= pageCount ? pageCount - 1 : index; 39 | return { val: index, updater: curIndex.updater }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/renderer/service/InfoService.ts: -------------------------------------------------------------------------------- 1 | import store from '../store' 2 | import DialogBean from '../bean/DialogBean' 3 | import { DialogOperation, DOClick } from '../bean/DialogOperation' 4 | import * as tags from '../assets/value/tags.js' 5 | import { TextReq } from '../service/request/TextReq' 6 | import ServerMessage from '../bean/ServerMessage' 7 | import SettingService from '../service/SettingService' 8 | import Logger from '../utils/Logger' 9 | import Formatter from '../utils/formatter' 10 | 11 | class InfoService { 12 | async showInstruction(config, isCompulsive) { 13 | let dialog = new DialogBean( 14 | isCompulsive ? tags.DIALOG_COMPULSIVE : tags.DIALOG_NORMAL, 15 | store.getters.string.instructionsAndAbouts, 16 | Formatter.replaceKey(store.getters.string.p_instruction, { 17 | HOME_PAGE: config.homePage, 18 | VERSION: config.version 19 | }), 20 | new DialogOperation(store.getters.string.confirm, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 21 | return true; 22 | }) 23 | ) 24 | store.dispatch('addDialog', dialog); 25 | } 26 | 27 | async showBookInstruction(isCompulsive): Promise { 28 | let dialog = new DialogBean( 29 | isCompulsive ? tags.DIALOG_COMPULSIVE : tags.DIALOG_NORMAL, 30 | store.getters.string.instructions, 31 | store.getters.string.p_bookInstruction, 32 | new DialogOperation(store.getters.string.confirm, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 33 | return true; 34 | }) 35 | ); 36 | store.dispatch('addDialog', dialog); 37 | } 38 | 39 | async checkUpdate(config): Promise { 40 | let message; 41 | let lastShowDialogTime = await SettingService.getUpdateTime(); 42 | Promise 43 | .race([ 44 | new TextReq(config.updateServer1, true, false).request(), 45 | new TextReq(config.updateServer2, true, false).request() 46 | ]) 47 | .then(data => { 48 | message = new ServerMessage(JSON.parse(data)); 49 | let isNewVersion = message.version !== config.version; 50 | let isReleaseTime = new Date().getTime() > message.time; 51 | let isOverDuration = (new Date().getTime() - lastShowDialogTime) > message.duration; 52 | if (isNewVersion && isReleaseTime && isOverDuration) { 53 | SettingService.setUpdateTime(new Date().getTime()); 54 | this.showUpdateInfo(message); 55 | } 56 | }) 57 | .catch(e => { 58 | Logger.logObj('InfoService', e); 59 | }); 60 | } 61 | 62 | showUpdateInfo(message): void { 63 | let operations: Array = []; 64 | operations.push(new DialogOperation(store.getters.string.later, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 65 | return true; 66 | })); 67 | message.operations.forEach(i => { 68 | operations.push(new DialogOperation(i.name, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 69 | window.open(i.url, '_blank'); 70 | return true; 71 | })); 72 | }); 73 | let dialog = new DialogBean( 74 | tags.DIALOG_COMPULSIVE, 75 | message.title, 76 | message.text, 77 | ...operations 78 | ); 79 | store.dispatch('addDialog', dialog); 80 | } 81 | 82 | showReloadError(text): void { 83 | let dialog = new DialogBean( 84 | tags.DIALOG_COMPULSIVE, 85 | store.getters.string.loadingFailed, 86 | text, 87 | new DialogOperation(store.getters.string.reload, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 88 | window.location.reload(); 89 | return true; 90 | }) 91 | ); 92 | store.dispatch('addDialog', dialog); 93 | } 94 | 95 | // if updated a new version, shows messages 96 | async checkNewVersion(config): Promise { 97 | if (await SettingService.getVersion() !== config.version && config.version !== '1.0.0') { 98 | let dialog = new DialogBean( 99 | tags.DIALOG_COMPULSIVE, 100 | `${store.getters.string.versionUpdate} v${config.version}`, 101 | store.getters.string.p_version, 102 | new DialogOperation(store.getters.string.confirm, tags.DIALOG_OPERATION_TYPE_PLAIN, () => { 103 | SettingService.setVersion(config.version); 104 | return true; 105 | }) 106 | ); 107 | store.dispatch('addDialog', dialog); 108 | } 109 | } 110 | } 111 | 112 | let instance = new InfoService(); 113 | export default instance; 114 | -------------------------------------------------------------------------------- /src/renderer/service/PlatformService.js: -------------------------------------------------------------------------------- 1 | // a service for crossing platform 2 | /* eslint-disable no-undef */ 3 | 4 | // hack for test 5 | if (typeof chrome === 'undefined') { 6 | var chrome = { extension: null }; 7 | } 8 | 9 | export default { 10 | storage: { 11 | get sync() { 12 | if (chrome && chrome.storage) { 13 | return chrome.storage.sync.QUOTA_BYTES ? chrome.storage.sync : chrome.storage.local; 14 | } else { 15 | return window.localStorage; 16 | } 17 | }, 18 | local: window.localStorage 19 | }, 20 | getExtension() { 21 | return chrome.extension; 22 | }, 23 | fetch(url, option) { 24 | /* eslint-disable camelcase */ 25 | if (typeof GM_info !== 'undefined' && GM_info.version) { // the ENV is Tampermonkey 26 | return new Promise((resolve, reject) => { 27 | GM_xmlhttpRequest({ 28 | method: option.method, 29 | url, 30 | onload: x => { 31 | let responseText = x.responseText; 32 | x.text = async function() { 33 | return responseText; 34 | } 35 | resolve(x); 36 | }, 37 | onerror: e => { 38 | reject(`GM_xhr error, ${e.status}`); 39 | } 40 | }); 41 | }); 42 | } else { // the ENV is Chrome or Firefox 43 | return window.fetch(url, option); 44 | } 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/renderer/service/StringService.ts: -------------------------------------------------------------------------------- 1 | import string from '../assets/value/string' 2 | import instruction from '../assets/value/instruction' 3 | import bookInstruction from '../assets/value/bookInstruction' 4 | import version from '../assets/value/version' 5 | 6 | class StringService { 7 | cn = {}; 8 | en = {}; 9 | jp = {}; 10 | 11 | constructor() { 12 | this.initString(); 13 | } 14 | 15 | initString() { 16 | for (let key in string) { 17 | this.cn[key] = string[key].cn; 18 | this.en[key] = string[key].en; 19 | this.jp[key] = string[key].jp; 20 | } 21 | this.cn['p_instruction'] = instruction.cn; 22 | this.en['p_instruction'] = instruction.en; 23 | this.jp['p_instruction'] = instruction.jp; 24 | this.cn['p_bookInstruction'] = bookInstruction.cn; 25 | this.en['p_bookInstruction'] = bookInstruction.en; 26 | this.jp['p_bookInstruction'] = bookInstruction.jp; 27 | this.cn['p_version'] = version.cn; 28 | this.en['p_version'] = version.en; 29 | this.jp['p_version'] = version.jp; 30 | } 31 | } 32 | 33 | let instance = new StringService(); 34 | export default instance; 35 | -------------------------------------------------------------------------------- /src/renderer/service/request/MultiAsyncReq.ts: -------------------------------------------------------------------------------- 1 | // a service for sync multi asynchronous text requests 2 | import { TextReq } from './TextReq' 3 | 4 | export class MultiAsyncReq { 5 | private urls: Array = []; 6 | private resultMap: Map = new Map(); 7 | private fetchSetting = null; 8 | private gen; 9 | 10 | constructor(urls) { 11 | this.urls = urls; 12 | this.fetchSetting = null; 13 | } 14 | 15 | request(): Promise> { 16 | return new Promise((resolve, reject) => { 17 | this._initGenerator(resolve, reject); 18 | this._request(); 19 | }); 20 | } 21 | 22 | setFetchSetting(setting) { 23 | this.fetchSetting = setting; 24 | return this; 25 | } 26 | 27 | _initGenerator(resolve, reject) { 28 | let self = this; 29 | this.gen = (function* () { 30 | try { 31 | for (let url of self.urls) { 32 | let item = yield url; 33 | self.resultMap.set(item.url, item.html); 34 | } 35 | resolve(self.resultMap); 36 | } catch (err) { 37 | reject(err); 38 | } 39 | })(); 40 | this.gen.next(); // run to first yield 41 | } 42 | 43 | _request() { 44 | for (let url of this.urls) { 45 | (new TextReq(url)) 46 | .setFetchSetting(this.fetchSetting) 47 | .request() 48 | .then(html => this.gen.next({ url: url, html: html }, 49 | err => this.gen.throw(err))); 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/renderer/service/request/ReqQueue.ts: -------------------------------------------------------------------------------- 1 | // a service for limiting num of async requests, avoiding too many concurrent requests 2 | import {MultiAsyncReq} from './MultiAsyncReq' 3 | 4 | export class ReqQueue { 5 | private urls: Array = []; 6 | private maxConcurrentedNum = 5; 7 | private resultMap: Map = new Map(); 8 | private fetchSetting = null; 9 | 10 | constructor(urls) { 11 | this.urls = urls; 12 | } 13 | 14 | setNumOfConcurrented(num: number) { 15 | this.maxConcurrentedNum = num; 16 | return this; 17 | } 18 | 19 | setFetchSetting(setting) { 20 | this.fetchSetting = setting; 21 | return this; 22 | } 23 | 24 | request(): Promise> { 25 | return new Promise((resolve, reject) => { 26 | let reqList = this._splitReqs(); 27 | this._request(reqList, resolve, reject); 28 | }); 29 | } 30 | 31 | _splitReqs() { 32 | if (this.urls.length < this.maxConcurrentedNum) { 33 | return [this.urls]; 34 | } 35 | let results: Array = []; 36 | let urls = JSON.parse(JSON.stringify(this.urls)); 37 | while (true) { 38 | let list = urls.splice(0, this.maxConcurrentedNum); 39 | if (list.length > 0) { 40 | results.push(list); 41 | } else { 42 | return results; 43 | } 44 | } 45 | } 46 | 47 | _addMap(destMap, srcMap) { 48 | for (let item of srcMap) { 49 | destMap.set(item[0], item[1]); 50 | } 51 | return destMap; 52 | } 53 | 54 | _request(reqList, resolve, reject) { 55 | if (reqList.length > 0) { 56 | (new MultiAsyncReq(reqList[0])) 57 | .setFetchSetting(this.fetchSetting) 58 | .request() 59 | .then(map => { 60 | this._addMap(this.resultMap, map); 61 | reqList.splice(0, 1); 62 | this._request(reqList, resolve, reject); 63 | }, err => { reject(err) }); 64 | } else { 65 | resolve(this.resultMap); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/renderer/service/request/TextReq.ts: -------------------------------------------------------------------------------- 1 | // a good resolution for poor network 2 | import PlatformService from '../PlatformService' 3 | 4 | export class TextReq { 5 | private url: string; 6 | private method = 'GET'; 7 | private credentials = 'include'; 8 | private retryTimes = 3; 9 | private timeoutTime = 15; // secs 10 | private curRetryTimes = 0; 11 | private retryInterval = 3; // secs 12 | private enabledLog = true; 13 | private fetchSetting = null; 14 | private noCache = false; 15 | private rejectError = true; 16 | 17 | constructor(url: string, noCache = false, rejectError = true) { 18 | this.url = url; 19 | this.noCache = noCache; 20 | this.rejectError = rejectError; 21 | } 22 | 23 | setMethod(method) { 24 | this.method = method; 25 | return this; 26 | } 27 | 28 | setCredentials(credential: string) { 29 | this.credentials = credential; 30 | return this; 31 | } 32 | 33 | setFetchSetting(setting: any) { 34 | this.fetchSetting = setting; 35 | return this; 36 | } 37 | 38 | setRetryTimes(times: number) { 39 | this.retryTimes = times; 40 | } 41 | 42 | setRetryInterval(secs: number) { 43 | this.retryInterval = secs; 44 | } 45 | 46 | setTimeOutTime(secs: number) { 47 | this.timeoutTime = secs; 48 | } 49 | 50 | request(): Promise { 51 | return new Promise((resolve, reject) => { 52 | this._request(res => { 53 | res.text().then(text => resolve(text)); 54 | }, err => { 55 | if (this.rejectError) { 56 | reject(err); 57 | } else { 58 | console.error(err); 59 | } 60 | }); 61 | }); 62 | } 63 | 64 | private printErrorLog(err) { 65 | console.error(`TextReq: request error in ${this.url}, retry:(${this.curRetryTimes}/${this.retryTimes}), error: ${err}`); 66 | } 67 | 68 | _request(successCallback, failureCallback) { 69 | this.curRetryTimes++; 70 | let url = this.url.includes('http') ? this.url : `${window.location.protocol}//${window.location.host}${this.url}`; 71 | if (this.noCache) { 72 | url = `${url}?_t=${new Date().getTime()}`; 73 | } 74 | let timeout = new Promise((resolve, reject) => { 75 | setTimeout(reject, this.timeoutTime * 1000 * this.curRetryTimes, 'request timed out'); 76 | }); 77 | let req = PlatformService.fetch(url, this.fetchSetting ? this.fetchSetting : { 78 | method: this.method, 79 | credentials: this.credentials 80 | }); 81 | Promise 82 | .race([timeout, req]) 83 | .then(res => { 84 | if (res.status === 200) { 85 | successCallback(res); 86 | } else { 87 | throw new Error(`${url}: ${res.status}`); 88 | } 89 | }) 90 | .catch(err => { 91 | this.printErrorLog(err); 92 | if (this.curRetryTimes < this.retryTimes) { 93 | setTimeout(() => { 94 | this._request(successCallback, failureCallback); 95 | }, this.retryInterval * 1000); 96 | } else { 97 | failureCallback(err); 98 | } 99 | }); 100 | } 101 | } -------------------------------------------------------------------------------- /src/renderer/service/storage/LocalStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from './base/Storage' 2 | import Platform from '../PlatformService' 3 | 4 | let storage = new Storage({ 5 | size: 10, 6 | storageBackend: Platform.storage.local, 7 | defaultExpires: null, 8 | enableCache: true, 9 | sync: {} 10 | }); 11 | 12 | export default storage; 13 | -------------------------------------------------------------------------------- /src/renderer/service/storage/SyncStorage.ts: -------------------------------------------------------------------------------- 1 | import Storage from './base/Storage' 2 | import Platform from '../PlatformService' 3 | 4 | let storage = new Storage({ 5 | size: 10, 6 | storageBackend: Platform.storage.sync, 7 | defaultExpires: null, 8 | enableCache: true, 9 | sync: {} 10 | }); 11 | 12 | export default storage; 13 | -------------------------------------------------------------------------------- /src/renderer/service/storage/base/Storage.js: -------------------------------------------------------------------------------- 1 | import Storage from '../../../utils/react-native-storage/storage'; 2 | import Logger from '../../../utils/Logger'; 3 | 4 | function wrapStorageArea(storageArea) { 5 | return { 6 | async getItem(key) { 7 | return new Promise((resolve, reject) => { 8 | Logger.logText('Storage', `get ${key}`); 9 | storageArea.get(key, (val) => { 10 | if (typeof val[key] !== 'undefined') { 11 | resolve(val[key]); 12 | } else { 13 | Logger.logText('Storage', `This key--${key} doesn't exist`); 14 | resolve(null); 15 | } 16 | }) 17 | }); 18 | }, 19 | async setItem(key, val) { 20 | return new Promise((resolve, reject) => { 21 | if (key) { 22 | storageArea.set({ 23 | [key]: val 24 | }, () => { 25 | Logger.logText('Storage', `chrome saved ${key}`); 26 | resolve(); 27 | }); 28 | } else { 29 | Logger.logText('Storage', `ERROR: setItem, key is null, ${val}`); 30 | } 31 | }); 32 | }, 33 | async removeItem(key) { 34 | return new Promise((resolve, reject) => { 35 | storageArea.remove(key, () => { 36 | Logger.logText('Storage', `chrome removed ${key}`); 37 | resolve(); 38 | }) 39 | }); 40 | } 41 | } 42 | } 43 | 44 | export default class UniStorage extends Storage { 45 | constructor(options = {}) { 46 | if (options.storageBackend.constructor.name === 'StorageArea') { 47 | options.storageBackend = wrapStorageArea(options.storageBackend); 48 | } 49 | super(options); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/renderer/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import modules from './modules' 5 | 6 | Vue.use(Vuex) 7 | 8 | export default new Vuex.Store({ 9 | modules, 10 | strict: process.env.NODE_ENV !== 'production' 11 | }) 12 | -------------------------------------------------------------------------------- /src/renderer/store/modules/AlbumView.js: -------------------------------------------------------------------------------- 1 | // import string from 'assets/value/string-cn.json' 2 | import * as types from '../mutation-types' 3 | import * as tags from '../../assets/value/tags' 4 | // import Logger from '../../utils/Logger' 5 | 6 | // initial state 7 | const state = { 8 | curIndex: { val: 0, updater: '' }, // current index of page 9 | readingMode: 1, // 0: scroll mode, 1: book mode 10 | volumeSize: 10, // the page quantity per volume 11 | volumePreloadCount: 2, // the preload count of page of next volume 12 | topBarHeight: 40, // px, for calc 13 | thumb: { 14 | width: 150, // px 15 | thumbView: true // show/hide the column of thumbnail 16 | }, 17 | album: { 18 | width: 80, // percent, the scale of img 19 | toggleSyncScroll: true, // unused 20 | showTopBar: true, 21 | loadNum: 3, // the sum of pages per loading 22 | scrolledPageMargin: 70 23 | }, 24 | book: { 25 | bookIndex: 0, // index of screens 26 | screenSize: 2, // the page quantity per screen 27 | showBookScreenAnimation: false, // show/hide sliding animation when changing location 28 | showBookPagination: true, // show/hide bottom floating pagination bar 29 | direction: 0, // 0: RTL, 1: LTR 30 | reverseFlip: false, // reverse the page flipping direction 31 | autoFlip: false, 32 | autoFlipFrequency: 10, 33 | showThumbViewInBook: false, 34 | wheelSensitivity: 100, 35 | wheelDirection: false, 36 | oddEven: false 37 | }, 38 | showMoreSettings: false 39 | } 40 | 41 | // getters 42 | const getters = { 43 | curIndex: state => state.curIndex, 44 | albumWidth: state => state.album.width, 45 | thumbWidth: state => state.thumb.width, 46 | showThumbView: state => state.thumb.thumbView, 47 | toggleSyncScroll: state => state.album.toggleSyncScroll, 48 | showTopBar: state => state.album.showTopBar, 49 | topBarHeight: state => state.topBarHeight, 50 | loadNum: state => state.album.loadNum, 51 | volumeSize: state => state.volumeSize, 52 | curVolume: state => { 53 | let remainder = state.curIndex.val % state.volumeSize; 54 | return (state.curIndex.val - remainder) / state.volumeSize; 55 | }, 56 | volFirstIndex: state => getters.curVolume(state) * state.volumeSize, 57 | volumePreloadCount: state => state.volumePreloadCount, 58 | bookScreenSize: state => state.book.screenSize, 59 | bookIndex: state => { // map curIndex to bookIndex 60 | // ((state.curIndex.val + 1) - (state.curIndex.val + 1) % state.book.screenSize) / state.book.screenSize 61 | if (state.curIndex.val <= state.book.screenSize - 2) { 62 | return 0; 63 | } else { 64 | let num = state.curIndex.val + 2; 65 | let remainder = num % state.book.screenSize; 66 | if (remainder === 0) { 67 | return (num - num % state.book.screenSize) / state.book.screenSize - 1; 68 | } else { 69 | return (num - num % state.book.screenSize) / state.book.screenSize; 70 | } 71 | } 72 | }, 73 | bookLoadNum: state => Math.ceil(state.album.loadNum / state.book.screenSize), 74 | readingMode: state => state.readingMode, 75 | showBookScreenAnimation: state => state.book.showBookScreenAnimation, 76 | showBookPagination: state => state.book.showBookPagination, 77 | bookDirection: state => state.book.direction, 78 | showMoreSettings: state => state.showMoreSettings, 79 | reverseFlip: state => state.book.reverseFlip, 80 | autoFlip: state => state.book.autoFlip, 81 | autoFlipFrequency: state => state.book.autoFlipFrequency, 82 | showThumbViewInBook: state => state.book.showThumbViewInBook, 83 | wheelSensitivity: state => state.book.wheelSensitivity, 84 | wheelDirection: state => state.book.wheelDirection, 85 | scrolledPageMargin: state => state.album.scrolledPageMargin, 86 | oddEven: state => state.book.oddEven 87 | } 88 | 89 | // actions 90 | const actions = { 91 | setIndex: ({ commit }, { val, updater }) => { 92 | commit(types.SET_INDEX, { val, updater }); 93 | }, 94 | setAlbumWidth: ({ commit }, width) => commit(types.SET_ALBUM_WIDTH, { width }), 95 | toggleThumbView: ({ commit }, show) => commit(types.TOGGLE_THUMB_VIEW, { show }), 96 | toggleSyncScroll: ({ commit }, isActive) => commit(types.TOGGLE_SYNC_SCROLL, { isActive }), 97 | toggleTopBar: ({ commit }, show) => commit(types.TOGGLE_SHOW_TOP_BAR, { show }), 98 | setLoadNum: ({ commit }, num) => commit(types.SET_LOAD_NUM, { num }), 99 | setVolumeSize: ({ commit }, num) => commit(types.SET_VOLUME_SIZE, { num }), 100 | setBookIndex: ({ commit }, index) => commit(types.SET_BOOK_INDEX, { index }), 101 | setReadingMode: ({ commit }, mode) => commit(types.SET_READING_MODE, { mode }), 102 | setBookScreenAnimation: ({ commit }, show) => commit(types.SET_BOOK_SCREEN_ANIMATION, { show }), 103 | setBookPagination: ({ commit }, show) => commit(types.SET_BOOK_PAGINATION, { show }), 104 | setBookDirection: ({ commit }, mode) => commit(types.SET_BOOK_DIRECTION, { mode }), 105 | setBookScreenSize: ({ commit }, num) => commit(types.SET_BOOK_SCREEN_SIZE, { num }), 106 | toggleMoreSettings: ( { commit }, show) => commit(types.TOGGLE_MORE_SETTINGS, { show }), 107 | setReverseFlip: ( { commit }, val) => commit(types.SET_REVERSE_FLIP, { val }), 108 | setAutoFlip: ( { commit }, val) => commit(types.SET_AUTO_FLIP, { val }), 109 | setAutoFlipFrequency: ( { commit }, val) => commit(types.SET_AUTO_FLIP_FREQUENCY, { val }), 110 | toggleThumbViewInBook: ( { commit }, val) => commit(types.TOGGLE_THUMB_VIEW_IN_BOOK, { val }), 111 | setWheelSensitivity: ( { commit }, val) => commit(types.SET_WHEEL_SENSITIVITY, { val }), 112 | setWheelDirection: ( { commit }, val) => commit(types.SET_WHEEL_DIRECTION, { val }), 113 | setScrolledPageMargin: ( { commit }, val) => commit(types.SET_SCROLLED_PAGE_MARGIN, { val }), 114 | setOddEven: ( { commit }, val) => commit(types.SET_ODD_EVEN, { val }) 115 | } 116 | 117 | // mutations 118 | const mutations = { 119 | [types.SET_INDEX](state, { val, updater }) { 120 | state.curIndex.val = val; 121 | state.curIndex.updater = updater; 122 | // update bookIndex 123 | if (state.curIndex.val <= state.book.screenSize - 2) { 124 | state.book.bookIndex = 0; 125 | } else { 126 | let num = state.curIndex.val + 2; 127 | let remainder = num % state.book.screenSize; 128 | if (remainder === 0) { 129 | state.book.bookIndex = (num - num % state.book.screenSize) / state.book.screenSize - 1; 130 | } else { 131 | state.book.bookIndex = (num - num % state.book.screenSize) / state.book.screenSize; 132 | } 133 | } 134 | }, 135 | [types.SET_ALBUM_WIDTH](state, { width }) { 136 | state.album.width = width; 137 | }, 138 | [types.TOGGLE_THUMB_VIEW](state, { show }) { 139 | state.thumb.thumbView = show; 140 | }, 141 | [types.TOGGLE_SYNC_SCROLL](state, { isActive }) { 142 | state.album.toggleSyncScroll = isActive; 143 | }, 144 | [types.TOGGLE_SHOW_TOP_BAR](state, { show }) { 145 | state.album.showTopBar = show; 146 | }, 147 | [types.SET_LOAD_NUM](state, { num }) { 148 | state.album.loadNum = num; 149 | }, 150 | [types.SET_VOLUME_SIZE](state, { num }) { 151 | state.volumeSize = num; 152 | state.curIndex.val = 0; 153 | state.curIndex.updater = tags.TOP_BAR; 154 | }, 155 | [types.SET_BOOK_INDEX](state, { index }) { 156 | let i = index * state.book.screenSize - 1; 157 | state.curIndex.val = i < 0 ? 0 : i; 158 | state.curIndex.updater = tags.BOOK_VIEW; 159 | }, 160 | [types.SET_READING_MODE](state, { mode }) { 161 | state.readingMode = mode; 162 | }, 163 | [types.SET_BOOK_SCREEN_ANIMATION](state, { show }) { 164 | state.book.showBookScreenAnimation = show; 165 | }, 166 | [types.SET_BOOK_PAGINATION](state, { show }) { 167 | state.book.showBookPagination = show; 168 | }, 169 | [types.SET_BOOK_DIRECTION](state, { mode }) { 170 | state.book.direction = mode; 171 | }, 172 | [types.SET_BOOK_SCREEN_SIZE](state, { num }) { 173 | state.book.screenSize = num; 174 | }, 175 | [types.TOGGLE_MORE_SETTINGS](state, { show }) { 176 | state.showMoreSettings = show; 177 | }, 178 | [types.SET_REVERSE_FLIP](state, { val }) { 179 | state.book.reverseFlip = val; 180 | }, 181 | [types.SET_AUTO_FLIP](state, { val }) { 182 | state.book.autoFlip = val; 183 | }, 184 | [types.SET_AUTO_FLIP_FREQUENCY](state, { val }) { 185 | state.book.autoFlipFrequency = val; 186 | }, 187 | [types.TOGGLE_THUMB_VIEW_IN_BOOK](state, { val }) { 188 | state.book.showThumbViewInBook = val; 189 | }, 190 | [types.SET_WHEEL_SENSITIVITY](state, { val }) { 191 | state.book.wheelSensitivity = val; 192 | }, 193 | [types.SET_WHEEL_DIRECTION](state, { val }) { 194 | state.book.wheelDirection = val; 195 | }, 196 | [types.SET_SCROLLED_PAGE_MARGIN](state, { val }) { 197 | state.album.scrolledPageMargin = val; 198 | }, 199 | [types.SET_ODD_EVEN](state, { val }) { 200 | state.book.oddEven = val; 201 | } 202 | } 203 | 204 | export default { 205 | state, 206 | getters, 207 | actions, 208 | mutations 209 | } 210 | -------------------------------------------------------------------------------- /src/renderer/store/modules/Modal.js: -------------------------------------------------------------------------------- 1 | import * as types from '../mutation-types' 2 | // import Logger from '../../utils/Logger' 3 | 4 | // initial state 5 | const state = { 6 | dialogs: [] 7 | } 8 | 9 | // getters 10 | const getters = { 11 | dialogs: state => state.dialogs 12 | } 13 | 14 | // actions 15 | const actions = { 16 | addDialog: ({ commit }, dialogBean) => commit(types.ADD_DIALOG, { dialogBean }), 17 | removeDialog: ({ commit }, dialogBean) => commit(types.REMOVE_DIALOG, { dialogBean }) 18 | } 19 | 20 | // mutations 21 | const mutations = { 22 | [types.ADD_DIALOG](state, { dialogBean }) { 23 | state.dialogs.push(dialogBean); 24 | }, 25 | [types.REMOVE_DIALOG](state, { dialogBean }) { 26 | state.dialogs.splice(state.dialogs.indexOf(dialogBean), 1); 27 | } 28 | } 29 | 30 | export default { 31 | state, 32 | getters, 33 | actions, 34 | mutations 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/store/modules/String.js: -------------------------------------------------------------------------------- 1 | import StringService from '../../service/StringService.ts' 2 | import * as types from '../mutation-types' 3 | import * as tags from '../../assets/value/tags' 4 | // import Logger from '../../utils/Logger' 5 | 6 | // initial state 7 | const state = { 8 | string: StringService.en 9 | } 10 | 11 | // getters 12 | const getters = { 13 | string: state => { 14 | return state.string 15 | } 16 | } 17 | 18 | // actions 19 | const actions = { 20 | setString({ commit }, langCode) { 21 | commit(types.SET_STRING, { langCode }); 22 | } 23 | } 24 | 25 | // mutations 26 | const mutations = { 27 | [types.SET_STRING](state, { langCode }) { 28 | /* eslint-disable indent */ 29 | switch (langCode) { 30 | case tags.LANG_CN: 31 | state.string = StringService.cn; 32 | break; 33 | case tags.LANG_EN: 34 | state.string = StringService.en; 35 | break; 36 | case tags.LANG_JP: 37 | state.string = StringService.jp; 38 | break; 39 | } 40 | } 41 | } 42 | 43 | export default { 44 | state, 45 | getters, 46 | actions, 47 | mutations 48 | } 49 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The file enables `@/store/index.js` to import all vuex modules 3 | * in a one-shot manner. There should not be any reason to edit this file. 4 | */ 5 | 6 | const files = require.context('.', false, /\.js$/) 7 | const modules = {} 8 | 9 | files.keys().forEach(key => { 10 | if (key === './index.js') return 11 | modules[key.replace(/(\.\/|\.js)/g, '')] = files(key).default 12 | }) 13 | 14 | export default modules 15 | -------------------------------------------------------------------------------- /src/renderer/store/mutation-types.js: -------------------------------------------------------------------------------- 1 | export const SET_LANG = 'SET_LANG' 2 | export const SET_SELECTOR = 'SET_SELECTOR' 3 | export const SET_RANK_LIST = 'SET_RANK_LIST' 4 | export const SET_CUR_RANK = 'SET_CUR_RANK' 5 | export const SET_CUR_SUB_RANK = 'SET_CUR_SUB_RANK' 6 | export const SET_CUR_TAB = 'SET_CUR_TAB' 7 | export const SHOW_SELECTOR = 'SHOW_SELECTOR' 8 | export const SHOW_TABLE_PICKER = 'SHOW_TABLE_PICKER' 9 | export const SHOW_ITEM_INFO = 'SHOW_ITEM_INFO' 10 | export const CLOSE_POPUPS = 'CLOSE_POPUPS' 11 | export const UPDATE_RANK = 'UPDATE_RANK' 12 | export const SET_ERROR = 'SET_ERROR' 13 | export const SWITCH_TABLE_ITEMS = 'SWITCH_TABLE_ITEMS' 14 | 15 | export const SET_INDEX = 'SET_INDEX' 16 | export const SET_ALBUM_WIDTH = 'SET_ALBUM_WIDTH' 17 | export const TOGGLE_THUMB_VIEW = 'TOGGLE_THUMB_VIEW' 18 | export const TOGGLE_SYNC_SCROLL = 'TOGGLE_SYNC_SCROLL' 19 | export const TOGGLE_SHOW_TOP_BAR = 'TOGGLE_SHOW_TOP_BAR' 20 | export const SET_LOAD_NUM = 'SET_LOAD_NUM' 21 | export const SET_VOLUME_SIZE = 'SET_VOLUME_SIZE' 22 | export const SET_BOOK_INDEX = 'SET_BOOK_INDEX' 23 | export const SET_READING_MODE = 'SET_READING_MODE' 24 | export const SET_BOOK_SCREEN_ANIMATION = 'SET_BOOK_SCREEN_ANIMATION' 25 | export const SET_BOOK_PAGINATION = 'SET_BOOK_PAGINATION' 26 | export const SET_BOOK_DIRECTION = 'SET_BOOK_DIRECTION' 27 | export const SET_BOOK_SCREEN_SIZE = 'SET_BOOK_SCREEN_SIZE' 28 | export const TOGGLE_MORE_SETTINGS = 'TOGGLE_MORE_SETTINGS' 29 | export const SET_REVERSE_FLIP = 'SET_REVERSE_FLIP' 30 | export const SET_AUTO_FLIP = 'SET_AUTO_FLIP' 31 | export const SET_AUTO_FLIP_FREQUENCY = 'SET_AUTO_FLIP_FREQUENCY' 32 | export const TOGGLE_THUMB_VIEW_IN_BOOK = 'TOGGLE_THUMB_VIEW_IN_BOOK' 33 | export const SET_WHEEL_SENSITIVITY = 'SET_WHEEL_SENSITIVITY' 34 | export const SET_WHEEL_DIRECTION = 'SET_WHEEL_DIRECTION' 35 | export const SET_SCROLLED_PAGE_MARGIN = 'SET_SCROLLED_PAGE_MARGIN' 36 | export const SET_ODD_EVEN = 'SET_ODD_EVEN' 37 | 38 | export const SET_STRING = 'SET_STRING' 39 | 40 | export const ADD_DIALOG = 'ADD_DIALOG' 41 | export const REMOVE_DIALOG = 'REMOVE_DIALOG' 42 | -------------------------------------------------------------------------------- /src/renderer/style/_markdown.scss: -------------------------------------------------------------------------------- 1 | p.markdown { 2 | font-size: 14px !important; 3 | line-height: 1.42857143 !important; 4 | color: #333 !important; 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | *:before, 10 | *:after { 11 | box-sizing: border-box; 12 | } 13 | 14 | hr { 15 | margin-top: 20px; 16 | margin-bottom: 20px; 17 | border: 0; 18 | border-top: 1px solid #eee; 19 | height: 0; 20 | } 21 | 22 | input, 23 | button, 24 | select, 25 | textarea { 26 | font-family: inherit; 27 | font-size: inherit; 28 | line-height: inherit; 29 | } 30 | 31 | a { 32 | color: #428bca; 33 | text-decoration: none; 34 | background: transparent; 35 | &:hover, 36 | &:focus { 37 | color: #2a6496; 38 | outline: none; 39 | text-decoration: underline; 40 | } 41 | } 42 | 43 | p { 44 | margin: 0 0 10px !important; 45 | } 46 | 47 | b, 48 | strong { 49 | font-weight: bold; 50 | } 51 | 52 | h1 { 53 | font-size: 36px; 54 | margin: .67em 0; 55 | } 56 | 57 | h2 { 58 | font-size: 30px; 59 | } 60 | 61 | h4 { 62 | font-size: 18px; 63 | } 64 | 65 | h5 { 66 | font-size: 14px; 67 | } 68 | 69 | h6 { 70 | font-size: 12px; 71 | } 72 | 73 | h1, 74 | h2, 75 | h3 { 76 | margin-top: 20px !important; 77 | margin-bottom: 10px !important; 78 | } 79 | 80 | h4, 81 | h5, 82 | h6 { 83 | margin-top: 10px !important; 84 | margin-bottom: 10px !important; 85 | } 86 | 87 | h1, 88 | h2, 89 | h3, 90 | h4, 91 | h5, 92 | h6 { 93 | font-family: inherit; 94 | font-weight: 500; 95 | line-height: 1.1; 96 | color: inherit; 97 | } 98 | 99 | blockquote { 100 | padding: 10px 20px; 101 | margin: 0 0 20px; 102 | font-size: 17.5px; 103 | border-left: 5px solid #eee; 104 | &:before { 105 | content: ''; 106 | } 107 | &:after { 108 | content: ''; 109 | } 110 | } 111 | 112 | ul, 113 | ol { 114 | margin-top: 0; 115 | margin-bottom: 10px; 116 | } 117 | 118 | code, 119 | kbd, 120 | pre, 121 | samp { 122 | font-family: Menlo, Monaco, Consolas, "Courier New", monospace; 123 | } 124 | 125 | code { 126 | padding: 2px 4px; 127 | font-size: 90%; 128 | color: #c7254e; 129 | background-color: #f9f2f4; 130 | border-radius: 4px; 131 | } 132 | 133 | ul { 134 | padding-left: 20px; 135 | } 136 | 137 | ul ul, 138 | ol ul, 139 | ul ol, 140 | ol ol { 141 | margin-bottom: 0; 142 | } 143 | 144 | pre { 145 | display: block; 146 | padding: 9.5px; 147 | margin: 0 0 10px; 148 | font-size: 13px; 149 | line-height: 1.42857143; 150 | color: #333; 151 | word-break: break-all; 152 | word-wrap: break-word; 153 | background-color: #f5f5f5; 154 | border: 1px solid #ccc; 155 | border-radius: 4px; 156 | overflow: auto; 157 | code { 158 | padding: 0; 159 | font-size: inherit; 160 | color: inherit; 161 | white-space: pre-wrap; 162 | background-color: transparent; 163 | border-radius: 0; 164 | } 165 | } 166 | 167 | table { 168 | width: 100%; 169 | max-width: 100%; 170 | margin-bottom: 20px; 171 | background-color: transparent; 172 | border-spacing: 0; 173 | border-collapse: collapse; 174 | } 175 | 176 | table>caption+thead>tr:first-child>th, 177 | table>colgroup+thead>tr:first-child>th, 178 | table>thead:first-child>tr:first-child>th, 179 | table>caption+thead>tr:first-child>td, 180 | table>colgroup+thead>tr:first-child>td, 181 | table>thead:first-child>tr:first-child>td { 182 | border-top: 0; 183 | } 184 | 185 | table>thead>tr>th { 186 | vertical-align: bottom; 187 | border-bottom: 2px solid #ddd; 188 | } 189 | 190 | table>thead>tr>th, 191 | table>tbody>tr>th, 192 | table>tfoot>tr>th, 193 | table>thead>tr>td, 194 | table>tbody>tr>td, 195 | table>tfoot>tr>td { 196 | padding: 8px; 197 | line-height: 1.42857143; 198 | vertical-align: top; 199 | border-top: 1px solid #ddd; 200 | } 201 | 202 | th { 203 | text-align: left; 204 | } 205 | 206 | td, 207 | th { 208 | padding: 0; 209 | } 210 | 211 | tbody>tr:nth-child(odd)>td, 212 | tbody>tr:nth-child(odd)>th { 213 | background-color: #f9f9f9; 214 | } 215 | 216 | img { 217 | max-width: 35%; 218 | vertical-align: middle; 219 | border: 0; 220 | } 221 | 222 | sub, 223 | sup { 224 | position: relative; 225 | font-size: 75%; 226 | line-height: 0; 227 | vertical-align: baseline; 228 | } 229 | 230 | sup { 231 | top: -.5em; 232 | } 233 | 234 | .emoji { 235 | height: 1.2em; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/renderer/style/_normalize.scss: -------------------------------------------------------------------------------- 1 | .normalize { 2 | line-height: 1.15; 3 | -webkit-text-size-adjust: 100%; 4 | margin: 0; 5 | -webkit-font-smoothing: auto; 6 | 7 | main { 8 | display: block; 9 | } 10 | 11 | h1 { 12 | font-size: 2em; 13 | margin: 0.67em 0; 14 | } 15 | 16 | hr { 17 | box-sizing: content-box; 18 | height: 0; 19 | overflow: visible; 20 | } 21 | 22 | pre { 23 | font-family: monospace, monospace; 24 | font-size: 1em; 25 | } 26 | 27 | a { 28 | background-color: transparent; 29 | } 30 | 31 | abbr[title] { 32 | border-bottom: none; 33 | text-decoration: underline; 34 | text-decoration: underline dotted; 35 | } 36 | 37 | b, 38 | strong { 39 | font-weight: bolder; 40 | } 41 | 42 | code, 43 | kbd, 44 | samp { 45 | font-family: monospace, monospace; 46 | font-size: 1em; 47 | } 48 | 49 | small { 50 | font-size: 80%; 51 | } 52 | 53 | sub, 54 | sup { 55 | font-size: 75%; 56 | line-height: 0; 57 | position: relative; 58 | vertical-align: baseline; 59 | } 60 | 61 | sub { 62 | bottom: -0.25em; 63 | } 64 | 65 | sup { 66 | top: -0.5em; 67 | } 68 | 69 | 70 | img { 71 | border-style: none; 72 | } 73 | 74 | button, 75 | input, 76 | optgroup, 77 | select, 78 | textarea { 79 | font-family: inherit; 80 | /* 1 */ 81 | font-size: 100%; 82 | /* 1 */ 83 | line-height: 1.15; 84 | margin: 0; 85 | } 86 | 87 | button, 88 | input { 89 | overflow: visible; 90 | } 91 | 92 | button, 93 | select { 94 | text-transform: none; 95 | } 96 | 97 | button, 98 | [type="button"], 99 | [type="reset"], 100 | [type="submit"] { 101 | -webkit-appearance: button; 102 | } 103 | 104 | button::-moz-focus-inner, 105 | [type="button"]::-moz-focus-inner, 106 | [type="reset"]::-moz-focus-inner, 107 | [type="submit"]::-moz-focus-inner { 108 | border-style: none; 109 | padding: 0; 110 | } 111 | 112 | 113 | button:-moz-focusring, 114 | [type="button"]:-moz-focusring, 115 | [type="reset"]:-moz-focusring, 116 | [type="submit"]:-moz-focusring { 117 | outline: 1px dotted ButtonText; 118 | } 119 | 120 | fieldset { 121 | padding: 0.35em 0.75em 0.625em; 122 | } 123 | 124 | 125 | legend { 126 | box-sizing: border-box; 127 | color: inherit; 128 | display: table; 129 | max-width: 100%; 130 | padding: 0; 131 | white-space: normal; 132 | } 133 | 134 | progress { 135 | vertical-align: baseline; 136 | } 137 | 138 | 139 | textarea { 140 | overflow: auto; 141 | } 142 | 143 | 144 | [type="checkbox"], 145 | [type="radio"] { 146 | box-sizing: border-box; 147 | padding: 0; 148 | } 149 | 150 | 151 | 152 | [type="number"]::-webkit-inner-spin-button, 153 | [type="number"]::-webkit-outer-spin-button { 154 | height: auto; 155 | } 156 | 157 | 158 | 159 | [type="search"] { 160 | -webkit-appearance: textfield; 161 | outline-offset: -2px; 162 | } 163 | 164 | 165 | [type="search"]::-webkit-search-decoration { 166 | -webkit-appearance: none; 167 | } 168 | 169 | 170 | ::-webkit-file-upload-button { 171 | -webkit-appearance: button; 172 | font: inherit; 173 | } 174 | 175 | details { 176 | display: block; 177 | } 178 | 179 | 180 | summary { 181 | display: list-item; 182 | } 183 | 184 | 185 | 186 | template { 187 | display: none; 188 | } 189 | 190 | [hidden] { 191 | display: none; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/renderer/style/_responsive.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | 3 | // Default font size 4 | // Because this is so important in many of your calculations as a context, 5 | // keep your default font size stored as a variable. 6 | 7 | $default-font-size: 16px !default; 8 | 9 | // Breakpoints 10 | 11 | // Set your desired breakpoints here. Ideally, breakpoints should be set to 12 | // where they make sense in the design, not targeted to specific devices. 13 | 14 | // Example: 15 | $breakpoint-xs: "min-width: 0px"; 16 | $breakpoint-sm: "min-width: 576px"; 17 | $breakpoint-md: "min-width: 768px"; 18 | $breakpoint-lg: "min-width: 992px"; 19 | $breakpoint-xl: "min-width: 1200px"; 20 | 21 | 22 | // Contexts 23 | 24 | // Not necessarily the same as breakpoints, but a point from which to judge 25 | // relative values. Put common ones here, such as the maximum width of 26 | // the content. 27 | 28 | // Example: 29 | $context-sm: 540px; 30 | $context-md: 720px; 31 | $context-lg: 960px; 32 | $context-xl: 1140px; 33 | 34 | 35 | 36 | // Mixins 37 | 38 | // responsive: to use, include the following code in your 39 | // declarations: 40 | // 41 | // .class { 42 | // some-property: some-value; 43 | // 44 | // @include responsive($breakpoint) { some-property: some-new-value; } 45 | // } 46 | // 47 | // By default, the declared media value is "all", but this can 48 | // be overridden by calling the mixin as: 49 | // @include responsive($breakpoint, screen) 50 | // (using "screen" as an example) 51 | // 52 | // You can define common breakpoints in the variables section above 53 | // and reuse them. Else, you can pass it literally when calling the 54 | // mixin: 55 | // 56 | // .class { 57 | // @include responsive('min-width: 640px') { some-property: some-new-value; } 58 | // } 59 | 60 | @mixin responsive($breakpoint, $media: all) { 61 | @media only #{$media} and ($breakpoint) { 62 | @content; 63 | } 64 | } 65 | 66 | // breakpoint: Chris Coyier's method for creating media queries. 67 | // See: http://css-tricks.com/media-queries-sass-3-2-and-codekit/ 68 | // Make up **meaningful** names for your breakpoints so that you 69 | // and the members of your team can understand and remember them. 70 | // Also, adjust this mixin for as many breakpoints as you need. 71 | // You'll likely need finer control for things like font size 72 | // adjustments, so the "responsive" mixin above can help fill in 73 | // the gaps. 74 | // 75 | // This is currently set up as mobile first, expanding upwards. 76 | // You'll need to change "min-width" to "max-width" if you're 77 | // working from the desktop down. 78 | 79 | @mixin breakpoint($breakpoint, $media: all) { 80 | 81 | // Change these values as required 82 | $medium: 640px / 16px * 1em; 83 | $large: 1024px / 16px * 1em; 84 | $xlarge: 1280px / 16px * 1em; 85 | 86 | @if $breakpoint == medium { 87 | @media only #{$media} and (min-width: $medium) { 88 | @content; 89 | } 90 | } @else if $breakpoint == large { 91 | @media only #{$media} and (min-width: $large) { 92 | @content; 93 | } 94 | } @else if $breakpoint == xlarge { 95 | @media only #{$media} and (min-width: $xlarge) { 96 | @content; 97 | } 98 | } 99 | } 100 | 101 | 102 | // bp: alias for breakpoint 103 | 104 | @mixin bp($breakpoint, $media: all) { 105 | @include breakpoint($breakpoint, $media) { 106 | @content; 107 | } 108 | } 109 | 110 | 111 | // hidpi: same concept as "responsive", except that this is focusing on 112 | // HiDPI (a.k.a. retina) displays. 113 | @mixin hidpi($media: all) { 114 | @media 115 | only #{$media} and (min--moz-device-pixel-ratio: 1.5), 116 | only #{$media} and (-o-min-device-pixel-ratio: 3/2), 117 | only #{$media} and (-webkit-min-device-pixel-ratio: 1.5), 118 | only #{$media} and (min-device-pixel-ratio: 1.5), 119 | only #{$media} and (min-resolution: 144dpi), 120 | only #{$media} and (min-resolution: 1.5dppx) { 121 | 122 | @content; 123 | } 124 | } 125 | 126 | 127 | // and just because the term "retina" is so widely used, we'll create 128 | // a mixin that uses that as an alias 129 | @mixin retina($media: all) { 130 | @include hidpi($media) { @content; } 131 | } 132 | 133 | 134 | // rem: Calculate the rem unit, and return both pixels and rems 135 | // to support older (non-rem supporting) browsers 136 | @mixin rem($property, $value, $context: $default-font-size) { 137 | #{$property}: $value; 138 | #{$property}: cr($value, $context); 139 | } 140 | 141 | // Since the most common application of this is likely to be 142 | // with font-size, I've created a special mixin just for that 143 | // which will save you a whopping **5 characters** per time you 144 | // have to type it. Productivity FTW! 145 | @mixin font-size($font-size) { 146 | @include rem(font-size, $font-size); 147 | } 148 | 149 | 150 | // Functions 151 | 152 | // calc-rem: calculate the rem value based on the desired pixel 153 | // value versus a context value, usually the default font size 154 | $default-font-size: 16px !default; 155 | 156 | @function calc-rem( $target, $context: $default-font-size ) { 157 | @return $target / $context * 1rem; 158 | } 159 | 160 | // Shorthand redeclaration of the above 161 | @function cr( $target, $context: $default-font-size ) { 162 | @return calc-rem( $target, $context ); 163 | } 164 | 165 | 166 | // calc-percent: calculate the percent using the target ÷ context 167 | // formula, expressed as a percentage. See Chapter 2 of "Responsive 168 | // Web Design" (Marcotte, A Book Apart, 2011) 169 | @function calc-percent( $target, $context ) { 170 | @return percentage( $target / $context ); 171 | } 172 | 173 | // Shorthand redeclaration of the above 174 | @function cp( $target, $context ) { 175 | @return calc-percent( $target, $context ); 176 | } 177 | 178 | 179 | // calc-em: sometimes, you really need to use ems, not rems, 180 | // esp. when you're dealing with fonts that should be sized 181 | // relative to other items in a component, such as headers. 182 | // Remember to define your context! Your context will be 183 | // the font-size of the parent element. 184 | 185 | @function calc-em( $target, $context ) { 186 | @return $target / $context * 1em; 187 | } 188 | 189 | // Shorthand redeclaration of the above 190 | 191 | @function ce( $target, $context ) { 192 | @return calc-em( $target, $context ); 193 | } 194 | 195 | 196 | // hidpi-sprite-adjustment 197 | // when using Sass & Compass to generate sprites, there are issues with calculating the 198 | // position of the HiDPI sprites. See http://blog.teamtreehouse.com/?p=20925 (I'll update 199 | // this documentation when I get a chance to later.) this function makes the necessary 200 | // adjustment for you. 201 | @function hidpi-sprite-adjustment($sprite-map, $sprite) { 202 | @return round(nth(sprite-position($sprite-map, $sprite), 2) / 2); 203 | } 204 | 205 | // retina-sprite-adjustment 206 | // prefer the term "retina" over "hidpi"? here you go, this aliases the previous function 207 | @function retina-sprite-adjustment($sprite-map, $sprite) { 208 | @return hidpi-sprite-adjustment($sprite-map, $sprite); 209 | } -------------------------------------------------------------------------------- /src/renderer/style/_variables.scss: -------------------------------------------------------------------------------- 1 | // color 2 | $primary_color: hsl(145, 63%, 49%); 3 | $light_primary_color: hsl(145, 63%, 60%); 4 | $split_grey: #DDDDDD; 5 | $accent_color: hsl(145, 63%, 42%); 6 | $background_grey: #EEEEEE; 7 | $contrast_color: #f1c40f; 8 | $table_grey: #f7f7f7; 9 | $text_grey: #777777; 10 | 11 | $slider_track_bg: #bdbdbd; 12 | $slider_track_fill_color: hsl(145, 63%, 42%); 13 | $slider_thumb_color: hsl(145, 63%, 49%); 14 | 15 | $flat_button_positive_color: hsl(145, 63%, 49%); 16 | $flat_button_positive_light_color: lighten($flat_button_positive_color, 10%); 17 | $flat_button_positive_dark_color: darken($flat_button_positive_color, 10%); 18 | $flat_button_negative_color: #AAAAAA; 19 | $flat_button_negative_light_color: lighten($flat_button_negative_color, 10%); 20 | $flat_button_negative_dark_color: darken($flat_button_negative_color, 10%); 21 | $flat_button_plain_color: hsl(145, 63%, 42%); 22 | $flat_button_plain_light_color: lighten($flat_button_plain_color, 10%); 23 | $flat_button_plain_dark_color: darken($flat_button_plain_color, 10%); 24 | $flat_button_warning_color: #e74c3c; 25 | $flat_button_warning_light_color: lighten($flat_button_warning_color, 10%); 26 | $flat_button_warning_dark_color: darken($flat_button_warning_color, 10%); 27 | 28 | $switch_track_disabled_color: #bdbdbd; 29 | $switch_track_enabled_color: #71ca96; 30 | $switch_thumb_disabled_color: #f5f5f5; 31 | $switch_thumb_enabled_color: #006548; 32 | 33 | $top_bar_float_btn_bg: rgba(0, 0, 0, 0.5); 34 | $top_bar_float_btn_icon_color: rgba(255, 255, 255, 0.9); 35 | $top_bar_float_btn_hover_bg: rgba(255, 255, 255, 0.9); 36 | $top_bar_float_btn_hover_icon_color: rgba(0, 0, 0, 0.5); 37 | $top_bar_float_btn_active_bg: rgba(255, 255, 255, 0.2); 38 | $top_bar_float_btn_active_icon_color: rgba(0, 0, 0, 0.5); 39 | 40 | $pagination_icon_active_color: #c9cacf; 41 | $pagination_icon_disabled_color: rgba(#c9cacf, 0.6); 42 | $pagination_icon_hovered_color: white; 43 | $pagination_item_text_normal_color: #c9cacf; 44 | $pagination_item_text_actived__color: white; 45 | $pagination_item_text_hovered__color: white; 46 | $pagination_item_background_actived__color: hsl(145, 63%, 49%); 47 | $pagination_item_background_hovered__color: #777777; 48 | 49 | $page_view_thumb_mask_color: rgba(0, 0, 0, 0.5); 50 | $page_view_index_color: rgba(255, 255, 255, 0.5); 51 | $page_view_border_color: hsl(231, 6%, 36%); 52 | $page_view_info_color: white; 53 | $page_view_loading_btn_color: rgba(255, 255, 255, 0.8); 54 | $page_view_loading_btn_hovered_color: hsl(145, 63%, 60%); 55 | $page_view_loading_btn_actived_color: hsl(145, 63%, 30%); 56 | 57 | $reader_view_location_color: hsl(145, 63%, 42%); 58 | $reader_view_full_screen_color: hsl(145, 63%, 42%); 59 | $reader_view_full_screen_hovered_color: hsl(145, 63%, 60%); 60 | $reader_view_loading_color: rgba(255,255,255,0.1); 61 | 62 | $book_view_title_color: rgba(0, 0, 0, 0.8); 63 | $book_view_ehunter_tag_bg_color: hsl(145, 63%, 42%); 64 | $book_view_page_bg: white; 65 | $book_view_ehunter_tag_bg: hsl(145, 63%, 42%); 66 | $book_view_ehunter_tag_text_color: white; 67 | $book_view_end_page_text_color: rgba(0, 0, 0, 0.7); 68 | $book_view_pagination_bg: rgb(51, 51, 51); 69 | 70 | $modal_view_bg: rgba(0, 0, 0, 0.6); 71 | 72 | 73 | 74 | 75 | /* mussy */ 76 | $body_bg: #333333; // directly use in app.inject.js 77 | $img_container_color:hsl(235, 16%, 13%); 78 | $title_color:hsl(231, 6%, 80%); 79 | 80 | // thumb-view 81 | $thumb-view-width: 150px; 82 | $thumb-view-height: 160px; 83 | $indicator_color: white; 84 | $thumb-width: 100px; 85 | $thumb_scroll_view_bg: #444444; 86 | $thumb-view-margin: 4px; 87 | $header-bg: #2ecc71; 88 | $header-height: 40px; 89 | 90 | // popup view 91 | $popup_primary_color: hsl(145, 63%, 49%); 92 | $popup_alternate_text_color: white; 93 | $popup_text_color: hsla(0, 0%, 0%, .67); 94 | $popup_secondary_text_color: hsla(0, 0%, 0%, .54); 95 | $popup_addition_bg: hsla(0, 0%, 97%, 1); 96 | -------------------------------------------------------------------------------- /src/renderer/utils/DateUtil.js: -------------------------------------------------------------------------------- 1 | export default { 2 | getIntervalFromNow(date) { 3 | let now = new Date().getTime(); 4 | let start = date instanceof Date ? date.getTime() : date; 5 | let interval = now - start; 6 | if (interval < 60 * 1000) { // sec level 7 | return `${(interval / 1000).toFixed(0)}秒前`; 8 | } 9 | if (60 * 1000 <= interval && interval < 60 * 60 * 1000) { // min level 10 | return `${(interval / (60 * 1000)).toFixed(0)}分钟前`; 11 | } 12 | if (60 * 60 * 1000 <= interval && interval < 24 * 60 * 60 * 1000) { // hour level 13 | return `${(interval / (60 * 60 * 1000)).toFixed(0)}小时前`; 14 | } 15 | if (24 * 60 * 60 * 1000 && interval < 365 * 24 * 60 * 60 * 1000) { // day level 16 | return `${(interval / (24 * 60 * 60 * 1000)).toFixed(0)}天前`; 17 | } 18 | return `${(interval / (365 * 24 * 60 * 60 * 1000)).toFixed(0)}年前`; // year level 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/utils/DateWrapper.js: -------------------------------------------------------------------------------- 1 | export default class DateWrapper { 2 | constructor(date) { 3 | if (date) { 4 | this.date = date; 5 | } else { 6 | this.date = new Date(); 7 | } 8 | } 9 | 10 | _paddy(n, p, c) { 11 | let padChar = typeof c !== 'undefined' ? c : '0'; 12 | let pad = new Array(1 + p).join(padChar); 13 | return (pad + n).slice(-pad.length); 14 | } 15 | 16 | addDays(days) { 17 | this.date.setDate(this.date.getDate() + days); 18 | return this; 19 | } 20 | 21 | addMonths(month) { 22 | this.date.setMonth(this.date.getMonth() + month); 23 | return this; 24 | } 25 | 26 | addYears(Years) { 27 | this.date.setFullYear(this.date.getFullYear() + Years); 28 | return this; 29 | } 30 | 31 | getDate() { 32 | return this.date; 33 | } 34 | 35 | toString(pattern) { 36 | pattern = pattern || 'yyyy/MM/dd HH:mm:ss'; 37 | let month = this.date.getMonth() + 1 // begin from 0 38 | let day = this.date.getDate() // not getDay(), it's wrong 39 | let year = this.date.getFullYear(); 40 | let hour = this.date.getHours(); 41 | let min = this.date.getMinutes(); 42 | let sec = this.date.getSeconds(); 43 | pattern = pattern 44 | .replace('MM', this._paddy(month, 2)) 45 | .replace('dd', this._paddy(day, 2)) 46 | .replace('HH', this._paddy(hour, 2)) 47 | .replace('mm', this._paddy(min, 2)) 48 | .replace('ss', this._paddy(sec, 2)); 49 | if (pattern.includes('yyyy')) { 50 | pattern = pattern.replace('yyyy', year); 51 | } else if (pattern.includes('yy')) { 52 | pattern = pattern.replace('yy', year % 100); 53 | } 54 | return pattern; 55 | } 56 | 57 | toGMTString() { 58 | return this.date.toGMTString(); 59 | } 60 | 61 | setTimeFromDate(date) { 62 | this.date.setHours(date.getHours()); 63 | this.date.setMinutes(date.getMinutes()); 64 | this.date.setSeconds(date.getSeconds()); 65 | return this; 66 | } 67 | 68 | setDateFromDate(date) { 69 | this.date.setMonth(date.getMonth()); 70 | this.date.setDate(date.getDate()); 71 | this.date.setFullYear(date.getFullYear()); 72 | return this; 73 | } 74 | 75 | clearTime() { 76 | this.date.setHours(0); 77 | this.date.setMinutes(0); 78 | this.date.setSeconds(0); 79 | return this; 80 | } 81 | 82 | clearDay() { 83 | this.date.setDate(1); 84 | this.clearTime(); 85 | return this; 86 | } 87 | 88 | clearMonth() { 89 | this.date.setMonth(0); 90 | this.clearDay(); 91 | return this; 92 | } 93 | } -------------------------------------------------------------------------------- /src/renderer/utils/Logger.js: -------------------------------------------------------------------------------- 1 | class Logger { 2 | logText(tag, text) { 3 | console.log(`%c[${tag}] %c${text}`, 'color:red', 'color:black'); 4 | } 5 | 6 | logObj(tag, obj, str = false) { 7 | this.logText(tag, ':'); 8 | console.log(str ? JSON.parse(JSON.stringify(obj)) : obj); 9 | this.logText(tag, '----------'); 10 | } 11 | } 12 | 13 | let instance = new Logger(); 14 | export default instance; 15 | -------------------------------------------------------------------------------- /src/renderer/utils/MdRenderer.js: -------------------------------------------------------------------------------- 1 | import MarkdownIt from 'markdown-it'; 2 | import emoji from 'markdown-it-emoji'; 3 | import twemoji from 'twemoji'; 4 | 5 | class MdRenderer { 6 | constructor() { 7 | this.md = new MarkdownIt(); 8 | this.md.use(emoji, []); 9 | let defaultRender = 10 | this.md.renderer.rules.link_open || 11 | function(tokens, idx, options, env, self) { 12 | return self.renderToken(tokens, idx, options); 13 | }; 14 | this.md.renderer.rules.link_open = (tokens, idx, options, env, self) => { 15 | // If you are sure other plugins can't add `target` - drop check below 16 | var aIndex = tokens[idx].attrIndex('target'); 17 | 18 | if (aIndex < 0) { 19 | tokens[idx].attrPush(['target', '_blank']); // add new attribute 20 | } else { 21 | tokens[idx].attrs[aIndex][1] = '_blank'; // replace value of existing attr 22 | } 23 | // pass token to default renderer. 24 | return defaultRender(tokens, idx, options, env, self); 25 | }; 26 | this.md.renderer.rules.emoji = (token, idx) => twemoji.parse(token[idx].content); 27 | } 28 | 29 | render(text) { 30 | return this.md.render(text); 31 | } 32 | } 33 | 34 | let instance = new MdRenderer(); 35 | export default instance; 36 | -------------------------------------------------------------------------------- /src/renderer/utils/Utils.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | timeout(ms): Promise { 3 | return new Promise(resolve => setTimeout(resolve, ms)); 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /src/renderer/utils/VueUtil.js: -------------------------------------------------------------------------------- 1 | export default { 2 | methods: { 3 | px(num) { 4 | return `${num}px`; 5 | }, 6 | range(start, count) { 7 | return Array.apply(0, Array(count)) 8 | .map(function(element, index) { 9 | return index + start; 10 | }); 11 | } 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /src/renderer/utils/bezier-easing.js: -------------------------------------------------------------------------------- 1 | /** 2 | * https://github.com/gre/bezier-easing 3 | * BezierEasing - use bezier curve for transition easing function 4 | * by Gaëtan Renaudeau 2014 - 2015 – MIT License 5 | */ 6 | 7 | // These values are established by empiricism with tests (tradeoff: performance VS precision) 8 | var NEWTON_ITERATIONS = 4; 9 | var NEWTON_MIN_SLOPE = 0.001; 10 | var SUBDIVISION_PRECISION = 0.0000001; 11 | var SUBDIVISION_MAX_ITERATIONS = 10; 12 | 13 | var kSplineTableSize = 11; 14 | var kSampleStepSize = 1.0 / (kSplineTableSize - 1.0); 15 | 16 | var float32ArraySupported = typeof Float32Array === 'function'; 17 | 18 | function A(aA1, aA2) { 19 | return 1.0 - 3.0 * aA2 + 3.0 * aA1; 20 | } 21 | 22 | function B(aA1, aA2) { 23 | return 3.0 * aA2 - 6.0 * aA1; 24 | } 25 | 26 | function C(aA1) { 27 | return 3.0 * aA1; 28 | } 29 | 30 | // Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. 31 | function calcBezier(aT, aA1, aA2) { 32 | return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; 33 | } 34 | 35 | // Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. 36 | function getSlope(aT, aA1, aA2) { 37 | return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); 38 | } 39 | 40 | function binarySubdivide(aX, aA, aB, mX1, mX2) { 41 | var currentX, currentT, i = 0; 42 | do { 43 | currentT = aA + (aB - aA) / 2.0; 44 | currentX = calcBezier(currentT, mX1, mX2) - aX; 45 | if (currentX > 0.0) { 46 | aB = currentT; 47 | } else { 48 | aA = currentT; 49 | } 50 | } while (Math.abs(currentX) > SUBDIVISION_PRECISION && ++i < SUBDIVISION_MAX_ITERATIONS); 51 | return currentT; 52 | } 53 | 54 | function newtonRaphsonIterate(aX, aGuessT, mX1, mX2) { 55 | for (var i = 0; i < NEWTON_ITERATIONS; ++i) { 56 | var currentSlope = getSlope(aGuessT, mX1, mX2); 57 | if (currentSlope === 0.0) { 58 | return aGuessT; 59 | } 60 | var currentX = calcBezier(aGuessT, mX1, mX2) - aX; 61 | aGuessT -= currentX / currentSlope; 62 | } 63 | return aGuessT; 64 | } 65 | 66 | export default function bezier(mX1, mY1, mX2, mY2) { 67 | if (!(0 <= mX1 && mX1 <= 1 && 0 <= mX2 && mX2 <= 1)) { 68 | throw new Error('bezier x values must be in [0, 1] range'); 69 | } 70 | 71 | // Precompute samples table 72 | var sampleValues = float32ArraySupported ? new Float32Array(kSplineTableSize) : new Array(kSplineTableSize); 73 | if (mX1 !== mY1 || mX2 !== mY2) { 74 | for (var i = 0; i < kSplineTableSize; ++i) { 75 | sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2); 76 | } 77 | } 78 | 79 | function getTForX(aX) { 80 | var intervalStart = 0.0; 81 | var currentSample = 1; 82 | var lastSample = kSplineTableSize - 1; 83 | 84 | for (; currentSample !== lastSample && sampleValues[currentSample] <= aX; ++currentSample) { 85 | intervalStart += kSampleStepSize; 86 | } 87 | --currentSample; 88 | 89 | // Interpolate to provide an initial guess for t 90 | var dist = (aX - sampleValues[currentSample]) / (sampleValues[currentSample + 1] - sampleValues[currentSample]); 91 | var guessForT = intervalStart + dist * kSampleStepSize; 92 | 93 | var initialSlope = getSlope(guessForT, mX1, mX2); 94 | if (initialSlope >= NEWTON_MIN_SLOPE) { 95 | return newtonRaphsonIterate(aX, guessForT, mX1, mX2); 96 | } else if (initialSlope === 0.0) { 97 | return guessForT; 98 | } else { 99 | return binarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2); 100 | } 101 | } 102 | 103 | return function BezierEasing(x) { 104 | if (mX1 === mY1 && mX2 === mY2) { 105 | return x; // linear 106 | } 107 | // Because JavaScript number are imprecise, we should guarantee the extremes are right. 108 | if (x === 0) { 109 | return 0; 110 | } 111 | if (x === 1) { 112 | return 1; 113 | } 114 | return calcBezier(getTForX(x), mY1, mY2); 115 | }; 116 | }; 117 | -------------------------------------------------------------------------------- /src/renderer/utils/formatter.js: -------------------------------------------------------------------------------- 1 | export default { 2 | replaceKey(str, options) { 3 | for (let key in options) { 4 | let re = new RegExp("\\$\\$" + key + "\\$\\$", "g"); 5 | str = str.replace(re, options[key]); 6 | } 7 | return str; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/renderer/utils/react-native-storage/error.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by sunny on 9/1/16. 3 | */ 4 | 5 | export class NotFoundError { 6 | constructor(message) { 7 | this.name = 'NotFoundError'; 8 | this.message = `Not Found! Params: ${message}`; 9 | this.stack = new Error().stack; // Optional 10 | } 11 | } 12 | // NotFoundError.prototype = Object.create(Error.prototype); 13 | 14 | export class ExpiredError { 15 | constructor(message) { 16 | this.name = 'ExpiredError'; 17 | this.message = `Expired! Params: ${message}`; 18 | this.stack = new Error().stack; // Optional 19 | } 20 | } 21 | // ExpiredError.prototype = Object.create(Error.prototype); -------------------------------------------------------------------------------- /static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hanFengSan/eHunter_local/4e8be3242b586aa07bcf56f4b9da1e9d116da4d2/static/.gitkeep -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | }, 5 | "globals": { 6 | "assert": true, 7 | "expect": true, 8 | "should": true, 9 | "__static": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | Vue.config.devtools = false 3 | Vue.config.productionTip = false 4 | 5 | // require all test files (files that ends with .spec.js) 6 | const testsContext = require.context('./specs', true, /\.spec$/) 7 | testsContext.keys().forEach(testsContext) 8 | 9 | // require all src files except main.js for coverage. 10 | // you can also change this to match only the subset of files that 11 | // you want coverage for. 12 | const srcContext = require.context('../../src/renderer', true, /^\.\/(?!main(\.js)?$)/) 13 | srcContext.keys().forEach(srcContext) 14 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const path = require('path') 4 | const merge = require('webpack-merge') 5 | const webpack = require('webpack') 6 | 7 | const baseConfig = require('../../.electron-vue/webpack.renderer.config') 8 | const projectRoot = path.resolve(__dirname, '../../src/renderer') 9 | 10 | // Set BABEL_ENV to use proper preset config 11 | process.env.BABEL_ENV = 'test' 12 | 13 | let webpackConfig = merge(baseConfig, { 14 | devtool: '#inline-source-map', 15 | plugins: [ 16 | new webpack.DefinePlugin({ 17 | 'process.env.NODE_ENV': '"testing"' 18 | }) 19 | ] 20 | }) 21 | 22 | // don't treat dependencies as externals 23 | delete webpackConfig.entry 24 | delete webpackConfig.externals 25 | delete webpackConfig.output.libraryTarget 26 | 27 | // apply vue option to apply isparta-loader on js 28 | webpackConfig.module.rules 29 | .find(rule => rule.use.loader === 'vue-loader').use.options.loaders.js = 'babel-loader' 30 | 31 | module.exports = config => { 32 | config.set({ 33 | browsers: ['visibleElectron'], 34 | client: { 35 | useIframe: false 36 | }, 37 | coverageReporter: { 38 | dir: './coverage', 39 | reporters: [ 40 | { type: 'lcov', subdir: '.' }, 41 | { type: 'text-summary' } 42 | ] 43 | }, 44 | customLaunchers: { 45 | 'visibleElectron': { 46 | base: 'Electron', 47 | flags: ['--show'] 48 | } 49 | }, 50 | frameworks: ['mocha', 'chai'], 51 | files: ['./index.js'], 52 | preprocessors: { 53 | './index.js': ['webpack', 'sourcemap'] 54 | }, 55 | reporters: ['spec', 'coverage'], 56 | singleRun: true, 57 | webpack: webpackConfig, 58 | webpackMiddleware: { 59 | noInfo: true 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /test/unit/specs/LandingPage.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import LandingPage from '@/components/LandingPage' 3 | 4 | describe('LandingPage.vue', () => { 5 | it('should render correct contents', () => { 6 | const vm = new Vue({ 7 | el: document.createElement('div'), 8 | render: h => h(LandingPage) 9 | }).$mount() 10 | 11 | expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./built/", 4 | "sourceMap": true, 5 | "strict": true, 6 | "noImplicitReturns": true, 7 | "noImplicitAny": false, 8 | "module": "es2015", 9 | "moduleResolution": "node", 10 | "target": "es5", 11 | "lib": [ 12 | "es2015", 13 | "DOM" 14 | ] 15 | }, 16 | "include": [ 17 | "./src/**/*" 18 | ], 19 | "baseUrl": ".", 20 | "paths": { 21 | "src": "src" 22 | } 23 | } -------------------------------------------------------------------------------- /update.json: -------------------------------------------------------------------------------- 1 | { 2 | "cn":{ 3 | "title":"新版本: v1.1.0", 4 | "version":"1.1.0", 5 | "text":"自动翻页&手动输入设置&奇偶页切换等功能更新", 6 | "time":1543811938814, 7 | "duration":604800000, 8 | "always":true, 9 | "operations":[ 10 | { 11 | "name": "百度云下载(提取码:czft)", 12 | "url": "https://yun.baidu.com/s/1wEnBe9uGoBKzNd4DCfbuAg" 13 | }, 14 | { 15 | "name":"Github下载", 16 | "url":"https://github.com/hanFengSan/eHunter_local/releases" 17 | } 18 | ] 19 | }, 20 | "en":{ 21 | "title":"New version: v1.1.0", 22 | "version":"1.1.0", 23 | "text":"Multiple features updated", 24 | "time":1543811938814, 25 | "duration":604800000, 26 | "always":true, 27 | "operations":[ 28 | { 29 | "name":"Github下载", 30 | "url":"https://github.com/hanFengSan/eHunter_local/releases" 31 | } 32 | ] 33 | }, 34 | "jp":{ 35 | "title":"新しいバージョン: v1.1.0", 36 | "version":"1.1.0", 37 | "text":"Multiple features updated", 38 | "time":1543811938814, 39 | "duration":604800000, 40 | "always":true, 41 | "operations":[ 42 | { 43 | "name":"Github下载", 44 | "url":"https://github.com/hanFengSan/eHunter_local/releases" 45 | } 46 | ] 47 | } 48 | } --------------------------------------------------------------------------------