Showing preview only (1,515K chars total). Download the full file or copy to clipboard to get everything.
Repository: xiandanin/magnetW
Branch: master
Commit: 012a86b799b3
Files: 89
Total size: 1.4 MB
Directory structure:
gitextract_nfmp_la9/
├── .babelrc
├── .editorconfig
├── .electron-vue/
│ ├── build.js
│ ├── dev-client.js
│ ├── dev-runner.js
│ ├── webpack.main.config.js
│ ├── webpack.renderer.config.js
│ └── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── issue_template.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── build/
│ └── icons/
│ └── icon.icns
├── icns.sh
├── package.json
├── rule.json
├── scripts/
│ ├── build-service.js
│ └── merge-filter-db.js
├── src/
│ ├── index.ejs
│ ├── main/
│ │ ├── api.js
│ │ ├── axios.js
│ │ ├── cache.js
│ │ ├── defaultConfig.js
│ │ ├── electron-cache.js
│ │ ├── filter/
│ │ │ └── filter.js
│ │ ├── format-parser.js
│ │ ├── index.dev.js
│ │ ├── index.js
│ │ ├── ipc.js
│ │ ├── logger.js
│ │ ├── memory-cache.js
│ │ ├── menu.js
│ │ ├── middleware/
│ │ │ ├── block.js
│ │ │ └── response-template.js
│ │ ├── process-config.js
│ │ ├── repository.js
│ │ └── service.js
│ └── renderer/
│ ├── App.vue
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── fonts/
│ │ │ └── iconfont.css
│ │ └── scss/
│ │ ├── app.scss
│ │ └── element-variables.scss
│ ├── components/
│ │ ├── AsideMenu.vue
│ │ ├── BrowserButton.vue
│ │ ├── BrowserLink.vue
│ │ ├── DetailDialog.vue
│ │ ├── GithubBadge.vue
│ │ ├── GuidePage.vue
│ │ ├── HeaderVersion.vue
│ │ ├── HighlightName.vue
│ │ ├── ItemButtonGroup.vue
│ │ ├── NumberInput.vue
│ │ ├── PagerFooter.vue
│ │ ├── PagerHeader.vue
│ │ ├── PagerItems.vue
│ │ ├── QrcodePopover.vue
│ │ ├── Router.vue
│ │ ├── SearchInput.vue
│ │ ├── SearchPagination.vue
│ │ ├── SearchSort.vue
│ │ ├── ServerConfig.vue
│ │ ├── SettingGroup.vue
│ │ ├── SettingItem.vue
│ │ └── TooltipFormItem.vue
│ ├── main.js
│ ├── pages/
│ │ ├── Index.vue
│ │ ├── Main.vue
│ │ └── Setting.vue
│ ├── plugins/
│ │ ├── app.js
│ │ ├── axios.js
│ │ ├── clipboard.js
│ │ ├── config.js
│ │ ├── element-ui.js
│ │ ├── event-proxy.js
│ │ ├── filter.js
│ │ ├── ga.js
│ │ ├── head.js
│ │ ├── index.js
│ │ ├── localsetting.js
│ │ ├── localstorage.js
│ │ └── menu.js
│ └── router/
│ └── index.js
└── static/
├── .gitkeep
├── keywords.txt
└── robots.txt
================================================
FILE CONTENTS
================================================
================================================
FILE: .babelrc
================================================
{
"comments": false,
"env": {
"main": {
"presets": [
[
"env",
{
"targets": {
"node": 7
}
}
],
"stage-0"
]
},
"renderer": {
"presets": [
[
"env",
{
"modules": false
}
],
"stage-0"
]
},
"web": {
"presets": [
[
"env",
{
"modules": false
}
],
"stage-0"
]
}
},
"plugins": [
"transform-runtime",
"transform-es2015-modules-commonjs",
"transform-async-to-generator"
]
}
================================================
FILE: .editorconfig
================================================
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
================================================
FILE: .electron-vue/build.js
================================================
'use strict'
process.env.NODE_ENV = 'production'
const { say } = require('cfonts')
const chalk = require('chalk')
const del = require('del')
const { spawn } = require('child_process')
const webpack = require('webpack')
const Multispinner = require('multispinner')
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
const webConfig = require('./webpack.web.config')
const doneLog = chalk.bgGreen.white(' DONE ') + ' '
const errorLog = chalk.bgRed.white(' ERROR ') + ' '
const okayLog = chalk.bgBlue.white(' OKAY ') + ' '
const isCI = process.env.CI || false
if (process.env.BUILD_TARGET === 'clean') clean()
else if (process.env.BUILD_TARGET === 'web') web()
else build()
function clean () {
del.sync(['build/*', '!build/icons', '!build/icons/icon.*'])
console.log(`\n${doneLog}\n`)
process.exit()
}
function build () {
greeting()
del.sync(['dist/electron/*', '!.gitkeep'])
const tasks = ['main', 'renderer']
const m = new Multispinner(tasks, {
preText: 'building',
postText: 'process'
})
let results = ''
m.on('success', () => {
process.stdout.write('\x1B[2J\x1B[0f')
console.log(`\n\n${results}`)
console.log(`${okayLog}take it away ${chalk.yellow('`electron-builder`')}\n`)
process.exit()
})
pack(mainConfig).then(result => {
results += result + '\n\n'
m.success('main')
}).catch(err => {
m.error('main')
console.log(`\n ${errorLog}failed to build main process`)
console.error(`\n${err}\n`)
process.exit(1)
})
pack(rendererConfig).then(result => {
results += result + '\n\n'
m.success('renderer')
}).catch(err => {
m.error('renderer')
console.log(`\n ${errorLog}failed to build renderer process`)
console.error(`\n${err}\n`)
process.exit(1)
})
}
function pack (config) {
return new Promise((resolve, reject) => {
config.mode = 'production'
webpack(config, (err, stats) => {
if (err) reject(err.stack || err)
else if (stats.hasErrors()) {
let err = ''
stats.toString({
chunks: false,
colors: true
})
.split(/\r?\n/)
.forEach(line => {
err += ` ${line}\n`
})
reject(err)
} else {
resolve(stats.toString({
chunks: false,
colors: true
}))
}
})
})
}
function web () {
del.sync(['dist/web/*', '!.gitkeep'])
webConfig.mode = 'production'
webpack(webConfig, (err, stats) => {
if (err || stats.hasErrors()) console.log(err)
console.log(stats.toString({
chunks: false,
colors: true
}))
process.exit()
})
}
function greeting () {
const cols = process.stdout.columns
let text = ''
if (cols > 85) text = 'lets-build'
else if (cols > 60) text = 'lets-|build'
else text = false
if (text && !isCI) {
say(text, {
colors: ['yellow'],
font: 'simple3d',
space: false
})
} else console.log(chalk.yellow.bold('\n lets-build'))
console.log()
}
================================================
FILE: .electron-vue/dev-client.js
================================================
const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')
hotClient.subscribe(event => {
/**
* Reload browser when HTMLWebpackPlugin emits a new index.html
*
* Currently disabled until jantimon/html-webpack-plugin#680 is resolved.
* https://github.com/SimulatedGREG/electron-vue/issues/437
* https://github.com/jantimon/html-webpack-plugin/issues/680
*/
// if (event.action === 'reload') {
// window.location.reload()
// }
/**
* Notify `mainWindow` when `main` process is compiling,
* giving notice for an expected reload of the `electron` process
*/
if (event.action === 'compiling') {
document.body.innerHTML += `
<style>
#dev-client {
background: #4fc08d;
border-radius: 4px;
bottom: 20px;
box-shadow: 0 4px 5px 0 rgba(0, 0, 0, 0.14), 0 1px 10px 0 rgba(0, 0, 0, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.3);
color: #fff;
font-family: 'Source Sans Pro', sans-serif;
left: 20px;
padding: 8px 12px;
position: absolute;
}
</style>
<div id="dev-client">
Compiling Main Process...
</div>
`
}
})
================================================
FILE: .electron-vue/dev-runner.js
================================================
'use strict'
const chalk = require('chalk')
const electron = require('electron')
const path = require('path')
const { say } = require('cfonts')
const { spawn } = require('child_process')
const webpack = require('webpack')
const WebpackDevServer = require('webpack-dev-server')
const webpackHotMiddleware = require('webpack-hot-middleware')
const mainConfig = require('./webpack.main.config')
const rendererConfig = require('./webpack.renderer.config')
let electronProcess = null
let manualRestart = false
let hotMiddleware
function logStats (proc, data) {
let log = ''
log += chalk.yellow.bold(`┏ ${proc} Process ${new Array((19 - proc.length) + 1).join('-')}`)
log += '\n\n'
if (typeof data === 'object') {
data.toString({
colors: true,
chunks: false
}).split(/\r?\n/).forEach(line => {
log += ' ' + line + '\n'
})
} else {
log += ` ${data}\n`
}
log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n'
console.log(log)
}
function startRenderer () {
return new Promise((resolve, reject) => {
rendererConfig.entry.renderer = [path.join(__dirname, 'dev-client')].concat(rendererConfig.entry.renderer)
rendererConfig.mode = 'development'
const compiler = webpack(rendererConfig)
hotMiddleware = webpackHotMiddleware(compiler, {
log: false,
heartbeat: 2500
})
compiler.hooks.compilation.tap('compilation', compilation => {
compilation.hooks.htmlWebpackPluginAfterEmit.tapAsync('html-webpack-plugin-after-emit', (data, cb) => {
hotMiddleware.publish({ action: 'reload' })
cb()
})
})
compiler.hooks.done.tap('done', stats => {
logStats('Renderer', stats)
})
const server = new WebpackDevServer(
compiler,
{
contentBase: path.join(__dirname, '../'),
quiet: true,
before (app, ctx) {
app.use(hotMiddleware)
ctx.middleware.waitUntilValid(() => {
resolve()
})
}
}
)
server.listen(9080)
})
}
function startMain () {
return new Promise((resolve, reject) => {
mainConfig.entry.main = [path.join(__dirname, '../src/main/index.dev.js')].concat(mainConfig.entry.main)
mainConfig.mode = 'development'
const compiler = webpack(mainConfig)
compiler.hooks.watchRun.tapAsync('watch-run', (compilation, done) => {
logStats('Main', chalk.white.bold('compiling...'))
hotMiddleware.publish({ action: 'compiling' })
done()
})
compiler.watch({}, (err, stats) => {
if (err) {
console.log(err)
return
}
logStats('Main', stats)
if (electronProcess && electronProcess.kill) {
manualRestart = true
process.kill(electronProcess.pid)
electronProcess = null
startElectron()
setTimeout(() => {
manualRestart = false
}, 5000)
}
resolve()
})
})
}
function startElectron () {
var args = [
'--inspect=5858',
path.join(__dirname, '../dist/electron/main.js')
]
// detect yarn or npm and process commandline args accordingly
if (process.env.npm_execpath.endsWith('yarn.js')) {
args = args.concat(process.argv.slice(3))
} else if (process.env.npm_execpath.endsWith('npm-cli.js')) {
args = args.concat(process.argv.slice(2))
}
electronProcess = spawn(electron, args)
electronProcess.stdout.on('data', data => {
electronLog(data, 'blue')
})
electronProcess.stderr.on('data', data => {
electronLog(data, 'red')
})
electronProcess.on('close', () => {
if (!manualRestart) process.exit()
})
}
function electronLog (data, color) {
let log = ''
data = data.toString().split(/\r?\n/)
data.forEach(line => {
log += ` ${line}\n`
})
if (/[0-9A-z]+/.test(log)) {
console.log(
chalk[color].bold('┏ Electron -------------------') +
'\n\n' +
log +
chalk[color].bold('┗ ----------------------------') +
'\n'
)
}
}
function greeting () {
const cols = process.stdout.columns
let text = ''
if (cols > 104) text = 'electron-vue'
else if (cols > 76) text = 'electron-|vue'
else text = false
if (text) {
say(text, {
colors: ['yellow'],
font: 'simple3d',
space: false
})
} else console.log(chalk.yellow.bold('\n electron-vue'))
console.log(chalk.blue(' getting ready...') + '\n')
}
function init () {
greeting()
Promise.all([startRenderer(), startMain()])
.then(() => {
startElectron()
})
.catch(err => {
console.error(err)
})
}
init()
================================================
FILE: .electron-vue/webpack.main.config.js
================================================
'use strict'
process.env.BABEL_ENV = 'main'
const path = require('path')
const { dependencies } = require('../package.json')
const webpack = require('webpack')
const MinifyPlugin = require("babel-minify-webpack-plugin")
let mainConfig = {
entry: {
main: path.join(__dirname, '../src/main/index.js')
},
externals: [
...Object.keys(dependencies || {})
],
module: {
rules: [
{
test: /\.(js)$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
}
]
},
node: {
__dirname: process.env.NODE_ENV !== 'production',
__filename: process.env.NODE_ENV !== 'production'
},
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
plugins: [
new webpack.NoEmitOnErrorsPlugin()
],
resolve: {
extensions: ['.js', '.json', '.node']
},
target: 'electron-main'
}
/**
* Adjust mainConfig for development settings
*/
if (process.env.NODE_ENV !== 'production') {
mainConfig.plugins.push(
new webpack.DefinePlugin({
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
})
)
}
/**
* Adjust mainConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
mainConfig.plugins.push(
new MinifyPlugin(),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
})
)
}
module.exports = mainConfig
================================================
FILE: .electron-vue/webpack.renderer.config.js
================================================
'use strict'
process.env.BABEL_ENV = 'renderer'
const path = require('path')
const {dependencies} = require('../package.json')
const webpack = require('webpack')
const MinifyPlugin = require('babel-minify-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {VueLoaderPlugin} = require('vue-loader')
/**
* List of node_modules to include in webpack bundle
*
* Required for specific packages like Vue UI libraries
* that provide pure *.vue files that need compiling
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/webpack-configurations.html#white-listing-externals
*/
let whiteListedModules = ['vue', 'element-ui']
let rendererConfig = {
devtool: '#cheap-module-eval-source-map',
entry: {
renderer: path.join(__dirname, '../src/renderer/main.js')
},
externals: [
...Object.keys(dependencies || {}).filter(d => !whiteListedModules.includes(d))
],
module: {
rules: [
{
test: /\.(js|vue)$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
{
test: /\.scss$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader',
{
loader: 'sass-resources-loader',
options: {
resources: path.resolve(__dirname, '../src/renderer/assets/scss/app.scss')
}
}
]
},
{
test: /\.sass$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
},
{
test: /\.less$/,
use: ['vue-style-loader', 'css-loader', 'less-loader']
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.html$/,
use: 'vue-html-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.node$/,
use: 'node-loader'
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: process.env.NODE_ENV === 'production',
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader',
less: 'vue-style-loader!css-loader!less-loader'
}
}
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'imgs/[name]--[folder].[ext]'
}
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: 'media/[name]--[folder].[ext]'
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'fonts/[name]--[folder].[ext]'
}
}
}
]
},
node: {
__dirname: process.env.NODE_ENV !== 'production',
__filename: process.env.NODE_ENV !== 'production'
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({filename: 'styles.css'}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
title: process.env.npm_package_build_productName,
description: process.env.npm_package_description,
templateParameters (compilation, assets, options) {
return {
compilation: compilation,
webpack: compilation.getStats().toJson(),
webpackConfig: compilation.options,
htmlWebpackPlugin: {
files: assets,
options: options
},
process,
}
},
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
nodeModules: process.env.NODE_ENV !== 'production'
? path.resolve(__dirname, '../node_modules')
: false
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
output: {
filename: '[name].js',
libraryTarget: 'commonjs2',
path: path.join(__dirname, '../dist/electron')
},
resolve: {
alias: {
'@': path.join(__dirname, '../src/renderer'),
'vue$': 'vue/dist/vue.esm.js',
'~': path.resolve(),
},
extensions: ['.js', '.vue', '.json', '.css', '.node']
},
target: 'electron-renderer'
}
/**
* Adjust rendererConfig for development settings
*/
if (process.env.NODE_ENV !== 'production') {
rendererConfig.plugins.push(
new webpack.DefinePlugin({
'__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
})
)
}
/**
* Adjust rendererConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
rendererConfig.devtool = ''
rendererConfig.plugins.push(
new MinifyPlugin(),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/electron/static'),
ignore: ['.*']
}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
module.exports = rendererConfig
================================================
FILE: .electron-vue/webpack.web.config.js
================================================
'use strict'
process.env.BABEL_ENV = 'web'
const path = require('path')
const webpack = require('webpack')
const MinifyPlugin = require('babel-minify-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const {VueLoaderPlugin} = require('vue-loader')
let webConfig = {
devtool: '#cheap-module-eval-source-map',
entry: {
web: path.join(__dirname, '../src/renderer/main.js')
},
module: {
rules: [
{
test: /\.(js|vue)$/,
enforce: 'pre',
exclude: /node_modules/,
use: {
loader: 'eslint-loader',
options: {
formatter: require('eslint-friendly-formatter')
}
}
},
{
test: /\.scss$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader']
},
{
test: /\.sass$/,
use: ['vue-style-loader', 'css-loader', 'sass-loader?indentedSyntax']
},
{
test: /\.less$/,
use: ['vue-style-loader', 'css-loader', 'less-loader']
},
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader']
},
{
test: /\.html$/,
use: 'vue-html-loader'
},
{
test: /\.js$/,
use: 'babel-loader',
include: [path.resolve(__dirname, '../src/renderer')],
exclude: /node_modules/
},
{
test: /\.vue$/,
use: {
loader: 'vue-loader',
options: {
extractCSS: true,
loaders: {
sass: 'vue-style-loader!css-loader!sass-loader?indentedSyntax=1',
scss: 'vue-style-loader!css-loader!sass-loader',
less: 'vue-style-loader!css-loader!less-loader'
}
}
}
},
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'imgs/[name].[ext]'
}
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
use: {
loader: 'url-loader',
query: {
limit: 10000,
name: 'fonts/[name].[ext]'
}
}
}
]
},
plugins: [
new VueLoaderPlugin(),
new MiniCssExtractPlugin({filename: 'styles.css'}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.resolve(__dirname, '../src/index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true
},
nodeModules: false
}),
new webpack.DefinePlugin({
'process.env.IS_WEB': 'true'
}),
new webpack.HotModuleReplacementPlugin(),
new webpack.NoEmitOnErrorsPlugin()
],
output: {
filename: '[name].js',
path: path.join(__dirname, '../dist/web')
},
resolve: {
alias: {
'@': path.join(__dirname, '../src/renderer'),
'vue$': 'vue/dist/vue.esm.js'
},
extensions: ['.js', '.vue', '.json', '.css']
},
target: 'web'
}
/**
* Adjust webConfig for production settings
*/
if (process.env.NODE_ENV === 'production') {
webConfig.devtool = ''
webConfig.plugins.push(
new MinifyPlugin(),
new CopyWebpackPlugin([
{
from: path.join(__dirname, '../static'),
to: path.join(__dirname, '../dist/web/static'),
ignore: ['.*']
}
]),
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.LoaderOptionsPlugin({
minimize: true
})
)
}
module.exports = webConfig
================================================
FILE: .eslintignore
================================================
================================================
FILE: .eslintrc.js
================================================
// https://eslint.org/docs/user-guide/configuring
module.exports = {
root: true,
parserOptions: {
'parser': 'babel-eslint',
'ecmaVersion': 2017,
'sourceType': 'module'
},
env: {
node: true,
browser: true,
},
extends: [
// https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention
// consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
'plugin:vue/essential',
// https://github.com/standard/standard/blob/master/docs/RULES-en.md
'standard'
],
globals: {
__static: true
},
plugins: [
'vue'
],
// add your custom rules here
rules: {
// 关闭缩进检查
'indent': 0,
'vue/require-v-for-key': 'off',
'no-unused-vars': 'off',
'vue/no-unused-components': 'off',
'vue/valid-v-for': 'warn',
// allow async-await
'generator-star-spacing': 'off',
// allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
================================================
FILE: .github/ISSUE_TEMPLATE/issue_template.md
================================================
---
name: issue_template
about: 提交issue前请先确认文档是否已有答案
title: ''
labels: ''
assignees: ''
---
>操作系统版本:
>应用版本:
#### 问题描述:
#### 异常日志(上传文件到这里):
```
```
================================================
FILE: .gitignore
================================================
.DS_Store
dist/electron/*
dist/web/*
build/*
!build/icons
node_modules/
npm-debug.log
npm-debug.log.*
thumbs.db
!.gitkeep
workspace.xml
src/dist
Project_Default.xml
package-lock.json
.idea
scripts/filter-data/.temp
build/icons/.icns.iconset
================================================
FILE: .travis.yml
================================================
osx_image: xcode8.3
sudo: required
dist: trusty
language: c
matrix:
include:
- os: osx
- os: linux
env: CC=clang CXX=clang++ npm_config_clang=1
compiler: clang
cache:
directories:
- node_modules
- "$HOME/.electron"
- "$HOME/.cache"
addons:
apt:
packages:
- libgnome-keyring-dev
- icnsutils
before_install:
- mkdir -p /tmp/git-lfs && curl -L https://github.com/github/git-lfs/releases/download/v1.2.1/git-lfs-$([
"$TRAVIS_OS_NAME" == "linux" ] && echo "linux" || echo "darwin")-amd64-1.2.1.tar.gz
| tar -xz -C /tmp/git-lfs --strip-components 1 && /tmp/git-lfs/git-lfs pull
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --no-install-recommends -y icnsutils graphicsmagick xz-utils; fi
install:
- nvm install 10
- curl -o- -L https://yarnpkg.com/install.sh | bash
- source ~/.bashrc
- npm install -g xvfb-maybe
- yarn
script:
- yarn run build
branches:
only:
- master
================================================
FILE: LICENSE
================================================
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
================================================
FILE: README.md
================================================
# [已失效,不再维护]
[](https://github.com/xiandanin/magnetW/releases)
[](https://github.com/xiandanin/magnetW/stars)
[](https://github.com/xiandanin/magnetW/forks)
[](https://github.com/xiandanin/magnetW/blob/master/LICENSE)

[[中文文档]](https://github.com/xiandanin/magnetW/wiki)</font>
## 安装
从[Github Releases](https://github.com/xiandanin/magnetW/releases)或者[Github Wiki](https://github.com/xiandanin/magnetW/wiki)下载对应平台
## 注意事项
* 本项目**没有任何群组**,代码托管和相关技术交流只有**Github**,其它地址获取的都有可能存在风险,请**仔细辨别**
* 本项目**开源且免费**,**没有捐款**等任何形式的收款渠道,**没有任何形式**的广告,如果你遇到上述情况,请**不要相信**
## 免责声明
本应用开源且免费,仅用于爬虫技术交流学习,搜索结果均来自源站,亦不承担任何责任
## 版权说明
* 项目中所使用的图标分别来自[@玥月](https://www.iconfont.cn/user/detail?uid=8898)、[@qqavh147](https://www.iconfont.cn/user/detail?uid=158352)
* 项目中规则原理来自[magnetX](https://github.com/youusername/magnetX)
## 开源协议
项目遵循GNU General Public License v3.0,如果要修改源码二次开发还需遵守以下协议:
1. 如果要在网络上分发,那么必须开源
2. 不能以盈利为目的,不能插入任何形式的广告
3. 注明原项目出处
4. 继承相同协议
================================================
FILE: appveyor.yml
================================================
version: 0.1.{build}
branches:
only:
- master
image: Visual Studio 2017
platform:
- x64
cache:
- node_modules
- '%APPDATA%\npm-cache'
- '%USERPROFILE%\.electron'
- '%USERPROFILE%\AppData\Local\Yarn\cache'
init:
- git config --global core.autocrlf input
install:
- ps: Install-Product node 8 x64
- git reset --hard HEAD
- yarn
- node --version
build_script:
- yarn build
test: off
================================================
FILE: icns.sh
================================================
#!/usr/bin/env bash
# brew install icoutils
filepath=256x256.png
iconset=.icns.iconset
cd build/icons
if [ ! -d $iconset ];then
mkdir $iconset
fi
sips -z 16 16 $filepath --out $iconset/icon_16x16.png
sips -z 32 32 $filepath --out $iconset/icon_16x16@2x.png
sips -z 32 32 $filepath --out $iconset/icon_32x32.png
sips -z 64 64 $filepath --out $iconset/icon_32x32@2x.png
sips -z 128 128 $filepath --out $iconset/icon_128x128.png
sips -z 256 256 $filepath --out $iconset/icon_128x128@2x.png
sips -z 256 256 $filepath --out $iconset/icon_256x256.png
sips -z 512 512 $filepath --out $iconset/icon_256x256@2x.png
sips -z 512 512 $filepath --out $iconset/icon_512x512.png
sips -z 1024 1024 $filepath --out $iconset/icon_512x512@2x.png
iconutil -c icns $iconset -o icon.icns
icotool -c $iconset/icon_256x256.png -o icon.ico
================================================
FILE: package.json
================================================
{
"name": "magnetw",
"version": "3.1.1",
"description": "磁力链接聚合搜索",
"license": "GNU General Public License v3.0",
"main": "./dist/electron/main.js",
"scripts": {
"build": "cross-env BUILD_TARGET=electron node .electron-vue/build.js && electron-builder -mwl",
"build:dir": "cross-env BUILD_TARGET=electron node .electron-vue/build.js && electron-builder --dir",
"build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js",
"build:mac": "cross-env BUILD_TARGET=electron node .electron-vue/build.js && electron-builder --mac",
"build:win": "cross-env BUILD_TARGET=electron node .electron-vue/build.js && electron-builder --win",
"build:service": "node scripts/build-service.js",
"dev": "cross-env BUILD_TARGET=electron node .electron-vue/dev-runner.js",
"lint": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter src",
"lint:fix": "eslint --ext .js,.vue -f ./node_modules/eslint-friendly-formatter --fix src",
"pack": "npm run pack:main && npm run pack:renderer",
"pack:main": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.main.config.js",
"pack:renderer": "cross-env NODE_ENV=production webpack --progress --colors --config .electron-vue/webpack.renderer.config.js",
"postinstall": "npm run lint:fix",
"ins": "npm install --no-optional",
"merge-filter-db": "node scripts/merge-filter-db.js"
},
"build": {
"productName": "magnetW",
"appId": "app.magnetw.desktop",
"artifactName": "${name}-${version}-${os}.${ext}",
"directories": {
"output": "build/releases"
},
"files": [
"dist/electron/**/*"
],
"dmg": {
"contents": [
{
"x": 410,
"y": 150,
"type": "link",
"path": "/Applications"
},
{
"x": 130,
"y": 150,
"type": "file"
}
]
},
"mac": {
"icon": "build/icons/icon.icns"
},
"win": {
"icon": "build/icons/icon.ico",
"target": [
"nsis",
"zip"
]
},
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"perMachine": true
},
"linux": {
"icon": "build/icons"
}
},
"dependencies": {
"@chenfengyuan/vue-qrcode": "^1.0.1",
"@koa/cors": "^2.2.3",
"axios": "^0.19.1",
"blueimp-md5": "^2.12.0",
"electron-is": "^3.0.0",
"electron-log": "^3.0.9",
"electron-store": "^5.1.0",
"element-ui": "^2.13.0",
"htmlparser2": "^4.0.0",
"js-base64": "^2.5.1",
"koa": "^2.11.0",
"koa-bodyparser": "^4.2.1",
"koa-router": "^7.4.0",
"koa-static": "^5.0.0",
"memory-cache": "^0.2.0",
"moment": "^2.24.0",
"request": "^2.88.0",
"request-promise-native": "^1.0.7",
"socks-proxy-agent": "^4.0.2",
"tunnel": "0.0.6",
"urijs": "^1.19.2",
"vue": "^2.6.11",
"vue-clipboard2": "^0.3.1",
"vue-electron": "^1.0.6",
"vue-event-proxy": "^1.0.5",
"vue-head": "^2.1.2",
"vue-localstorage": "^0.6.2",
"vue-router": "^3.0.1",
"xmldom": "^0.1.27",
"xpath": "0.0.27"
},
"devDependencies": {
"@babel/plugin-transform-async-to-generator": "^7.7.4",
"ajv": "^6.5.0",
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.3",
"babel-loader": "^7.1.4",
"babel-minify-webpack-plugin": "^0.3.1",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"babel-register": "^6.26.0",
"cfonts": "^2.1.2",
"chalk": "^2.4.1",
"copy-webpack-plugin": "^4.5.1",
"cross-env": "^5.1.6",
"css-loader": "^0.28.11",
"del": "^3.0.0",
"devtron": "^1.4.0",
"electron": "^7.1.8",
"electron-builder": "^22.2.0",
"electron-debug": "^3.0.0",
"electron-devtools-installer": "^2.2.4",
"element-ui": "^2.13.0",
"eslint": "^4.19.1",
"eslint-config-standard": "^11.0.0",
"eslint-friendly-formatter": "^4.0.1",
"eslint-loader": "^2.0.0",
"eslint-plugin-html": "^4.0.3",
"eslint-plugin-import": "^2.12.0",
"eslint-plugin-node": "^6.0.1",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-standard": "^3.1.0",
"eslint-plugin-vue": "^6.0.1",
"file-loader": "^1.1.11",
"findit2": "^2.2.3",
"fs-extra": "^8.1.0",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "0.4.0",
"multispinner": "^0.2.1",
"node-loader": "^0.6.0",
"node-sass": "^4.9.2",
"nodemon": "^2.0.2",
"sass-loader": "^7.0.3",
"sass-resources-loader": "^2.0.1",
"style-loader": "^0.21.0",
"terser": "^4.4.2",
"uglify-js": "^3.7.2",
"url-loader": "^1.0.1",
"vue-html-loader": "^1.2.4",
"vue-loader": "^15.2.4",
"vue-style-loader": "^4.1.0",
"vue-template-compiler": "^2.5.16",
"webpack": "^4.15.1",
"webpack-cli": "^3.0.8",
"webpack-dev-server": "^3.1.4",
"webpack-hot-middleware": "^2.22.2",
"webpack-merge": "^4.1.3"
}
}
================================================
FILE: rule.json
================================================
[
{
"id": "zhongzisou",
"name": "种子搜",
"proxy": false,
"url": "https://zhongzidi1.com",
"paths": {
"time": "/list/{k}/{p}",
"size": "/list_length/{k}/{p}",
"hot": "/list_click/{k}/{p}"
},
"xpath": {
"group": "//table[@class='table table-bordered table-striped']",
"magnet": "./tbody/tr[1]/td/div/h4/a/@href",
"name": "./tbody/tr[1]/td/div/h4/a",
"size": "./tbody/tr/td[2]/strong",
"hot": "./tbody/tr[2]/td[3]/strong",
"date": "./tbody/tr/td[1]/strong",
"detail": {
"files": "//*[@id=\"wrapp\"]/div[2]/div/div/div[3]/div[1]/div[2]/div[2]/select/option"
}
}
},
{
"id": "btgg",
"name": "BTGG",
"proxy": false,
"url": "https://www.btgg.cc",
"paths": {
"preset": "/search?q={k}&p={p}"
},
"xpath": {
"group": "//div[@class=\"item\"]",
"magnet": "./div[2]/a/@href",
"name": "./div[1]/a",
"size": "./div[2]/span[1]",
"hot": "./div[2]/span[2]",
"date": "./div[2]/span[3]"
}
},
{
"id": "idope",
"name": "idope",
"proxy": true,
"url": "https://idope.se",
"paths": {
"preset": "/torrent-list/{k}/?p={p}",
"size": "/torrent-list/{k}/?p={p}&o=-2"
},
"xpath": {
"group": "//div[@class=\"resultdiv\"]",
"magnet": "./div[1]/a/@href",
"name": "./div[1]/a",
"size": "./div[2]/div[3]/div[2]",
"date": "./div[2]/div[2]/div[2]",
"detail": {
"files": "//div[@class=\"filetag\"]"
}
}
},
{
"id": "btsow",
"name": "BTSOW",
"proxy": false,
"url": "https://btsow.cyou",
"paths": {
"preset": "/search/{k}/page/{p}"
},
"xpath": {
"group": "//div[@class='row']",
"magnet": "./a/@href",
"name": "./a/div[1]",
"size": "./div[1]",
"date": "./div[2]",
"detail": {
"files": "//div[@class=\"detail data-list\"]/div/text()"
}
}
},
{
"id": "btdet",
"name": "BT蚂蚁",
"proxy": false,
"url": "https://btmayi.iproxy.gq",
"paths": {
"time": "/search/{k}-first-asc-{p}",
"size": "/search/{k}-size-desc-{p}",
"hot": "/search/{k}-hot-desc-{p}"
},
"xpath": {
"group": "//div[@id='wall']/div[@class='search-item']",
"magnet": "./div[@class='item-title']/a/@href",
"name": "./div[@class='item-title']/a",
"size": "./div[@class='item-bar']/span[4]/b",
"date": "./div[@class='item-bar']/span[1]/b",
"hot": "./div[@class='item-bar']/span[2]/b",
"detail": {
"files": "//*[@id=\"wall\"]/ol/li"
}
}
},
{
"id": "bt4g",
"name": "BT4G",
"proxy": false,
"url": "https://bt4g.proxyit.ga",
"paths": {
"time": "/search/{k}/{p}",
"size": "/search/{k}/bysize/{p}",
"hot": "/search/{k}/byrequests/{p}"
},
"xpath": {
"group": "/html/body/main/div/div[3]/div/div",
"magnet": "./h5/a/@href",
"name": "./h5/a",
"size": "./span[4]/b",
"date": "./span[2]/b",
"detail": {
"files": "//*[@id=\"wall\"]/div[6]/div[2]/ol/li"
}
}
},
{
"id": "btdb",
"name": " BTDB",
"proxy": false,
"url": "https://btdb.proxyit.ga",
"paths": {
"time": "?s={k}&sort=time&page={p}",
"preset": "?s={k}&page={p}",
"size": "?s={k}&sort=length&page={p}"
},
"xpath": {
"group": "//li[@class=\"search-ret-item\"]",
"name": "./h2/a",
"magnet": "./div/a/@href",
"size": "./div/span[1]",
"date": "./div/span[3]",
"hot": "./div/span[4]"
}
},
{
"id": "btdiguo",
"name": "BT目录",
"proxy": false,
"url": "https://91bt.info",
"paths": {
"time": "/search-update/{k}/page-{p}.html",
"size": "/search-size/{k}/page-{p}.html",
"hot": "/search-hot/{k}/page-{p}.html"
},
"xpath": {
"group": "//article[@class=\"item\"]/div",
"magnet": "./a/@href",
"name": "./a",
"size": "./p[1]",
"date": "./p[1]",
"hot": "./p[1]"
}
},
{
"id": "cilibao",
"name": "磁力宝",
"proxy": false,
"url": "http://cilibao.me",
"paths": {
"time": "/s/{k}_time_{p}.html",
"size": "/s/{k}_size_{p}.html",
"preset": "/s/{k}.html"
},
"xpath": {
"group": "//div[@class='search-item']",
"magnet": "./div[1]/h3[1]/a[1]/@href",
"name": "./div[1]/h3[1]/a[1]",
"size": "./div[3]/span[3]/b[1]",
"date": "./div[3]/span[2]/b[1]",
"hot": "./div[3]/span[4]/b[1]"
}
},
{
"id": "bthub",
"name": "BThub",
"proxy": false,
"url": "https://bthub.monster",
"paths": {
"preset": "/cn/main-search-kw-{k}-{p}.html",
"size": "/cn/main-search-kw-{k}-length-{p}.html",
"time": "/cn/main-search-kw-{k}-time-{p}.html",
"hot": "/cn/main-search-kw-{k}-requests-{p}.html"
},
"xpath": {
"group": "//div[@class='search-item detail-width']",
"magnet": "./div[3]/span[5]/a/@href",
"name": "./div[1]/h3/a",
"size": "./div[3]/span[3]/b",
"date": "./div[3]/span[2]/b",
"hot": "./div[3]/span[4]/b"
}
},
{
"id": "btdad",
"name": " Btdad",
"url": "http://www.btdad.co",
"paths": {
"preset": "/search-{k}-0-0-{p}.html",
"size": "/search-{k}-0-1-{p}.html",
"time": "/search-{k}-0-2-{p}.html"
},
"xpath": {
"group": "//div[@class='ssbox']",
"magnet": "./div[2]/span[1]/a[1]/@href",
"name": "./div[1]/h3[1]/a[1]",
"size": "./div[2]/span[3]/b[1]",
"date": "./div[2]/span[4]/b[1]",
"hot": "./div[2]/span[5]/b[1]"
}
},
{
"id": "alibt",
"name": " 阿里BT",
"proxy": true,
"url": "https://alibt.pw",
"paths": {
"preset": "/zh-cn/s/{p}/{k}"
},
"xpath": {
"group": "//*[@id=\"wrapper\"]/ul/li",
"magnet": "./p[1]/a/@href",
"name": "./p[1]/a",
"size": "./p[2]/span[2]",
"date": "./p[2]/span[1]"
}
},
{
"id": "mag",
"name": "MAG磁力站",
"url": "http://mag234.com",
"paths": {
"preset": "/index/index/k/{k}/p/{p}"
},
"xpath": {
"group": "//ul[@class='link-list']",
"magnet": "./li/@data-magnet",
"name": "./li/span[1]",
"size": "./li/span[2]/span[1]",
"date": "./li/span[2]/span[2]"
}
},
{
"id": "clzz",
"name": "磁力蜘蛛",
"proxy": false,
"url": "http://www.eclzz.net",
"paths": {
"preset": "/s/{k}_rel_{p}.html",
"time": "/s/{k}_time_{p}.html",
"size": "/s/{k}_size_{p}.html"
},
"xpath": {
"group": "//div[@class='search-item']",
"magnet": "./div[1]/h3[1]/a[1]/@href",
"name": "./div[1]/h3[1]/a[1]",
"size": "./div[3]/span[3]/b[1]",
"date": "./div[3]/span[2]/b[1]",
"hot": "./div[3]/span[4]/b[1]"
}
},
{
"id": "ciligou",
"name": " 磁力狗",
"url": "https://ciligou.app",
"paths": {
"time": "/search?word={k}&sort=time&p={p}",
"preset": "/search?word={k}&sort=rele&p={p}",
"size": "/search?word={k}&sort=length&p={p}"
},
"xpath": {
"group": "//*[@id=\"Search_list_wrapper\"]/li",
"name": "./div[1]/div[1]/a[1]",
"size": "./div[2]/text()[1]",
"date": "./div[2]/text()[2]",
"hot": "./div[2]/span[1]",
"detail": {
"root": "//*[@id=\"Information_container\"]/div[2]",
"magnet": "./div[1]/div[2]/a/@href",
"files": "./div[3]/div[2]/ul/li/div"
}
}
},
{
"id": "ciliba",
"name": " 磁力吧",
"url": "https://www.ciliba.icu",
"paths": {
"time": "/s/{k}/1/{p}.html",
"hot": "/s/{k}/3/{p}.html",
"size": "/s/{k}/2/{p}.html"
},
"xpath": {
"group": "//div[@class='search-item']",
"name": "./div[1]/h3[1]/a[1]",
"size": "./div[3]/span[3]/b[1]",
"date": "./div[3]/span[2]/b[1]",
"hot": "./div[3]/span[4]/b[1]",
"detail": {
"root": "//*[@id=\"wall\"]",
"magnet": "./div[1]/p[7]/a[1]/@href"
}
}
},
{
"id": "btfox",
"name": " btfox",
"url": "http://btfox0.net",
"paths": {
"time": "/s?wd={k}&sort=time&page={p}",
"size": "/s?wd={k}&sort=length&page={p}",
"hot": "/s?wd={k}&sort=hits&page={p}",
"preset": "/s?wd={k}&sort=rele&page={p}"
},
"xpath": {
"group": "//div[@class=\"item\"]",
"name": "./div/div[1]/div/div/div/a",
"size": "./div/div/div[2]/text()[2]",
"date": "./div/div/div[2]/text()[3]",
"detail": {
"root": "//div[@class=\"maincontent\"]",
"magnet": "./div[2]/div/div[2]/ul/li[1]/a/@href",
"files": "./div[3]/div[2]/ul/li[1]"
}
}
},
{
"id": "kickass",
"name": "KickassTorrents",
"proxy": true,
"url": "https://kickasstorrents.to",
"paths": {
"preset": "/usearch/{k}/{p}"
},
"xpath": {
"group": "//tr[@class='odd']",
"magnet": "./td[1]/div[2]/div/a/@href",
"name": "./td[1]/div[2]/div/a",
"size": "./td[2]",
"date": "./td[4]",
"detail": {
"files": "//div[@class=\"dd filelist\"]"
}
}
},
{
"id": "zooqle",
"name": "Zooqle",
"url": "https://zooqle.com",
"paths": {
"preset": "/search?pg={p}&q={k}&v=t"
},
"xpath": {
"group": "//*[@class='table table-condensed table-torrents vmiddle']/tr",
"magnet": "./td[3]/ul/li/a/@href",
"name": "./td[2]/a",
"size": ".//td[4]/div/div",
"date": "./td[5]"
}
},
{
"id": "nyaa",
"name": "Nyaa",
"proxy": false,
"url": "https://nyaa.fun",
"paths": {
"preset": "/search/c_{p}_0_k_{k}"
},
"xpath": {
"group": "//div[@class='table-responsive']/table/tbody/tr",
"magnet": "./td[3]/a[2]/@href",
"name": "./td[2]/a",
"size": "./td[4]",
"date": "./td[5]",
"detail": {
"files": "//div[@class=\"torrent-file-list panel-body\"]/ul/li"
}
}
},
{
"id": "sukebei",
"name": "Sukebei Nyaa",
"proxy": true,
"url": "https://sukebei.nyaa.fun",
"paths": {
"preset": "/search/c_{p}_0_k_{k}"
},
"xpath": {
"group": "//div[@class='table-responsive']/table/tbody/tr",
"magnet": "./td[3]/a[2]/@href",
"name": "./td[2]/a",
"size": "./td[4]",
"date": "./td[5]",
"hot": "./td[8]",
"detail": {
"files": "//div[@class=\"torrent-file-list panel-body\"]/ul/li"
}
}
},
{
"id": "sobt",
"name": " Sobt",
"url": "http://sobt0.net",
"paths": {
"preset": "/q/{k}.html?sort=rel&page={p}",
"time": "/q/{k}.html?sort=time&page={p}",
"size": "/q/{k}.html?sort=size&page={p}"
},
"xpath": {
"group": "//div[@class='search-item']",
"magnet": "./div[1]/h3[1]/a[1]/@href",
"name": "./div[1]/h3[1]/a[1]",
"size": "./div[3]/span[3]/b[1]",
"date": "./div[3]/span[2]/b[1]",
"hot": "./div[3]/span[4]/b[1]"
}
},
{
"id": "dmhy",
"name": "动漫花园",
"proxy": false,
"url": "https://dmhy.anoneko.com",
"paths": {
"time": "/topics/list/page/{p}?keyword={k}"
},
"xpath": {
"group": "//*[@id=\"topic_list\"]/tr",
"magnet": "./td[4]/a/@href",
"name": "./td[3]/a",
"size": "./td[5]",
"date": "./td[1]/text()",
"hot": "./td[8]",
"detail": {
"files": "//div[@class=\"file_list\"]/ul/li"
}
}
},
{
"id": "thepiratebay_z",
"name": "The Pirate Bay",
"proxy": false,
"url": "https://thepiratebay.unblocker.cc",
"paths": {
"preset": "/search/{k}/{p}/7/",
"time": "/search/{k}/{p}/3/",
"size": "/search/{k}/{p}/5/",
"hot": "/search/{k}/{p}/9/"
},
"xpath": {
"group": "//table[@id='searchResult']/tr",
"magnet": "./td[2]/a[1]/@href",
"name": "./td[2]/div/a",
"size": "./td[2]/font/text()",
"date": "./td[2]/font/text()",
"hot": "./td[4]"
}
},
{
"id": "extratorrent",
"name": "ExtraTorrent",
"url": "https://extratorrent.si",
"paths": {
"preset": "/search/?page={p}&search={k}&srt=added",
"size": "/search/?page={p}&search={k}&srt=size&order=desc"
},
"xpath": {
"group": "//tr[@class='tlr']",
"magnet": "./td[1]/a[2]/@href",
"name": "./td[3]/a",
"size": "./td[5]",
"date": "./td[4]",
"hot": "./td[6]"
}
},
{
"id": "1337x",
"name": "1337X(英文)",
"proxy": true,
"url": "https://1337x.to",
"paths": {
"preset": "/search/{k}/{p}/",
"time": "/sort-search/{k}/time/desc/{p}/",
"size": "/sort-search/{k}/size/desc/{p}/",
"hot": "/sort-search/{k}/leechers/desc/{p}/"
},
"xpath": {
"group": "//div[@class='table-list-wrap']/table/tbody/tr",
"name": "./td[1]/a[2]",
"size": "./td[5]",
"date": "./td[4]",
"hot": "./td[8]",
"detail": {
"root": "/html/body/main/div/div/div/div[2]",
"magnet": "./div[1]/ul[1]/li[1]/a/@href"
}
}
}
]
================================================
FILE: scripts/build-service.js
================================================
/* 编译成node服务 */
const path = require('path')
const fs = require('fs-extra')
const Terser = require('terser')
// 需要忽略的文件/文件夹
const ignore = ['.DS_Store', 'service.js', 'index.js', 'index.dev.js', 'ipc.js', 'node_modules', 'electron-cache.js', 'menu.js']
// 忽略压缩的文件
const ignoreMinify = ['defaultConfig.js', 'process-config.js']
const releases = 'build/releases'
if (!fs.existsSync(releases)) {
fs.mkdirsSync(releases)
}
// 源代码路径
const source = 'src/main'
const finder = require('findit2')(source)
// 清空发布文件夹
const releasesIgnore = ['package-lock.json', 'node_modules']
const dir = fs.readdirSync(releases)
dir.forEach((file) => {
if (!isIgnore(file, releasesIgnore)) {
fs.removeSync(`${releases}/${file}`)
}
})
fs.copySync(`${source}/service.js`, `${releases}/index.js`)
finder.on('file', function (file, stat, linkPath) {
if (isIgnore(file)) {
console.info('忽略路径', file)
return
}
const target = file.replace(source, releases)
const targetDir = path.join(target, '..')
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir)
}
if (/\.js$/.test(file) && isMinify(file)) {
const result = Terser.minify(fs.readFileSync(file, 'utf-8'))
fs.writeFileSync(target, result.code)
// console.info('压缩js', file)
} else {
fs.copySync(file, target)
// console.info('复制文件', file, target)
}
})
// 生成package.json
console.log('生成package.json')
const json = require(path.resolve('package.json'))
json.main = './index.js'
json.scripts = {
'start': 'node index.js'
}
delete json.build
delete json.appName
delete json.description
json.name = 'magnetw-service'
fs.writeFileSync(path.resolve(releases, 'package.json'), JSON.stringify(json, '\t', 2))
/**
* 是否忽略此文件
* @param file
* @returns {boolean}
*/
function isIgnore (file, ignoreArray) {
const array = ignoreArray || ignore
for (let i = 0; i < array.length; i++) {
if (new RegExp(array[i]).test(file)) {
return true
}
}
return false
}
/**
* 是否需要压缩
* @param file
* @returns {boolean}
*/
function isMinify (file) {
const minify = ignore.concat(ignoreMinify)
for (let i = 0; i < minify.length; i++) {
if (new RegExp(minify[i]).test(file)) {
return false
}
}
return true
}
================================================
FILE: scripts/merge-filter-db.js
================================================
// 合并过滤词库
// https://github.com/fighting41love/funNLP/tree/master/data/%E6%95%8F%E6%84%9F%E8%AF%8D%E5%BA%93
// https://raw.githubusercontent.com/toolgood/ToolGood.Words/8bfbcfbf7b1db26b06766146029a4615fd8cfa5c/csharp/ToolGood.Words.Contrast/BadWord.txt
// https://raw.githubusercontent.com/elulis/sensitive-words/master/src/main/resources/sensi_words.txt
// https://github.com/fanhua1994/DzFilter/blob/master/database/data_filter20180120.db
// https://raw.githubusercontent.com/FireLustre/php-dfa-sensitive/master/tests/data/words.txt
// https://raw.githubusercontent.com/spetacular/bannedwords/master/pub_banned_words.txt
// https://raw.githubusercontent.com/importcjj/sensitive/master/dict/dict.txt
// https://github.com/aojiaotage/text-censor/blob/master/keywords
const {execSync} = require('child_process')
const path = require('path')
const fs = require('fs-extra')
// 如果临时git文件夹不存在 就拉取
const zip = 'scripts/filter-data/data.zip'
const temp = 'scripts/filter-data/.temp'
fs.emptyDirSync(temp)
fs.mkdirsSync(temp)
execSync(`unzip ${zip} -d ${temp} -x __MACOSX/*> /dev/null 2>&1`, {stdio: 'inherit'})
const words = []
const files = fs.readdirSync(temp)
files.forEach((it) => {
const file = `${temp}/${it}`
const itemWords = fs.readFileSync(file, 'utf-8').split('\n')
words.push.apply(words, itemWords)
console.log('添加过滤词 %d条', itemWords.length)
})
const uniqueWords = unique(words)
console.log('去重复 %d 条', words.length - uniqueWords.length)
console.log('加载完成,共%d条', uniqueWords.length)
function unique (arr) {
const result = []
const hash = {}
for (var i = 0, elem; (elem = arr[i]) != null; i++) {
if (!hash[elem]) {
result.push(elem)
hash[elem] = true
}
}
return result
}
fs.writeFileSync('static/keywords.txt', Buffer.from(uniqueWords.join('\n')).toString('base64'))
================================================
FILE: src/index.ejs
================================================
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title><%= htmlWebpackPlugin.options.title %></title>
<meta name="robots" content="noindex,nofollow,noarchive">
<meta name="format-detection" content="telephone=no,email=no,adress=no">
<meta http-equiv="window-target" content="_top">
<% if (htmlWebpackPlugin.options.nodeModules) { %>
<!-- Add `node_modules/` to global paths so `require` works properly in development -->
<script>
require('module').globalPaths.push('<%= htmlWebpackPlugin.options.nodeModules.replace(/\\/g, '\\\\') %>')
</script>
<% } %>
</head>
<body>
<div id="app"></div>
<!-- Set `__static` path to static files in production -->
<% if (!process.browser) { %>
<script>
if (process.env.NODE_ENV !== 'development') window.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
</script>
<% } %>
<!-- webpack builds are automatically injected -->
</body>
</html>
================================================
FILE: src/main/api.js
================================================
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const prefix = '/api'
const router = new Router({prefix})
const repo = require('./repository')
let serverInfo = null
let koaServer = null
app.use(require('@koa/cors')())
app.use(require('koa-bodyparser')())
app.use(require('./middleware/block'))
app.use(require('./middleware/response-template'))
router.get('/rule', async (ctx) => {
ctx.success(await repo.getRule())
})
router.get('/load-rule', async (ctx) => {
ctx.success(await repo.loadRuleByURL())
})
router.get('/search', async (ctx) => {
if (ctx.query.keyword) {
const current = repo.makeupSearchOption(ctx.query)
const {originalCount, items} = await repo.obtainSearchResult(current, ctx.headers)
ctx.success({
current,
originalCount,
items
})
if (items && items.length > 0) {
// 异步缓存后续结果
repo.asyncCacheSearchResult(current, ctx.headers)
}
} else {
ctx.throw(400, '请输入关键词')
}
})
router.get('/detail', async (ctx) => {
const id = ctx.query.id
const path = ctx.query.path
if (id && path) {
const detail = await repo.obtainDetailResult({id, path}, ctx.headers)
ctx.success(detail)
} else {
ctx.throw(400, '请指定ID和URL')
}
})
app.use(router.routes()).use(router.allowedMethods())
function getIPAddress () {
const interfaces = require('os').networkInterfaces()
for (let devName in interfaces) {
const iface = interfaces[devName]
for (let i = 0; i < iface.length; i++) {
const alias = iface[i]
if (alias.family === 'IPv4' && alias.address !== '127.0.0.1' && !alias.internal) {
return alias.address
}
}
}
}
async function reload (config, preload) {
repo.applyConfig(config)
if (preload) {
const rule = await repo.loadRuleByURL()
const ruleLog = rule.map((it) => `[加载][${it.name}][${it.url}]`).join('\n')
const proxyCount = rule.filter(it => it.proxy).length
const log = `${ruleLog}\n${rule.length}个规则加载完成,其中${rule.length - proxyCount}个可直接使用,${proxyCount}个需要代理\n`
console.info(log)
}
}
async function start (config, preload) {
try {
const customPort = config.customServerPort ? config.customServerPortValue : undefined
const port = config.port || customPort
koaServer = await app.listen(port)
const address = koaServer.address()
serverInfo = {
port: address.port,
ip: getIPAddress(),
local: 'localhost',
url: `http://localhost:${address.port}`
}
await reload(config, preload)
return serverInfo
} catch (e) {
return {message: e.message}
}
}
function stop (callback) {
if (koaServer) {
koaServer.close(() => {
serverInfo = null
koaServer = null
callback()
})
}
}
function isStarting () {
return serverInfo !== null && serverInfo !== undefined
}
function getServerInfo () {
return serverInfo
}
module.exports = {
reload, start, stop, getServerInfo, isStarting, prefix, getProxyNetworkInfo: repo.getProxyNetworkInfo
}
================================================
FILE: src/main/axios.js
================================================
const axios = require('axios')
const tunnel = require('tunnel')
const SocksProxyAgent = require('socks-proxy-agent')
/**
* @param appConfig 必填
* @returns {*}
*/
function create (appConfig) {
const defaultTimeout = 5000
const http = axios.create({
timeout: defaultTimeout
})
http.interceptors.request.use(config => {
let proxyURL
if (appConfig && appConfig.hasOwnProperty('proxy') && appConfig.hasOwnProperty('proxyType') &&
appConfig.hasOwnProperty('proxyHost') && appConfig.hasOwnProperty('proxyPort')) {
const {proxy, proxyType, proxyHost, proxyPort} = appConfig
if (proxy) {
const timeout = config.timeout
proxyURL = `${proxyType}://${proxyHost}:${proxyPort}`
const proxyAgent = proxyType.startsWith('socks') ? new SocksProxyAgent({
protocol: `${proxyType}:`,
hostname: proxyHost,
port: proxyPort,
timeout: timeout
}) : tunnel.httpsOverHttp({
timeout: timeout,
proxy: {
host: proxyHost,
port: proxyPort
}
})
config.httpAgent = proxyAgent
config.httpsAgent = proxyAgent
}
}
const headers = config.headers
const customHeaders = {}
for (let key in headers) {
if (!/common|delete|get|head|post|put|patch/.test(key)) {
customHeaders[key] = headers[key]
}
}
console.info({
url: config.url,
headers: customHeaders,
proxy: proxyURL
})
return config
})
http.interceptors.response.use(rsp => rsp.data, error => Promise.reject(error))
return http
}
module.exports = create
================================================
FILE: src/main/cache.js
================================================
const moment = require('moment')
const store = process.env.BUILD_TARGET === 'electron' ? require('./electron-cache') : require('./memory-cache')
/**
* 添加缓存
* @param key
* @param value
* @param expired 过期时间(秒) 0或null则永久
*/
function set (key, value, expired) {
const expiredDate = expired > 0 ? new Date(Date.now() + (expired * 1000)) : new Date(2100, 0, 1)
store.put(key, {
created: new Date(),
expired: expiredDate,
data: value
})
console.info(`新增缓存: ${key}, 过期时间: ${moment(expiredDate).format('YYYY-MM-DD HH:mm:ss.SSS')}`)
}
/**
* 获取缓存
* @param key
* @returns
*/
function get (key) {
let value = store.get(key)
if (value) {
// 没过期
if (moment().isBefore(value.expired)) {
return value.data
} else {
store.delete(key)
console.info(`删除过期缓存: ${key}`)
}
}
return null
}
function clear () {
store.clear()
}
module.exports = {
set, get, clear
}
================================================
FILE: src/main/defaultConfig.js
================================================
module.exports = function () {
return {
checkUpdateURL: 'https://magnetw.app/update.json',
// 云解析URL
cloud: false,
cloudUrl: '',
// 解析规则文件URL 支持网络链接和本地路径
ruleUrl: 'https://magnetw.app/rule.json',
// 默认最大化窗口
maxWindow: false,
// 是否显示需要代理的源站
showProxyRule: false,
// 是否显示源站入口
showSourceLink: false,
// 过滤
filterBare: false,
filterEmpty: false,
// 自定义服务映射端口
customServerPort: false,
customServerPortValue: null,
// 使用代理
proxy: false,
// http|socks5
proxyType: 'socks5',
proxyHost: '127.0.0.1',
proxyPort: 1080,
// 是否启用预加载 启用后会预加载下一页和下一个源站
preload: true,
// 缓存过期时间
cacheExpired: 7200,
// 追加请求标识
requestIdentifier: false,
// 自定义UserAgent
customUserAgent: false,
customUserAgentValue: null
}
}
================================================
FILE: src/main/electron-cache.js
================================================
const Store = require('electron-store')
const store = new Store()
function put (key, value) {
store.set(key, value)
}
function get (key) {
return store.get(key)
}
function deleteValue (key) {
store.delete(key)
}
function clear () {
store.clear()
}
module.exports = {
put, get, 'delete': deleteValue, clear
}
================================================
FILE: src/main/filter/filter.js
================================================
const path = require('path')
const fs = require('fs')
const map = {}
let load = false
const isDev = process.env.NODE_ENV === 'development'
async function loadFilterData () {
if (load) {
return
}
load = true
const file = isDev ? path.resolve('static/keywords.txt') : path.resolve(__dirname, './static/keywords.txt')
const original = Buffer.from(fs.readFileSync(file, 'utf-8'), 'base64')
const words = original.toString().split('\n')
words.forEach((line) => {
if (line) {
addWord(line)
}
})
console.info('加载词条%d条', words.length)
}
function addWord (word) {
let parent = map
for (let i = 0; i < word.length; i++) {
if (!parent[word[i]]) parent[word[i]] = {}
parent = parent[word[i]]
}
parent.isEnd = true
}
function isFilter (s, cb) {
let parent = map
for (let i = 0; i < s.length; i++) {
if (s[i] === '*') {
continue
}
let found = false
let skip = 0
let sWord = ''
for (let j = i; j < s.length; j++) {
if (!parent[s[j]]) {
found = false
skip = j - i
parent = map
break
}
sWord = sWord + s[j]
if (parent[s[j]].isEnd) {
found = true
skip = j - i
break
}
parent = parent[s[j]]
}
if (skip > 1) {
i += skip - 1
}
if (!found) {
continue
}
let stars = '*'
for (let k = 0; k < skip; k++) {
stars = stars + '*'
}
let reg = new RegExp(sWord, 'g')
if (reg.test(s)) {
return true
}
// s = s.replace(reg, stars)
}
if (typeof cb === 'function') {
cb(null, s)
}
return false
}
module.exports = {
loadFilterData, isFilter
}
================================================
FILE: src/main/format-parser.js
================================================
const moment = require('moment')
const sizeUnit = ['B|bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
const sizeUnitSpare = ['B|bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const sizeUnitRegx = sizeUnit.join('|')
const sizeUnitSpareRegx = sizeUnitSpare.join('|')
function extractNumber (str) {
const match = /\d+/.exec(str)
return match ? parseInt(match[0]) : str
}
function extractFloat (str) {
const match = /(\d+(\.\d+)?)/.exec(str)
return match ? parseFloat(match[0]) : str
}
function extractSizeText (str) {
const match = new RegExp(`(\\d+(\\.\\d+)?)( | )*(${sizeUnitRegx}|${sizeUnitSpareRegx})`, 'gi').exec(str)
return match ? match[0] : str
}
module.exports = {
extractNumber,
extractFloat,
/**
* 从节点里提取文本
* @param node
*/
extractTextByNode (node) {
if (node) {
if (Array.isArray(node)) {
if (node.length > 0) {
return node[0].textContent.trim()
}
} else {
return node.textContent.trim()
}
}
return null
},
/**
* 提取分辨率
* @param name
*/
extractResolution (name) {
const regx = {
'4K': '2160|4k',
'2K': '1440|2k',
'1080P': '1920|1080',
'720P': '1280|720'
}
for (let key in regx) {
if (new RegExp(regx[key], 'i').test(name)) {
return key
}
}
},
/**
* 提取磁力链
* @param url
*/
extractMagnet (url) {
if (url) {
// 如果是磁力链 直接返回
if (/^(magnet:\?xt=urn:btih:)/.test(url)) {
return url.toLowerCase()
} else {
// 如果不是磁力链 就提取 连续字母数字32-40位
let match = /[\da-zA-Z]{32,40}/.exec(url)
if (match) {
return `magnet:?xt=urn:btih:${match[0]}`.toLowerCase()
}
}
}
return null
},
/**
* 提取时间
* @param dateText
*/
extractDate: function (dateText) {
if (dateText) {
const parser = [
{
// 2019-12-22
regx: /(\d{4})-(\d{1,2})-(\d{1,2})/,
format: ['YYYY-MM-DD']
},
{
// 2019-12-22 21:26
regx: /(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2})/,
format: ['YYYY-MM-DD HH:mm']
},
{
// 2019-12-22 21:26:51
regx: /(\d{4})-(\d{1,2})-(\d{1,2}) (\d{1,2}):(\d{1,2}):(\d{1,2})/,
format: ['YYYY-MM-DD HH:mm:ss']
},
{
// 09-09 2018
regx: /(\d{1,2})-(\d{1,2})( | )(\d{4})/,
format: 'MM-DD YYYY'
},
{
// 12-17 08:08
regx: /(\d{1,2})-(\d{1,2})( | )(\d{2}):(\d{2})/,
format: 'MM-DD HH:mm'
}
]
for (let i = 0; i < parser.length; i++) {
const regx = parser[i].regx
if (regx.test(dateText)) {
const exec = regx.exec(dateText)
const text = exec ? exec[0] : dateText
return moment(text, parser[i].format).valueOf()
}
}
// 如果是时间间隔
if (/yesterday|昨天/.test(dateText)) {
return moment().subtract(1, 'day').valueOf()
} else {
const unit = [
{regx: 'yesterday|昨天', name: 'days'},
{regx: 'year|年', name: 'years'}, {regx: 'month|月', name: 'months'},
{regx: 'day|天', name: 'days'}, {regx: 'hour|小时', name: 'hour'},
{regx: 'minute|分钟', name: 'minutes'}, {regx: 'second|秒', name: 'seconds'}
]
for (let i = 0; i < unit.length; i++) {
const dateTextMatches = new RegExp(`\\d+( {0,3})${unit[i].regx}`, 'gi').exec(dateText)
if (dateTextMatches) {
const number = extractNumber(dateTextMatches[0])
return moment().subtract(number, unit[i].name).valueOf()
}
}
}
return 0
}
},
/**
* 提取文件大小(字节)
* @param sizeText
*/
extractFileSize (sizeText) {
let extSizeText = extractSizeText(sizeText)
if (extSizeText) {
let index = -1
for (let i = sizeUnit.length - 1; i >= 0; i--) {
if (new RegExp(`${sizeUnit[i]}|${sizeUnitSpare[i]}`, 'i').test(sizeText)) {
index = i
break
}
}
if (index >= 0) {
return parseInt(extractFloat(extSizeText) * Math.pow(1024, index))
}
return extSizeText
} else {
return sizeText
}
},
splitByFileSize (str) {
const regx = new RegExp(`(\\d+(\\.\\d+)?) {1,3}(${sizeUnitRegx})$`, 'gi')
const match = regx.exec(str)
const filesize = match ? match[0] : str
const filename = str.replace(regx, '')
const array = [filename, filesize]
return array.filter(it => it.trim())
}
}
================================================
FILE: src/main/index.dev.js
================================================
/**
* This file is used specifically and only for development. It installs
* `electron-debug` & `vue-devtools`. There shouldn't be any need to
* modify this file, but it can be used to extend your development
* environment.
*/
/* eslint-disable */
// Install `electron-debug` with `devtron`
require('electron-debug')({ showDevTools: true })
// Install `vue-devtools`
require('electron').app.on('ready', () => {
let installExtension = require('electron-devtools-installer')
installExtension.default(installExtension.VUEJS_DEVTOOLS)
.then(() => {})
.catch(err => {
console.log('Unable to install `vue-devtools`: \n', err)
})
})
// Require `main` process to boot app
require('./index')
================================================
FILE: src/main/index.js
================================================
'use strict'
import {app, BrowserWindow, session} from 'electron'
const registerMenu = require('./menu')
const {appName, build} = require('../../package.json')
const is = require('electron-is')
const Store = require('electron-store')
const store = new Store()
/**
* Set `__static` path to static files in production
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
*/
if (process.env.NODE_ENV !== 'development') {
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}
// 关闭安全警告
process.env.ELECTRON_DISABLE_SECURITY_WARNINGS = true
let quitApp = false
let mainWindow
const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080`
: `file://${__dirname}/index.html`
function createWindow () {
/**
* Initial window options
*/
const width = is.dev() ? 1200 : 1000
mainWindow = new BrowserWindow({
height: 680,
useContentSize: true,
width: width,
minWidth: 900,
minHeight: 550,
frame: true,
titleBarStyle: 'hidden',
backgroundColor: '#fff',
show: false,
webPreferences: {
nodeIntegration: true
}
})
// 是否设置最大化
const configVariable = store.get('config_variable')
if (configVariable && configVariable.maxWindow) {
mainWindow.maximize()
}
mainWindow.show()
const userAgent = mainWindow.webContents.userAgent.replace(new RegExp(`${app.name}\\/.* `, 'gi'), '')
mainWindow.webContents.userAgent = userAgent
session.defaultSession.setUserAgent(userAgent)
registerMenu(mainWindow)
mainWindow.loadURL(winURL)
mainWindow.on('close', (e) => {
// 不是mac 或者 已标志退出
if (process.platform !== 'darwin' || quitApp) {
mainWindow = null
} else {
e.preventDefault()
mainWindow.hide()
}
})
mainWindow.on('closed', (e) => {
mainWindow = null
})
registerServer()
}
async function registerServer () {
const {registerIPC, registerServer} = require('./ipc')
registerIPC(mainWindow)
registerServer()
}
app.on('ready', createWindow)
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit()
}
})
app.on('activate', () => {
if (mainWindow === null) {
createWindow()
} else {
if (!mainWindow.isVisible()) {
mainWindow.show()
}
}
})
app.on('before-quit', () => {
quitApp = true
})
/**
* Auto Updater
*
* Uncomment the following code below and install `electron-updater` to
* support auto updating. Code Signing with a valid certificate is required.
* https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-electron-builder.html#auto-updating
*/
/*
import { autoUpdater } from 'electron-updater'
autoUpdater.on('update-downloaded', () => {
autoUpdater.quitAndInstall()
})
app.on('ready', () => {
if (process.env.NODE_ENV === 'production') autoUpdater.checkForUpdates()
})
*/
================================================
FILE: src/main/ipc.js
================================================
const moment = require('moment')
const logger = require('./logger')
const path = require('path')
const {ipcMain, app} = require('electron')
const createAxios = require('./axios')
const {reload, start, isStarting, getServerInfo} = require('./api')
const {defaultConfig, extractConfigVariable, getConfig} = require('./process-config')
const Store = require('electron-store')
const store = new Store()
async function registerServer () {
if (isStarting()) {
console.info('服务已启动')
} else {
const configVariable = store.get('config_variable')
const newConfig = getConfig(configVariable)
configVariable ? console.info('使用自定义配置加载服务', configVariable, newConfig) : console.info('使用默认配置加载服务', configVariable, newConfig)
const {port, ip, local, message} = await start(newConfig, false)
if (message) {
console.error(message)
} else {
console.info(`启动成功,本地访问 http://${local}:${port},IP访问 http://${ip}:${port}`)
}
}
}
function getLocalConfig () {
return getConfig(store.get('config_variable'))
}
function registerIPC (mainWindow) {
ipcMain.on('get-server-info', function (event) {
const configVariable = store.get('config_variable')
const newConfig = getConfig(configVariable)
event.sender.send('on-get-server-info', getServerInfo(), newConfig)
})
ipcMain.on('window-max', function () {
if (mainWindow.isMaximized()) {
mainWindow.unmaximize()
} else {
mainWindow.maximize()
}
})
ipcMain.on('api-base-url', function (event) {
let serverInfo = getServerInfo()
event.returnValue = serverInfo && serverInfo.url ? serverInfo.url : ''
})
/**
* 保存配置
*/
ipcMain.on('save-server-config', async (event, config) => {
let oldConfig = getLocalConfig()
const configVariable = extractConfigVariable(config)
const newConfig = getConfig(configVariable)
let err
try {
// 如果修改了规则url 就重新加载
await reload(newConfig, newConfig.ruleUrl !== oldConfig.ruleUrl)
} catch (e) {
err = e.message
}
if (configVariable) {
store.set('config_variable', configVariable)
console.info('保存配置', configVariable, newConfig)
} else {
store.delete('config_variable')
}
event.sender.send('on-save-server-config', newConfig, oldConfig, err)
})
/**
* 获取配置信息
*/
ipcMain.on('get-server-config', (event) => {
event.returnValue = getLocalConfig()
})
/**
* 获取默认配置信息
*/
ipcMain.on('get-default-server-config', (event) => {
event.returnValue = defaultConfig()
})
/**
* 检查更新
*/
ipcMain.on('check-update', async (event) => {
try {
const request = createAxios(getLocalConfig())
const response = await request({
url: defaultConfig().checkUpdateURL,
responseType: 'json'
})
let newVerArray = response.version.split('.')
let currentVerArray = app.getVersion().split('.')
for (let i = 0; i < newVerArray.length; i++) {
if (parseInt(newVerArray[i]) > parseInt(currentVerArray[i])) {
event.sender.send('new-version', response)
return
}
}
console.info(`is the latest, cur: ${app.getVersion()}, latest: ${response.version}`)
} catch (e) {
console.error(e.message)
}
})
/**
* 获取信息
*/
ipcMain.on('get-app-info', (event) => {
event.returnValue = {
logDir: path.resolve(logger.transports.file.file, '..'),
server: getServerInfo()
}
})
/**
* 获取网络信息
*/
ipcMain.on('get-network-info', async (event, config) => {
const ip = {
url: 'http://gip.dog',
headers: {'User-Agent': 'curl'}
}
const options = {
url: 'https://www.google.com'
}
let googleTest = false
let ipBody
const start = Date.now()
try {
const request = createAxios(config)
const googleBody = await request(options)
googleTest = googleBody.length > 0
ipBody = await request(ip)
} catch (e) {
console.error(e.message)
}
const time = Date.now() - start
const test = {info: ipBody, test: googleTest, time}
console.info(test)
event.sender.send('on-get-network-info', test)
})
}
module.exports = {registerIPC, registerServer}
================================================
FILE: src/main/logger.js
================================================
const logger = require('electron-log')
const moment = require('moment')
const util = require('util')
const path = require('path')
const level = process.env.NODE_ENV === 'development' ? 'debug' : 'silly'
logger.transports.console.format = '[{y}-{m}-{d} {h}:{i}:{s}.{ms}] [{level}] {text}'
logger.transports.console.level = level
logger.transports.file.fileName = `${moment().format('YYYY-MM-DD_HH_mm')}.log`
logger.transports.file.level = level
const styles = {
// styles
bold: [1, 22],
italic: [3, 23],
underline: [4, 24],
inverse: [7, 27],
// grayscale
white: [37, 39],
grey: [90, 39],
black: [90, 39],
// colors
blue: [34, 39],
cyan: [36, 39],
green: [32, 39],
magenta: [35, 39],
red: [91, 39],
yellow: [33, 39]
}
const levels = {
error: {id: 1, color: 'red'},
warn: {id: 2, color: 'yellow'},
info: {id: 3, color: 'green'},
verbose: {id: 4, color: 'blue'},
debug: {id: 5, color: 'cyan'},
silly: {id: 6, color: 'magenta'}
}
function colorizeStart (style) {
return style ? `\x1B[${styles[style][0]}m` : ''
}
function colorizeEnd (style) {
return style ? `\x1B[${styles[style][1]}m` : ''
}
/**
* Taken from masylum's fork (https://github.com/masylum/log4js-node)
*/
function colorize (str, style) {
return colorizeStart(style) + str + colorizeEnd(style)
}
logger.transports.console = (msg) => {
const messageLevel = levels[msg.level]
// 如果是可以打印的等级
if (messageLevel.id <= levels[level].id) {
const time = moment(msg.date).format('YYYY-MM-DD HH:mm:ss.SSS')
const color = messageLevel.color
const stack = getStackInfo()
const header = colorize(util.format('[%s][%s][%s]', time, msg.level.toUpperCase(), `${stack.file}${stack.line}`), color)
const text = colorize(util.format(...msg.data), color)
const message = util.format('%s %s', header, text)
console.log(message)
}
}
function getStackInfo () {
try {
const stackReg = /at\s+(.*)\s+\((.*):(\d*):(\d*)\)/i
const stackReg2 = /at\s+()(.*):(\d*):(\d*)/i
const stackList = (new Error()).stack.split('\n').slice(3)
const sp = stackReg.exec(stackList[1]) || stackReg2.exec(stackList[1])
const data = {}
if (sp && sp.length === 5) {
data.method = sp[1]
data.path = sp[2]
data.line = sp[3]
data.pos = sp[4]
data.file = path.basename(data.path)
}
return data
} catch (e) {
return {
method: '',
path: '',
line: '',
pos: '',
file: ''
}
}
}
console.error = logger.error
console.warn = logger.warn
console.info = logger.info
console.verbose = logger.verbose
console.debug = logger.debug
console.silly = logger.silly
logger.debug('注册日志工具', logger.transports.file.fileName)
module.exports = logger
================================================
FILE: src/main/memory-cache.js
================================================
const store = require('memory-cache')
function put (key, value) {
store.put(key, value)
}
function get (key) {
return store.get(key)
}
function deleteValue (key) {
store.del(key)
}
function clear () {
store.clear()
}
module.exports = {
put, get, 'delete': deleteValue, clear
}
================================================
FILE: src/main/menu.js
================================================
const {Menu, app, session} = require('electron')
const is = require('electron-is')
module.exports = function (mainWindow) {
const appSubmenu = [
...(is.dev() ? [
{
label: '开发人员工具',
role: 'toggledevtools'
},
{type: 'separator'}
] : []),
/* {label: `关于 ${app.name}`, role: 'about'}, */
{label: '清除缓存', click: () => session.defaultSession.clearCache(() => console.info('清除完成'))},
{type: 'separator'},
...(is.macOS() ? [
{label: `隐藏 ${app.name}`, role: 'hide'},
{label: '隐藏其他应用', role: 'hideothers'},
{label: '显示全部', role: 'unhide'},
{type: 'separator'}
] : []),
{label: `退出 ${app.name}`, role: 'quit'}
]
const windowMenu = [
{label: '重新加载', role: 'reload'},
{label: '最小化', role: 'minimize'},
{label: '最大化', click: () => mainWindow.isMaximized() ? mainWindow.unmaximize() : mainWindow.maximize()},
{type: 'separator'},
{label: '关闭窗口', role: 'close'},
...(is.macOS() ? [
{type: 'separator'},
{label: '前置全部窗口', role: 'front'}
] : [])
]
const editMenu = [
{label: '撤销', role: 'undo'},
{label: '重做', role: 'redo'},
{type: 'separator'},
{label: '剪切', role: 'cut'},
{label: '复制', role: 'copy'},
{label: '粘贴', role: 'paste'},
...(is.macOS() ? [
{label: '粘贴并匹配样式', role: 'pasteAndMatchStyle'},
{label: '删除', role: 'delete'},
{label: '全选', role: 'selectAll'},
{type: 'separator'},
{
label: '语音',
submenu: [
{label: '开始讲话', role: 'startspeaking'},
{label: '停止讲话', role: 'stopspeaking'}
]
}
] : [
{label: '删除', role: 'delete'},
{type: 'separator'},
{label: '全选', role: 'selectAll'}
])
]
const menu = Menu.buildFromTemplate([
{
label: app.name,
submenu: appSubmenu
},
{
label: '编辑',
submenu: editMenu
},
{
label: '窗口',
submenu: windowMenu
}
])
Menu.setApplicationMenu(menu)
}
================================================
FILE: src/main/middleware/block.js
================================================
const blacklistRegx = ['googlebot', 'mediapartners-google', 'adsbot-google', 'baiduspider', '360spider', 'haosouspider', 'sosospider', 'sogou spider', 'sogou news spider', 'sogou web spider', 'sogou inst spider', 'sogou spider2', 'sogou blog', 'sogou orion spider', 'yodaobot', 'youdaobot', '360spider', 'bingbot', 'slurp', 'teoma', 'ia_archiver', 'twiceler', 'msnbot', 'scrubby', 'robozilla', 'gigabot', 'yahoo-mmcrawler', 'yahoo-blogs', 'yahoo! slurp china', 'yahoo!-adcrawler', 'psbot', 'yisouspider', 'easouspider', 'jikespider', 'etaospider', 'glutenfreepleasure'].join('|')
module.exports = async (ctx, next) => {
const headers = ctx.headers
const userAgent = headers['user-agent']
if (new RegExp(blacklistRegx, 'gi').test(userAgent)) {
console.info('拦截爬虫', userAgent)
ctx.throw(404)
return
}
await next()
}
================================================
FILE: src/main/middleware/response-template.js
================================================
module.exports = async (ctx, next) => {
try {
ctx.success = function (data) {
ctx.body = {
success: true,
data: data
}
}
await next()
if (ctx.status !== 200) {
ctx.throw(ctx.status, ctx.message)
}
} catch (e) {
let message = e.message
const statusCode = e.statusCode || 500
console.error(statusCode, e.message)
const maxLength = 100
if (statusCode !== 500) {
message = `${statusCode} - 请求源站异常,请查看日志`
}
ctx.status = statusCode
ctx.body = {
success: false,
message: message.length > maxLength ? `${message.substring(0, maxLength)}...` : message
}
}
}
================================================
FILE: src/main/process-config.js
================================================
const defaultConfig = require('./defaultConfig')
/**
* 从修改后的配置对象中提取修改的变量
* @param newConfig
* @returns {null}
*/
function extractConfigVariable (newConfig) {
const tempSettingVariable = {}
let defaultSetting = defaultConfig()
for (let key in newConfig) {
// 如果不是默认配置 就保存
if (newConfig.hasOwnProperty(key)) {
const value = newConfig[key]
if (value != null && value.toString().length > 0 && value !== defaultSetting[key]) {
tempSettingVariable[key] = value
}
}
}
// 如果修改了配置 就重新合并配置数据
const isModified = Object.keys(tempSettingVariable).length > 0
if (isModified) {
let tempSetting = defaultConfig()
Object.assign(tempSetting, tempSettingVariable)
return tempSettingVariable
}
return null
}
/**
* 合并一个新的配置对象
* @param configVariable 可为null
* @returns {{trackers, checkUpdateURL, memoryLastRule, showProxyRule, customUserAgent, proxyHost, preload, proxy, proxyPort, customUserAgentValue, cacheExpired, trackersUrl, ruleUrl}}
*/
function getConfig (configVariable) {
let localSetting = defaultConfig()
// 合并配置
Object.assign(localSetting, configVariable)
return localSetting
}
module.exports = {
defaultConfig,
extractConfigVariable,
getConfig
}
================================================
FILE: src/main/repository.js
================================================
const format = require('./format-parser')
const URI = require('urijs')
const fs = require('fs')
const createAxios = require('./axios')
const cacheManager = require('./cache')
const {loadFilterData, isFilter} = require('./filter/filter')
const xpath = require('xpath')
const DOMParser = require('xmldom').DOMParser
const htmlparser2 = require('htmlparser2')
const domParser = new DOMParser({
errorHandler: {
warning: w => {
// console.warn(w)
},
error: e => {
// console.error(e)
},
fatalError: e => {
// console.error(e)
}
}
})
let ruleMap = {}
let config = null
let request = null
function applyConfig (newConfig) {
config = newConfig
request = createAxios(newConfig)
if (config.filterBare) {
loadFilterData()
}
}
function clearCache () {
cacheManager.clear()
}
/**
* 补齐搜索参数
* @param rule
* @param keyword
* @param page
* @param sort
* @returns {{id: *, page: *, sort: *, keyword: *, url: *}}
*/
function makeupSearchOption ({id, keyword, page, sort}) {
const rule = getRuleById(id)
const newPage = Math.max(1, page || null)
// 如果没有指定的排序 就取第一个排序
const pathKeys = Object.keys(rule.paths)
const newSort = pathKeys.indexOf(sort) !== -1 ? sort : pathKeys[0]
// 拼接完整url
const url = rule.url + rule.paths[newSort].replace(/{k}/g, encodeURIComponent(keyword)).replace(/{p}/g, newPage)
return {id: rule.id, keyword, page: newPage, sort: newSort, url}
}
function getRuleById (id) {
return ruleMap[id] || ruleMap[Object.keys(ruleMap)[0]]
}
async function requestDocument (url, clientHeaders) {
const timeout = config.timeout || 10000
// header
const uri = new URI(url)
const host = uri.host()
const origin = uri.origin()
const headers = {
'host': host,
'origin': origin,
'referer': origin
}
const acceptLanguage = clientHeaders['accept-language']
headers['accept-language'] = acceptLanguage || 'zh-CN,zh-TW;q=0.9,zh;q=0.8,en;q=0.7,und;q=0.6,ja;q=0.5'
const xForwardedFor = clientHeaders['x-forwarded-for']
if (xForwardedFor) {
headers['x-forwarded-for'] = xForwardedFor
}
const userAgent = clientHeaders['user-agent']
if (userAgent) {
const newUserAgent = config.requestIdentifier && / windows | mac | android | ios /gi.test(userAgent) && process.env.npm_package_version ? `${userAgent} MWBrowser/${process.env.npm_package_version}` : userAgent
headers['user-agent'] = config.customUserAgent && config.customUserAgentValue ? config.customUserAgentValue : newUserAgent
}
const options = {url: url, headers: headers, timeout: timeout}
const html = await request(options)
// 用htmlparser2转换一次再解析
const outerHTML = htmlparser2.DomUtils.getOuterHTML(htmlparser2.parseDOM(html))
return domParser.parseFromString(outerHTML)
}
async function obtainDetailResult ({id, path}, headers) {
const rule = getRuleById(id)
if (!rule || !rule.xpath.detail) {
throw new Error('此源站没有配置详情规则')
}
const url = rule.url + path
// 如果有缓存
let detail = cacheManager.get(url)
if (!detail) {
// 去源站请求详情
let document = await requestDocument(url, headers)
detail = parseDetailDocument(document, rule.xpath.detail)
if (detail) {
detail['url'] = url
// 缓存请求到的详情
cacheManager.set(url, detail)
}
}
return detail
}
async function obtainSearchResult ({id, url}, headers) {
const rule = getRuleById(id)
// 如果没有缓存
let items = cacheManager.get(url)
if (!items || items.length <= 0) {
// 去源站请求
let document = await requestDocument(url, headers)
items = parseItemsDocument(document, rule.xpath)
if (items && items.length > 0) {
// 缓存请求到的列表
cacheManager.set(url, items, config.cacheExpired)
}
}
// 过滤
const originalCount = items.length
if (config.filterBare || config.filterEmpty) {
items = items.filter((item) => {
if (config.filterBare) {
return !isFilter(item.name.replace(/ /g, ''))
} else if (config.filterEmpty) {
return typeof item.size === 'number' && item.size > 0
}
})
}
return {originalCount, items}
}
/**
* 缓存下一页
* @param id
* @param keyword
* @param page
* @param sort
* @param headers
* @param userAgent
*/
function asyncCacheSearchResult ({id, keyword, page, sort}, headers) {
if (!config.preload) {
return
}
// 缓存下一页
const next = makeupSearchOption({id, keyword, page: page + 1, sort})
obtainSearchResult({id, url: next.url}, headers)
/*
if (page === 1) {
// 是第一页才缓存下一个源站
let ruleKeys = Object.keys(ruleMap)
const rule = ruleMap[ruleKeys[ruleKeys.indexOf(id) + 1]]
if (rule) {
const next = makeupSearchOption({id: rule.id, keyword, page, sort})
obtainSearchResult({id: next.id, url: next.url}, headers)
}
}
*/
}
/**
* 解析列表Document
* @param document
* @param expression xpath表达式对象
*/
function parseItemsDocument (document, expression) {
const items = []
const groupNodes = xpath.select(expression.group, document)
groupNodes.forEach((child, index) => {
// 名称
const nameNode = xpath.select(expression.name, child)
const name = format.extractTextByNode(nameNode)
// 分辨率
const resolution = format.extractResolution(name)
// 磁力链
const magnet = format.extractMagnet(format.extractTextByNode(xpath.select(expression.magnet, child)))
// 时间
const date = format.extractDate(format.extractTextByNode(xpath.select(expression.date, child)))
// 文件大小
const size = format.extractFileSize(format.extractTextByNode(xpath.select(expression.size, child)))
// 人气
const hot = expression.hot ? format.extractNumber(format.extractTextByNode(xpath.select(expression.hot, child))) : null
// 详情url
const detailExps = expression.name + '/@href'
const detailUrlText = format.extractTextByNode(xpath.select(detailExps, child))
const detailUrl = detailUrlText ? new URI(detailUrlText).hostname('').toString() : null
if (name) {
items.push({
name, magnet, resolution, date, size, hot, detailUrl
})
}
})
// console.silly(`\n${JSON.stringify(items, '\t', 2)}`)
return items
}
/**
* 解析详情
* @param document
* @param expression
*/
function parseDetailDocument (document, expression) {
const rootNode = xpath.select1(expression.root, document)
let magnet
if (expression.magnet) {
magnet = format.extractMagnet(format.extractTextByNode(xpath.select1(expression.magnet, rootNode)))
if (!magnet) {
return null
}
}
const fileNodes = expression.files ? xpath.select(expression.files, rootNode) : null
let files = null
if (fileNodes) {
files = []
fileNodes.forEach((child, index) => {
const fileArray = format.splitByFileSize(format.extractTextByNode(child))
files.push({
name: fileArray[0],
size: format.extractFileSize(fileArray[1])
})
})
}
return {magnet, files}
}
/**
* 从网络或者本地更新并缓存规则
* @returns {Promise<void>}
*/
async function loadRuleByURL () {
const url = config.ruleUrl
let rule
try {
if (url.startsWith('http')) {
// 如果是网络文件
console.info('获取网络规则文件', url)
rule = await request(url, {timeout: 8000, json: true})
} else {
console.info('读取本地规则文件', url)
rule = JSON.parse(fs.readFileSync(url))
}
if (!Array.isArray(rule) || rule.length <= 0) {
throw new Error('规则格式不正确')
}
} catch (e) {
console.error(e.message, '规则加载失败,将使用内置规则')
rule = require('../../rule.json')
}
cacheManager.set('rule_json', JSON.stringify(rule))
rule.forEach((it) => {
ruleMap[it.id] = it
})
return rule
}
async function getRule () {
const ruleJson = cacheManager.get('rule_json')
const rule = ruleJson ? JSON.parse(ruleJson) : await loadRuleByURL()
return rule
}
module.exports = {
applyConfig,
loadRuleByURL,
getRule,
obtainSearchResult,
clearCache,
makeupSearchOption,
obtainDetailResult,
asyncCacheSearchResult
}
================================================
FILE: src/main/service.js
================================================
const processConfig = require('./process-config')
const {start} = require('./api')
async function startServer () {
let configVariable = null
try {
const args = process.argv.splice(2)
const configPath = args[0]
if (configPath) {
configVariable = processConfig.extractConfigVariable(require(configPath)())
}
} catch (e) {
console.error(e.message)
}
const newConfig = processConfig.getConfig(configVariable)
configVariable ? console.info('使用自定义配置启动服务', configVariable, newConfig) : console.info('使用默认配置启动服务', configVariable, newConfig)
const {port, ip, local, message} = await start(newConfig, true)
if (message) {
console.error(message)
} else {
console.info(`启动成功,本地访问 http://${local}:${port},IP访问 http://${ip}:${port}`)
}
}
startServer()
================================================
FILE: src/renderer/App.vue
================================================
<template>
<div id="app">
<router-view></router-view>
</div>
</template>
<script>
export default {
methods: {},
created () {
},
mounted () {
}
}
</script>
<style lang="scss">
/* CSS */
.el-scrollbar {
height: 100%;
.el-scrollbar__wrap {
overflow-x: hidden;
}
}
.header-submenu {
.el-menu--popup {
min-width: 120px !important;
}
}
.el-textarea__inner[disabled="disabled"], .el-input__inner[disabled="disabled"] {
color: #909399 !important;
}
</style>
================================================
FILE: src/renderer/assets/.gitkeep
================================================
================================================
FILE: src/renderer/assets/fonts/iconfont.css
================================================
@font-face {font-family: "iconfont";
src: url('iconfont.eot?t=1574010210992'); /* IE9 */
src: url('iconfont.eot?t=1574010210992#iefix') format('embedded-opentype'), /* IE6-IE8 */
url('data:application/x-font-woff2;charset=utf-8;base64,d09GMgABAAAAAATMAAsAAAAACXgAAAR/AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHEIGVgCDEgqGQIVIATYCJAMQCwoABCAFhG0HRBssCBEVpCmQ/RxwN5dtSZPxkfwULBfx8ea0bLBIyszMHUE01mbv7lXBEwk+0j1qxKunTinUQkjioXGAp12ltJPdTvZD3WPIOshvKAFo1XRxzcL4EypNxtJ89Sn7Sb9wuQ9gFUohJELyP5fTa0y6MJufsnIZc9EYww4DjHveO6DGWLjIChSjLGLXzVYPJvA8gaZV0AXcvrt3MFAV4rRAPGakJQDVY1cSWKFuqkasLThcI5DU0yb8WeDJ+H58MsZRQVLJIM49SXVLBscXgGEZE60hoKjOi9smMmYACnEd6T8FgpWaAUHzoycmb6sqQnr1NhHDMOx0U3b/4SWZqIKob4Oymc68guKVTJkCQrJenFYtiL7Q5DrmAWIZIA4YZbm1oS1XSNUkkUzUMVVRfTIzaVVDPd/aU9VwDSjUX+0TjI5KC3qlQ1d1sjsEO06cuXAEt+q6dKd4n0xX7+iy6rCmYPt2/lHj4aqT6qPbh7ulOuRQyfZ+qL92aJ/aqRBUVBQFmEeHh/m7d4trC0ZHBaIoKtbaOiL36ZnZxWgYk4B6lNjiAvp1FLfqRlF5q1irqHEHdKGqPsSS8rZVekhU4ZmEFIUbxsDRidwooEeErixKajTSoZkZ0IrQaEIIIUbLQF4VI7Jzkg7cqyb544CHRH2EZDgDhZ5LZuwdVxRWVbJDxBHRHGR0ctWUBntkon7c1pwmLKLNNJG/iyzxMnf78/n+uzODjNrCa6pjuVILC4oymiTYp2V3rDMlyk3qKOi7s32aR67764hrZ81WTl/5KnnMQswkyPTSVOU5Lo/CNL4/PPhUyUDYndDiae1IKA/rMSdQTFrMk8Qp8vdMgy1cPlJgVKHRa+TB2eLSXQ3p+Xg/sn7ltSUOkSdVHRtZa9wWnOm/m8/f449bk2fIce43pguLCK/kkhEW4uJGExEVUedSETYiRAuLg9wRTjasWUuch9t4G9qtSmFjFo/L81gm+xa3mIbbt7m827zsOwHdrRSeJg+eaOW6qdrbyiVgd9ALn8JI/LYVa0SEZIWQxpMG5eIbWKLfEsHA2yLs938PP75nI0aX8JW2/Y0PG1/1PjSc1Jw01AZDPoD/P5ggYwQr8ZHQSOBQ5mB+mA0vsXFWd+eOx7Ftv1EZKzr68qO3IFkxZoEydYnMiguqF5GI/QHlrhLtpah27I+hzB5RPB+0WQlNTfwjPIT7VQQnAI431O8KQFKzAlndGqFQZlDRsglVdVvQtAEem1tGUAgEUWjAOqxjEEAcH8BDHAACji+BSPkBibg/kHGCAHShsdmTQT0ygp0gV1RUvKkteTKySwOz+ozaDrm4VQbnDlnaKCVh7M+eMEGqWLCsdapqyAiPdGSXYRiYZuEOpYaN6pxFkYl6U1jy6ARcCMgpqJDCG2mV2MRonVUQ+PwZ1KxBThIwP+J3IBYtfpIIxRkIJ/2UCbMrgxerWkopw00yBBuRI7wwSClG5uhRHSipUFMgMctE1M5k1YfNG8c13AUAnfgKYqTIUaKCfNNqYwsX8kQ75u5g32wfreMAAA==') format('woff2'),
url('iconfont.woff?t=1574010210992') format('woff'),
url('iconfont.ttf?t=1574010210992') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+ */
url('iconfont.svg?t=1574010210992#iconfont') format('svg'); /* iOS 4.1- */
}
.iconfont {
font-family: "iconfont" !important;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-github:before {
content: "\f01ca";
}
.icon-qrcode:before {
content: "\e700";
}
.icon-router:before {
content: "\ec62";
}
================================================
FILE: src/renderer/assets/scss/app.scss
================================================
@import "./element-variables";
@import "~element-ui/packages/theme-chalk/src/common/var";
$font-size: 14px;
$color-border: #e6e6e6;
$color-title: #555555;
$color-sub-title: #5b5b5b;
$color-text-gray: #b3b3b3;
html, body, #app, .el-container {
height: 100%;
}
body {
margin: 0;
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
}
.drag {
-webkit-app-region: drag;
}
.no-drag {
-webkit-app-region: no-drag;
}
.el-button, .el-radio__label, .el-checkbox__label {
font-weight: normal;
}
.el-button [class*="iconfont"] + span {
margin-left: 5px;
}
.el-table th.is-leaf {
padding-top: 5px;
padding-bottom: 5px;
}
================================================
FILE: src/renderer/assets/scss/element-variables.scss
================================================
/* 改变主题色变量 */
$--color-primary: #6078ea !default;
/* 改变 icon 字体路径变量,必需 */
$--font-path: '~element-ui/lib/theme-chalk/fonts';
================================================
FILE: src/renderer/components/AsideMenu.vue
================================================
<template>
<el-scrollbar>
<div class="aside-menu">
<div class="menu-setting">
<div class="menu-setting-checkbox">
<el-checkbox v-model='localSetting.memoryLastRule'
@change='handleApplySetting' label="记住选择" border
size="mini"></el-checkbox>
<el-button size="mini" class="menu-setting-button-right" @click="handleReloadRules(true)">刷新</el-button>
</div>
</div>
<el-menu :default-active="active" v-loading="loading" @select="emitRuleChangeByID">
<el-menu-item v-for="it in filterRules" :key="it.id" :index="it.id">
<div slot="title" class="menu-item-title">
<span class="menu-item-title-text">
<el-image :src="it.icon||formatDefaultIcon(it.id)" class="favicon">
<i slot="placeholder"></i>
<i slot="error"></i>
</el-image>
<span class="source-name">{{it.name}}</span>
</span>
<el-tooltip v-if="config.cloud&&config.cloudUrl" effect="dark" placement="right">
<div slot="content">此源站将使用云解析</div>
<i class="el-icon-cloudy"></i>
</el-tooltip>
<el-tooltip v-else-if="it.proxy" effect="dark" placement="right">
<template v-if="config.proxy">
<div slot="content">此源站需要设置代理,已启用代理</div>
<i class="el-icon-connection"></i>
</template>
<template v-else>
<div slot="content">此源站需要设置代理,
<browser-link class="tooltip-content-proxy"
:href="$config.proxyDocURL" type="primary">查看详情
</browser-link>
</div>
<i class=" el-icon-warning-outline"></i>
</template>
</el-tooltip>
</div>
</el-menu-item>
</el-menu>
</div>
</el-scrollbar>
</template>
<script>
import {ipcRenderer} from 'electron'
import ruleList from '~/rule'
import BrowserLink from './BrowserLink'
export default {
components: {BrowserLink},
props: {
active: String
},
data () {
return {
ruleList: this.$localSetting.get('rule_list') || ruleList,
localSetting: {
memoryLastRule: false
},
config: ipcRenderer.sendSync('get-server-config'),
loading: false
}
},
watch: {
active (id) {
// this.emitRuleChangeByID(id)
}
},
computed: {
filterRules () {
if (this.config.showProxyRule) {
return this.ruleList
} else {
return this.ruleList.filter((it) => !it.proxy)
}
}
},
methods: {
emitRuleChangeByID (id) {
const rules = this.filterRules
let active = rules[0]
for (let i = 0; i < rules.length; i++) {
if (id === rules[i].id) {
active = rules[i]
break
}
}
this.$emit('change', active)
},
handleRefreshActiveRule () {
const localSetting = this.$localSetting.get()
if (Object.keys(localSetting).length > 0) {
this.localSetting = localSetting
}
let active = this.active
if (localSetting.memoryLastRule && localSetting.last_rule_id) {
active = localSetting.last_rule_id
}
this.emitRuleChangeByID(active)
},
formatDefaultIcon (id) {
return `${this.$config.icons.baseUrl}/${id}.${this.$config.icons.extension}`
},
handleApplySetting () {
this.$localSetting.save(this.localSetting)
},
handleReloadRules (reload) {
if (reload) this.loading = true
this.$http.get('load-rule').then((rsp) => {
this.$localSetting.saveValue('rule_list', rsp.data)
this.ruleList = rsp.data
this.handleRefreshActiveRule()
this.emitChangeRules()
}).catch((err) => {
this.emitChangeRules(err)
}).finally(() => {
if (reload) this.loading = false
})
},
emitChangeRules (err) {
if (err) {
this.$emit('rule-refresh-finished', 'error', '刷新规则失败')
} else {
const title = `成功刷新${this.ruleList.length}个规则`
const message = this.config.showProxyRule ? null : `其中${this.ruleList.length - this.filterRules.length}个已隐藏,如需显示请更改设置`
this.$emit('rule-refresh-finished', 'success', title, message)
}
}
},
created () {
},
mounted () {
// 接收设置刷新的通知
this.$on('global:event-config-refreshed', (config, oldConfig) => {
this.config = config
// 如果修改规则url 重新加载列表
if (config.ruleUrl !== oldConfig.ruleUrl) {
this.handleReloadRules(false)
} else if (config.showProxyRule !== oldConfig.showProxyRule) {
// 如果仅修改了显示所有开关 重新检查过滤列表
this.emitChangeRules()
}
})
this.handleRefreshActiveRule()
this.handleReloadRules()
},
activated () {
}
}
</script>
<style lang="scss" scoped>
.el-scrollbar {
border-right: solid 1px $color-border;
}
.el-menu {
border: none !important;
}
.el-menu-item:active {
cursor: pointer;
}
.el-menu-item.is-active {
background-color: $--color-primary-light-9;
}
.menu-item-title {
display: flex;
align-items: center;
.menu-item-title-text {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1;
}
.el-link {
justify-content: left;
}
}
.tooltip-content-proxy {
cursor: pointer;
color: $--color-primary;
}
.favicon {
margin-right: 8px;
width: 18px;
height: 18px;
}
.menu-setting {
text-align: center;
.menu-setting-checkbox {
margin-top: 12px;
margin-bottom: 12px;
}
.menu-setting-button-right {
margin-left: 5px;
}
.menu-setting-update {
display: flex;
justify-content: center;
.el-button {
padding: 7px 12px;
margin-left: 10px;
}
}
}
.el-icon-warning-outline {
color: $--color-danger !important;
font-size: 16px;
}
.el-icon-connection {
font-size: 16px;
color: $--color-success !important;
}
.el-icon-cloudy {
font-size: 16px;
color: $--color-primary !important;
}
</style>
================================================
FILE: src/renderer/components/BrowserButton.vue
================================================
<template>
<el-button :size="size" :icon='icon' :type="type" :plain='plain' @click="handleClickButton">
<slot></slot>
</el-button>
</template>
<script>
export default {
props: {
'href': String,
'type': String,
'size': String,
'icon': String,
'plain': Boolean
},
methods: {
handleClickButton (e) {
if (this.href) {
window.open(this.href)
} else {
this.$emit('click')
}
}
}
}
</script>
<style scoped>
</style>
================================================
FILE: src/renderer/components/BrowserLink.vue
================================================
<template>
<el-link :type="type"
:target="target||'_blank'"
:underline="underline||false"
:icon="icon"
:class="linkClass"
@click="handleClickLink">
<slot></slot>
</el-link>
</template>
<script>
import {shell} from 'electron'
export default {
props: {
'href': String,
'underline': Boolean,
'type': String,
'button': Boolean,
'size': String,
'target': String,
'icon': String
},
computed: {
linkClass () {
const linkClass = this.href ? 'browser-link' : 'browser-link browser-link-empty'
return this.button ? `el-button el-button--default el-button--${this.size} browser-link-button` : linkClass
}
},
methods: {
handleClickLink () {
if (this.href) {
shell.openExternal(this.href)
}
}
}
}
</script>
<style lang="scss" scoped>
.browser-link {
font-size: inherit;
font-weight: normal;
}
.browser-link-button {
vertical-align: baseline;
}
.browser-link-empty, .browser-link-empty:hover {
color: $--color-text-primary !important;
cursor: inherit;
}
</style>
================================================
FILE: src/renderer/components/DetailDialog.vue
================================================
<template>
<el-dialog :visible.sync="dialog.show"
width="80%">
<div slot="title">{{dialog.name}}</div>
<div class="detail-dialog-content"
v-loading="loading"
element-loading-text="此源站需要分析详情,请稍等">
<div class="dialog-error-parent" v-show="!loading&&errorMessage">
<span class="dialog-error-message">{{errorMessage}}</span>
</div>
<div v-if="detail" class="detail-info">
<div class="detail-info-left">
<browser-link v-show="detail.magnet" type="primary" :href="detail.magnet">{{detail.magnet}}</browser-link>
<div v-if="detail.files">
<div class="row-title">文件列表</div>
<div v-for="f in detail.files" :key="f.name" class="file-row-item">
<span class="file-row-item-left">{{f.name}}</span>
<span class="file-row-item-right">{{f.size | size}}</span>
</div>
</div>
</div>
<div class="detail-info-right">
<qrcode v-show="detail.magnet" :value="detail.magnet"
:options="{ width: 150, margin:0 }"></qrcode>
<item-button-group :item="detail"
:list="['miwifi','copy']"
class="detail-button-group">
</item-button-group>
</div>
</div>
</div>
</el-dialog>
</template>
<script>
import BrowserLink from './BrowserLink'
import ItemButtonGroup from './ItemButtonGroup'
import Qrcode from '@chenfengyuan/vue-qrcode'
export default {
components: {Qrcode, ItemButtonGroup, BrowserLink},
props: {
'dialog': {
type: Object,
required: true
}
},
data () {
return {
loading: false,
detail: null,
detailCache: {},
errorMessage: null
}
},
watch: {
dialog (val) {
const vm = this
if (vm.detail && vm.detail.url && vm.detail.url.indexOf(val.path) !== -1) {
return
}
vm.errorMessage = null
const path = val.path
const id = val.id
if (path in vm.detailCache) {
vm.detail = vm.detailCache[path]
} else {
vm.loading = true
this.$http.get('/detail', {
params: {id, path}
}).then((rsp) => {
const data = rsp.data
vm.detail = data
if (data) {
vm.detailCache[path] = data
}
}).catch((err) => {
vm.errorMessage = err.message
}).finally(() => {
vm.loading = false
})
}
}
},
methods: {},
created () {
}
}
</script>
<style lang="scss" scoped>
.detail-dialog-content {
min-height: 150px;
position: relative;
}
.dialog-error-parent {
height: 100%;
position: absolute;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
.dialog-error-message {
text-align: center;
font-size: 16px;
color: $--color-danger;
position: absolute;
margin: auto;
}
}
/deep/ .el-dialog__header {
word-break: break-all;
margin-right: 20px;
padding-bottom: 0;
}
.el-loading-mask {
display: flex;
justify-content: center;
align-items: center;
}
.el-loading-spinner {
top: auto;
margin-top: auto;
}
.file-row-item {
margin-top: 5px;
display: flex;
}
.file-row-item-left {
flex: 1;
}
.file-row-item-right {
min-width: 100px;
text-align: right;
}
.row-title {
font-size: 1.17em;
font-weight: bold;
margin: 15px 0 5px 0;
}
.detail-info {
display: flex;
.detail-info-left {
flex: 1;
margin-right: 20px;
}
}
.detail-button-group /deep/ .el-link, .detail-button-group /deep/ .el-button {
margin-top: 10px;
width: 100%;
display: block;
margin-left: 0 !important;
}
</style>
================================================
FILE: src/renderer/components/GithubBadge.vue
================================================
<template>
<i class="github-badge">
</i>
</template>
<script>
export default {}
</script>
<style scoped>
.github-badge {
z-index: -100;
top: 0;
position: absolute;
right: 0;
}
</style>
================================================
FILE: src/renderer/components/GuidePage.vue
================================================
<template>
<div class="guide-page">
<el-alert
class="guide-page-alert"
v-show="title"
:type="type"
:closable="false"
:description="message"
show-icon>
<span slot="title">{{title}}</span>
</el-alert>
<div class="guide-page-content">
<div class="guide-page-log" v-if="serverInfo">
<div>搜索服务已启动,启动模式:{{serverInfo.customServerPort?'自定义':'自动分配'}},<span class="guide-page-log-success">{{serverInfo.local}}:{{serverInfo.port}}</span>
</div>
<div v-if="serverInfo.proxy">
代理已启用:<span class="guide-page-log-success">{{serverInfo.proxyType}}://{{serverInfo.proxyHost}}:{{serverInfo.proxyPort}}</span>,请在设置中“测试连接”检查代理是否可用
</div>
<div v-if="serverInfo.filterBare||serverInfo.filterEmpty">内容过滤已启用</div>
<div v-show="serverInfo.message" class="guide-page-log-error">{{serverInfo.message}}</div>
</div>
<div v-for="it in $config.guide.content" class="guide-content">
<span class="guide-title">{{it.title}}</span>
<div class="guide-content-item" v-for="item in it.items">
<browser-link v-if="item.link" :href="item.link" type="primary">{{item.text}}</browser-link>
<span v-else class="guide-content-item-text">{{item.text}}</span>
</div>
</div>
</div>
</div>
</template>
<script>
import BrowserLink from './BrowserLink'
import {ipcRenderer} from 'electron'
export default {
props: ['message', 'type', 'title'],
components: {
BrowserLink
},
data () {
return {
serverInfo: null
}
},
computed: {},
methods: {},
created () {
// 接收设置刷新的通知
this.$on('global:event-config-refreshed', (config, oldConfig) => {
ipcRenderer.send('get-server-info')
})
ipcRenderer.on('on-get-server-info', (event, serverInfo, config) => {
if (serverInfo) {
const info = {}
Object.assign(info, serverInfo, config)
this.serverInfo = info
} else {
this.serverInfo = {message: '搜索服务启动失败,请查看日志'}
}
})
ipcRenderer.send('get-server-info')
}
}
</script>
<style scoped lang="scss">
.guide-page {
margin-top: 70px;
padding: 0 20px 20px 20px;
position: absolute;
z-index: 2000;
left: 0;
right: 0;
}
.guide-page-alert {
min-height: 36px;
}
.guide-page-content {
position: absolute;
top: 0;
margin-top: 60px;
}
.guide-content {
margin: 10px 0 20px 0;
}
.guide-title {
display: block;
font-size: 1.3em;
margin: 10px 0;
font-weight: bold;
color: $--color-text-primary;
}
.guide-content-item {
margin-bottom: 5px;
margin-left: 20px;
font-size: 16px;
.el-link {
font-size: 16px;
}
}
.guide-content-item-text {
margin-right: 10px;
}
.footerText {
color: $color-text-gray;
font-size: $font-size;
}
.guide-page-log {
color: $--color-text-primary;
line-height: 300%;
}
.guide-page-log-success {
color: $--color-success;
}
.guide-page-log-error {
color: $--color-danger;
}
</style>
================================================
FILE: src/renderer/components/HeaderVersion.vue
================================================
<template>
<browser-link target="_self">
<div class="header-version align-items-center">
<img src="../assets/logo.png" width="36" height="36"/>
<div>
<div class="header-version-text">{{ appName }}</div>
<div class="header-version-text">v{{ version }}</div>
</div>
</div>
</browser-link>
</template>
<script>
import BrowserLink from './BrowserLink'
export default {
components: {BrowserLink},
data () {
return {
version: this.$app.version,
appName: this.$app.name
}
},
created () {
}
}
</script>
<style lang="scss" scoped>
.header-version {
}
.header-version-text {
margin-left: 10px;
color: $color-title;
line-height: 1.3;
font-size: 16px;
}
.el-main {
padding: 0 !important;
}
.el-header {
height: 80px !important;
.header {
height: 100%;
}
}
.title {
font-size: 18px;
color: $color-title;
}
.align-items-center {
display: flex;
align-items: center;
}
.el-checkbox {
font-weight: normal !important;
}
.el-header {
border-bottom: 1px solid $color-border;
}
.scroll-container {
height: 100%;
.el-scrollbar {
height: 100%;
}
.el-scrollbar__wrap {
overflow-x: hidden !important;
}
.el-scrollbar__view {
height: auto !important;
}
}
.el-dialog__header {
padding-bottom: 0;
}
</style>
================================================
FILE: src/renderer/components/HighlightName.vue
================================================
<template>
<browser-link :href="url" type="primary">
<el-tag size="mini" v-show="resolution"
disable-transitions :type="getResolutionTagType(resolution)">
{{resolution}}
</el-tag>
<span v-html="highlight(keyword, value, 'highlight-name')"></span>
</browser-link>
</template>
<script>
import BrowserLink from './BrowserLink'
export default {
components: {BrowserLink},
props: ['keyword', 'resolution', 'url', 'value'],
methods: {
getResolutionTagType (resolution) {
if (resolution) {
const regx = {
'4K': 'primary',
'2K': 'danger',
'1080P': 'success'
}
for (let key in regx) {
if (new RegExp(key, 'i').test(resolution)) {
return regx[key]
}
}
return 'info'
}
},
/**
* 点击磁力链
*/
handleOpenMagnet (url) {
window.open(url)
}
}
}
</script>
<style lang="scss">
.highlight-name {
color: $--color-danger
}
</style>
================================================
FILE: src/renderer/components/ItemButtonGroup.vue
================================================
<template>
<div>
<qrcode-popover :text="item.magnet" :title="item.name" v-if="show('qrcode')">
<browser-button :size="size" icon="iconfont icon-qrcode" class="popover-button">
二维码
</browser-button>
</qrcode-popover>
<browser-link :size="size" icon="el-icon-document"
:href="baseURL + item.detailUrl|formatURL"
:button="true"
v-if="config.showSourceLink&&show('detail')"
:_blank="true">
源站详情
</browser-link>
<browser-button :size="size" icon="el-icon-files"
v-if="show('detail-dialog')"
type="primary"
plain
@click="handleClickDetail(item)">
查看详情
</browser-button>
<browser-link :size="size" icon="iconfont icon-router"
:href="formatMiWiFiURL"
v-if="show('miwifi')"
:button="true"
:_blank="true">
小米路由
</browser-link>
<browser-button :size="size" type="primary" plain icon="el-icon-copy-document"
v-if="show('copy')"
@click="handleCopyMagnet(item.magnet)">
复制链接
</browser-button>
</div>
</template>
<script>
import {ipcRenderer} from 'electron'
import {Base64} from 'js-base64'
import QrcodePopover from './QrcodePopover'
import BrowserButton from './BrowserButton'
import BrowserLink from './BrowserLink'
export default {
props: {
'baseURL': String,
'item': Object,
'list': Array
},
components: {
BrowserLink,
BrowserButton,
QrcodePopover
},
data () {
return {
config: ipcRenderer.sendSync('get-server-config'),
size: 'mini'
}
},
computed: {
/**
* 小米路由
*/
formatMiWiFiURL () {
const url = this.item.magnet
return 'http://d.miwifi.com/d2r/?url=' + Base64.encodeURI(url)
}
},
methods: {
show (val) {
return !this.list || this.list.length <= 0 || this.list.indexOf(val) !== -1
},
/**
* 复制链接
* @param url
*/
handleCopyMagnet (url) {
this.$copyText(url).then((e) => {
this.$message({
message: '复制成功',
type: 'success'
})
})
},
/**
* 详情
* @param item
*/
handleClickDetail (item) {
this.$emit('show-detail', item)
}
}
}
</script>
<style scoped lang="scss">
.header-version {
width: 180px;
}
.header-version-text {
margin-left: 10px;
color: $color-title;
line-height: 1.3;
}
.popover-button {
margin-right: 5px;
}
.el-button + .el-button {
margin-left: 5px;
}
.el-button--mini {
padding: 7px 10px;
}
</style>
================================================
FILE: src/renderer/components/NumberInput.vue
================================================
<template>
<el-input class="number-input" v-model="value" size="mini" :placeholder="placeholder">
<span slot="append" class="number-input__append">{{append}}</span>
</el-input>
</template>
<script>
export default {
props: ['value', 'placeholder', 'append'],
methods: {}
}
</script>
<style lang="scss">
.number-input {
width: auto !important;
.el-input__inner {
width: 80px;
text-align: center;
}
}
</style>
================================================
FILE: src/renderer/components/PagerFooter.vue
================================================
<template>
<div class="pager-footer">
<span class="header-disclaimer-text">{{$config.footerText}}</span>
</div>
</template>
<script>
export default {
props: ['text'],
methods: {},
mounted () {
}
}
</script>
<style lang="scss" scoped>
.pager-footer {
position: absolute;
width: 100%;
z-index: 2000;
bottom: 0;
padding: 15px;
min-height: 20px;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
background-color: cornsilk;
}
.header-disclaimer-text {
color: $color-text-gray;
font-size: 12px;
}
</style>
================================================
FILE: src/renderer/components/PagerHeader.vue
================================================
<template>
<div class="pager-header">
<div class="header-left" @dblclick="dblclick">
<header-version></header-version>
</div>
<div class="header-center">
<div class="header-center-placeholder" @dblclick="dblclick"></div>
<slot name="center"></slot>
</div>
<div class="header-right">
<el-menu mode="horizontal" default-active="index" @select="select">
<el-menu-item index="index">首页</el-menu-item>
<el-menu-item index="setting">设置</el-menu-item>
<template v-for="menuItem in $config.menu">
<el-submenu v-if="menuItem.submenu" :index="menuItem.index" popper-class="header-submenu"
:key="menuItem.index">
<template slot="title">{{menuItem.text}}</template>
<el-menu-item v-for="subItem in menuItem.submenu" :key="subItem.link">
<browser-link :href="subItem.link" :underline="false">{{subItem.text}}</browser-link>
</el-menu-item>
</el-submenu>
<el-menu-item v-else :index="menuItem.index" :key="menuItem.index">
<browser-link :href="menuItem.link" :underline="false">{{menuItem.text}}</browser-link>
</el-menu-item>
</template>
</el-menu>
</div>
</div>
</template>
<script>
import HeaderVersion from './HeaderVersion'
import BrowserLink from './BrowserLink'
export default {
props: ['dblclick', 'select'],
components: {
BrowserLink,
HeaderVersion
},
data () {
return {
defaultActive: 'index'
}
},
created () {
const menus = this.$config.menu
if (menus && menus.length > 0) {
this.defaultActive = menus[0].index
}
}
}
</script>
<style lang="scss" scoped>
.pager-header {
display: flex;
.header-right, .el-menu, .el-menu .el-submenu, /deep/ .el-menu .el-submenu__title, .el-menu .el-link {
height: 100%;
}
.header-right {
margin-left: auto;
z-index: 1000;
}
.el-menu {
background-color: transparent;
border: none !important;
}
/deep/ .el-menu-item {
background-color: transparent !important;
}
.header-left {
display: flex;
}
.header-center {
position: relative;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
.pager-header-input {
width: 100%;
}
.header-center-placeholder {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
}
}
</style>
================================================
FILE: src/renderer/components/PagerItems.vue
================================================
<template>
<el-table
border
default-expand-all=""
:data="items"
:empty-text="emptyMessage||'什么也没搜到'"
style="width: 100%">
<el-table-column
type="index"
width="40"
align="center"
label="#">
</el-table-column>
<el-table-column
label="名称">
<template slot-scope="scope">
<highlight-name :keyword="keyword" :url="scope.row.magnet"
:resolution="scope.row.resolution" :value="scope.row.name">
</highlight-name>
</template>
</el-table-column>
<el-table-column
label="大小"
align="right"
:sort-by="['size','hot','date']"
sortable
width="100">
<template slot-scope="scope">
<span>{{scope.row.size| size}}</span>
</template>
</el-table-column>
<el-table-column type="expand">
<div class="page-column-expand" slot-scope="scope">
<div>
<span class="page-item-expand">
<span class="page-item-expand-label">时间</span>
<span>{{scope.row.date|date}}</span>
</span>
<span class="page-item-expand" v-show="typeof scope.row.hot=='number'">
<span class="page-item-expand-label">人气</span>
<span>{{scope.row.hot}}</span>
</span>
</div>
<div class="page-column-expand-action">
<item-button-group :baseURL="baseURL"
:item="scope.row"
:list="getButtons(scope.row)"
@show-detail="handleShowDetail">
</item-button-group>
</div>
</div>
</el-table-column>
</el-table>
</template>
<script>
import HighlightName from '../components/HighlightName'
import ItemButtonGroup from './ItemButtonGroup'
export default {
props: {'items': Array, 'keyword': String, 'baseURL': String, 'emptyMessage': String},
components: {HighlightName, ItemButtonGroup},
methods: {
handleShowDetail (item) {
this.$emit('show-detail', {name: item.name, path: item.detailUrl})
},
getButtons (item) {
return item.magnet ? ['qrcode', item.detailUrl ? 'detail' : null, 'miwifi', 'copy'] : ['detail', 'detail-dialog']
}
},
mounted () {
}
}
</script>
<style lang="scss" scoped>
/deep/ .el-table__expanded-cell[class*=cell] {
padding: 10px;
}
.page-column-expand {
display: flex;
align-items: center;
// margin-left: 40px;
}
.page-column-expand-action {
text-align: right;
flex: 1;
}
.page-item-expand-label {
color: #99a9bf;
margin-right: 8px;
}
.page-item-expand {
color: #606266;
margin-right: 15px;
}
.el-table th {
padding-top: 7px !important;
padding-bottom: 7px !important;
}
</style>
================================================
FILE: src/renderer/components/QrcodePopover.vue
================================================
<template>
<el-popover
placement="top"
:title="title"
width="150"
:close-delay="100"
popper-class="qrcode-popover"
trigger="hover">
<qrcode :value="text" :options="{ width: 150, margin:0 }"></qrcode>
<slot slot="reference"></slot>
</el-popover>
</template>
<script>
import VueQrcode from '@chenfengyuan/vue-qrcode'
export default {
props: ['title', 'text'],
components: {
'qrcode': VueQrcode
},
methods: {},
mounted () {
}
}
</script>
<style>
.qrcode-popover .el-popover__title {
font-size: 14px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
</style>
================================================
FILE: src/renderer/components/Router.vue
================================================
<template>
<router-link :to="router" :tag="tag||'span'">
<slot></slot>
</router-link>
</template>
<script>
export default {
props: {
'name': String,
'query': Object,
'params': Object,
'tag': String
},
computed: {
router () {
const query = {}
Object.assign(query, this.$route.query, this.query)
const router = {query: query}
if (this.name) {
router['name'] = this.name
}
router['params'] = this.params || this.$route.params
return router
}
}
}
</script>
================================================
FILE: src/renderer/components/SearchInput.vue
================================================
<template>
<div class="search-input">
<el-input :placeholder="placeholder"
@keyup.enter.native="emitClickSearch"
v-model="value"
clearable
size="small">
<el-button slot="append" icon="el-icon-search" @click="emitClickSearch">搜索</el-button>
</el-input>
</div>
</template>
<script>
export default {
props: ['name', 'keyword'],
data () {
return {
value: null,
placeholder: null
}
},
watch: {
keyword (val) {
this.value = val
}
},
methods: {
emitClickSearch () {
const value = this.value || this.placeholder
this.value = value
this.$emit('search', value)
}
},
created () {
this.placeholder = this.$config.searchPlaceholder[Math.floor(Math.random() * this.$config.searchPlaceholder.length)]
this.value = this.keyword
},
mounted () {
}
}
</script>
================================================
FILE: src/renderer/components/SearchPagination.vue
================================================
<template>
<!--页码-->
<el-pagination
@current-change="emitPageChanged"
:current-page="page"
background
layout="prev, pager, next"
:pager-count="5"
:page-count="50">
</el-pagination>
</template>
<script>
export default {
props: {
page: Number
},
components: {},
methods: {
emitPageChanged (page) {
this.$emit('change', page)
}
},
created () {
}
}
</script>
<style lang="scss" scoped>
.el-pagination {
padding-right: 0 !important;
/deep/ .number:last-child {
display: none;
}
/deep/ .btn-next {
margin-right: 0 !important;
}
}
</style>
================================================
FILE: src/renderer/components/SearchSort.vue
================================================
<template>
<div class="search-sort">
<!--源站按钮-->
<browser-link v-show="config.showSourceLink" :button="true" size="mini" :href="url|formatURL" :_blank="true"
class="link-button">
去源站
</browser-link>
<!--排序方式-->
<!--
<el-dropdown>
<el-button size="mini">{{getLabelByKey(checkedSortKey)||'选择排序'}}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown" @command="emitSortChanged">
<el-dropdown-item v-for="(value, key) in paths" :key="key"
:command="key" class="dropdown-sort">{{getLabelByKey(key)}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
-->
<el-radio-group v-model="checkedLabel" size="mini" @change="emitSortChanged">
<el-radio-button v-for="(value, key) in paths" :key="key" :label="getLabelByKey(key)"></el-radio-button>
</el-radio-group>
<!--调整窗口-->
<el-dropdown @command="emitWindowChanged" v-if="false">
<el-button size="mini">{{windowName||'调整窗口'}}<i class="el-icon-arrow-down el-icon--right"></i>
</el-button>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item v-for="(name, key) in window" :key="key"
:command="key">{{name}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</template>
<script>
import {ipcRenderer} from 'electron'
import BrowserLink from './BrowserLink'
export default {
props: {
'url': String, 'paths': Object, 'sortKey': String, 'windowKey': String
},
data () {
return {
config: ipcRenderer.sendSync('get-server-config'),
checkedLabel: null,
presetLabels: {
'preset': '默认排序',
'time': '收录时间',
'size': '文件大小',
'hot': '下载人气'
},
window: {
'normal': '标准大小',
'max': '填满窗口'
}
}
},
watch: {
sortKey (val) {
this.checkedLabel = this.getLabelByKey(val)
}
},
computed: {
windowName () {
return this.window[this.windowKey]
}
},
components: {
BrowserLink
},
methods: {
emitSortChanged (label) {
this.$emit('sort-change', this.getKeyByLabel(label))
},
emitWindowChanged (key) {
this.$emit('window-change', key)
},
/**
* 根据key返回显示文字
* @param key
* @returns {*}
*/
getLabelByKey (key) {
return key in this.presetLabels ? this.presetLabels[key] : key
},
/**
* 根据文字返回key
* @param label
* @returns {*}
*/
getKeyByLabel (label) {
for (let key in this.presetLabels) {
if (this.presetLabels.hasOwnProperty(key) && this.presetLabels[key] === label) {
return key
}
}
return label
}
},
created () {
this.checkedLabel = this.getLabelByKey(this.sortKey)
},
mounted () {
// 接收设置刷新的通知
this.$on('global:event-config-refreshed', (config, oldConfig) => {
this.config = config
})
}
}
</script>
<style lang="scss" scoped>
.el-button, .el-radio-group {
margin-top: 2px;
margin-bottom: 2px;
}
.pager-items-pagination {
text-align: right;
}
.link-button {
vertical-align: middle;
margin-right: 15px;
}
.dropdown-sort {
padding: 0;
.dropdown-sort-item {
padding: 0 20px;
}
}
</style>
================================================
FILE: src/renderer/components/ServerConfig.vue
================================================
<template>
<div class="config">
<el-form ref="settingForm" label-width="130px" label-position="left" :model='config' :rules="formRules">
<el-form-item label="默认窗口大小">
<el-radio-group v-model="config.maxWindow">
<el-radio :label="false">标准</el-radio>
<el-radio :label="true">最大化</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="源站">
<el-checkbox v-model="config.showProxyRule">显示所有源站</el-checkbox>
<el-checkbox v-model="config.showSourceLink">显示源站入口</el-checkbox>
</el-form-item>
<tooltip-form-item label="云解析" prop="cloudUrl" tooltip="通过服务器中转来代理请求">
<el-row>
<el-col :span="3">
<el-switch v-model="config.cloud"></el-switch>
</el-col>
<el-col :span="21">
<el-input :size="formSize" v-model="config.cloudUrl"
:placeholder="defaultConfig.cloudUrl||'输入解析服务器的BaseURL'"></el-input>
</el-col>
</el-row>
</tooltip-form-item>
<div v-show="!config.cloud">
<div class="setting-item-dividing"></div>
<el-form-item label="映射端口">
<el-radio-group v-model="config.customServerPort">
<el-radio :label="false">自动分配</el-radio>
<el-radio :label="true">自定义</el-radio>
</el-radio-group>
<el-input :size="formSize" v-model="config.customServerPortValue" class="input-server-port"
type="number" placeholder="端口号" :disabled="!config.customServerPort"></el-input>
<div v-if="appInfo.server" class="server-status-success">
<span>搜索服务已启动,映射端口:{{appInfo.server.port}}</span>
</div>
<div v-else class="server-status-error">服务启动失败,请查看日志</div>
</el-form-item>
<tooltip-form-item label="规则同步URL" tooltip="解析源站的规则文件URL,支持网络链接和本地路径">
<el-input :size="formSize" v-model="config.ruleUrl" :placeholder="defaultConfig.ruleUrl"></el-input>
</tooltip-form-item>
<el-form-item label="启用代理">
<el-row>
<el-col :span="3">
<el-switch v-model="config.proxy"></el-switch>
</el-col>
<el-col :span="10">
<el-radio-group v-model="config.proxyType">
<el-radio label="http">HTTP</el-radio>
<el-radio label="socks5">Socks5</el-radio>
</el-radio-group>
</el-col>
</el-row>
<div>
<el-input :size="formSize" v-model="config.proxyHost" :placeholder="defaultConfig.proxyHost"
class="form-input-center medium-input-width">
<template slot="prepend">地址</template>
</el-input>
<el-input :size="formSize" v-model.number="config.proxyPort" :placeholder="defaultConfig.proxyPort"
type="number"
class="form-input-center small-input-width input-append">
<template slot="prepend">端口</template>
</el-input>
<el-button :size="formSize" @click="handleCheckProxy" class="input-append" :loading="checkProxyLoading">
测试连接
</el-button>
</div>
<el-input v-show="checkProxyInfo" type="textarea"
v-model="checkProxyInfo"
disabled
autosize
class="textarea-proxy-info"
:size="formSize"></el-input>
</el-form-item>
<div class="setting-item-dividing"></div>
<el-form-item label="内容过滤">
<el-checkbox v-model="config.filterBare">过滤暴露内容</el-checkbox>
<el-checkbox v-model="config.filterEmpty">过滤空文件</el-checkbox>
</el-form-item>
<el-form-item label="预加载">
<el-switch v-model="config.preload"></el-switch>
</el-form-item>
<el-form-item label="缓存有效时间">
<el-input :size="formSize" v-model.number="config.cacheExpired" :placeholder="defaultConfig.cacheExpired"
type="number"
class="form-input-center small-input-width">
<template slot="append">秒</template>
</el-input>
</el-form-item>
<el-form-item label="自定义UserAgent" prop="customUserAgentValue">
<el-switch v-model="config.customUserAgent"></el-switch>
<el-input v-show="config.customUserAgent" type="textarea"
v-model="config.customUserAgentValue" placeholder="自定义UserAgent"
:size="formSize"></el-input>
</el-form-item>
<el-form-item label="请求标识">
<el-switch v-model="config.requestIdentifier"></el-switch>
</el-form-item>
</div>
<div class="setting-item-dividing"></div>
<el-form-item label="日志">
<el-input disabled :size="formSize" v-model="appInfo.logDir">
<el-button slot="append" icon="el-icon-folder-opened" @click="handleOpenLoggerDir"></el-button>
</el-input>
</el-form-item>
</el-form>
</div>
</template>
<script>
import TooltipFormItem from './TooltipFormItem'
import {ipcRenderer, shell} from 'electron'
export default {
components: {TooltipFormItem},
props: {
config: Object
},
data () {
return {
formSize: 'mini',
checkProxyLoading: false,
checkProxyInfo: null,
defaultConfig: ipcRenderer.sendSync('get-default-server-config'),
appInfo: ipcRenderer.sendSync('get-app-info'),
formRules: {
cloudUrl: [{
validator: (rule, value, callback) => {
// 如果开启了云解析 但不是url
const regx = /^(http|https):\/\//
const err = this.config.cloud && !regx.test(value) ? new Error('请输入正确的BaseURL') : undefined
callback(err)
},
trigger: 'change'
}],
customUserAgentValue: [{
validator: (rule, value, callback) => {
// 如果开启了自定义UserAgent 但是值不合适
const regx = /^\s*$|[\u4e00-\u9fa5]|magnet|magnetw|magnetx|mwbrowser|mwspider/
const err = this.config.customUserAgent && (!value || regx.test(value)) ? new Error('请输入正确的UserAgent') : undefined
callback(err)
},
trigger: 'change'
}]
}
}
},
methods: {
handleOpenLoggerDir () {
if (this.appInfo.logDir) {
shell.showItemInFolder(this.appInfo.logDir)
}
},
handleCheckProxy () {
this.checkProxyLoading = true
ipcRenderer.send('get-network-info', this.config)
}
},
created () {
// 测试代理的监听
ipcRenderer.on('on-get-network-info', (event, {info, test, time}) => {
this.checkProxyLoading = false
this.checkProxyInfo = (test ? `连接正常 ${time}ms` : '连接失败,请检查地址端口是否正确') + (info ? `\n\n${info.trim()}` : '')
})
}
}
</script>
<style scoped lang="scss">
.config {
position: relative;
}
.el-form-item__content .el-input-group {
vertical-align: middle;
}
.form-input-center /deep/ input {
text-align: center;
}
.placeholder-disabled {
background-color: white;
opacity: 0.7;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.small-input-width {
width: 130px;
}
.input-append {
margin-left: 20px;
}
.medium-input-width {
width: 180px;
}
/deep/ input::-webkit-outer-spin-button,
/deep/ input::-webkit-inner-spin-button {
-webkit-appearance: none;
}
/deep/ input[type="number"] {
-moz-appearance: textfield;
}
.textarea-proxy-info {
margin-top: 10px;
/deep/ .el-textarea__inner {
line-height: 120% !important;
}
}
.config-item-title {
font-size: 20px;
font-weight: bolder;
color: $--color-text-primary;
}
.server-status-error {
color: $--color-danger;
}
.server-status-success {
color: $--color-success
}
.server-status-success-value {
margin-left: 15px;
}
.setting-title {
font-size: 20px;
font-weight: bolder;
color: $--color-text-primary;
border-bottom: $color-border solid 1px;
padding-bottom: 15px;
margin-bottom: 7px;
}
.el-form-item {
margin-bottom: 18px;
}
.setting-item-dividing {
margin-bottom: 10px;
border-bottom: $color-border solid 1px;
}
.input-server-port {
width: 80px;
margin-left: 10px;
/deep/ input {
text-align: center;
}
}
</style>
================================================
FILE: src/renderer/components/SettingGroup.vue
================================================
<template>
<div>
<h3 class="setting-title">{{title}}</h3>
<el-divider></el-divider>
<slot></slot>
</div>
</template>
<script>
export default {
props: ['title']
}
</script>
<style lang="scss">
.setting-title {
color: $color-title;
}
.el-divider--horizontal {
margin: 12px 0 !important;
}
</style>
================================================
FILE: src/renderer/components/SettingItem.vue
================================================
<template>
<el-form-item>
<div slot="label" class="setting-item-label">
<span>{{label}}</span>
<el-tooltip v-if="tooltip" effect="light" :content="tooltip" placement="bottom">
<i class="el-icon-question"></i>
</el-tooltip>
</div>
<slot></slot>
</el-form-item>
</template>
<script>
export default {
props: ['label', 'tooltip']
}
</script>
<style lang="scss">
.setting-item-label {
color: $color-title;
}
.el-form-item {
margin-bottom: 0 !important;
}
.el-form-item__content .el-input-group {
vertical-align: middle !important;
}
</style>
================================================
FILE: src/renderer/components/TooltipFormItem.vue
================================================
<template>
<el-form-item :prop="prop">
<div slot="label" class="setting-item-label">
<span>{{label}}</span>
<el-tooltip v-if="tooltip" effect="light" :content="tooltip" placement="bottom">
<i class="el-icon-question"></i>
</el-tooltip>
</div>
<slot></slot>
</el-form-item>
</template>
<script>
export default {
props: ['label', 'tooltip', 'prop'],
data () {
return {}
},
methods: {},
created () {
}
}
</script>
<style scoped lang="scss">
</style>
================================================
FILE: src/renderer/main.js
================================================
import Vue from 'vue'
import create from '@/plugins/axios'
import App from './App'
import router from './router'
import './plugins'
if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.http = Vue.prototype.$http = create()
Vue.prototype.$resethttp = function () {
Vue.http = Vue.prototype.$http = create()
}
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
components: {App},
router,
template: '<App/>'
}).$mount('#app')
================================================
FILE: src/renderer/pages/Index.vue
================================================
<template>
<el-container>
<el-aside ref="indexAside" width="200px">
<aside-menu
:active="page.current.id"
@rule-refresh-finished="handleRuleRefreshFinished"
@change="handleRuleChanged"></aside-menu>
</el-aside>
<el-main>
<el-scrollbar class="index-main">
<guide-page ref="guidePage" v-show="guidePage.show"
:title="guidePage.title"
:message="guidePage.message" :type="guidePage.type"></guide-page>
<div v-if="activeRule">
<div class="pager-content">
<div ref="pagerSearchHeader">
<div class="search-option">
<!--排序选项-->
<search-sort
class="search-option-left"
:url="page.current.url||activeRule.url"
:paths="activeRule.paths"
:window-key="windowKey"
@sort-change="handleSortChanged"
@window-change="handleWindowChanged"
:sortKey="page.current.sort"></search-sort>
<!--页码-->
<search-pagination :page="page.current.page"
v-if="page.items"
@change="handlePageChanged"></search-pagination>
</div>
</div>
<!--搜索结果-->
<div class="search-items-message" v-if="page.originalCount">
搜索到{{page.originalCount}}条结果
<span v-show="getItemsCount>0">,已过滤{{getItemsCount}}条,如需显示请更改设置</span>
</div>
<div ref="pagerSearchItems" class="pager-search-items" v-loading="loading.table">
<div class="index-main-content" v-if="page.items">
<pager-items :items="page.items"
:emptyMessage="page.emptyMessage"
:keyword="page.current.keyword"
:baseURL="activeRule.url"
@show-detail="handleShowDetailDialog"></pager-items>
<search-pagination class="footer-search-pagination"
:page="page.current.page"
@change="handlePageChanged"></search-pagination>
</div>
</div>
</div>
<el-backtop target=".index-main .el-scrollbar__wrap" ref="backtop">
</el-backtop>
<detail-dialog v-if="detailDialog"
:dialog="detailDialog"></detail-dialog>
</div>
</el-scrollbar>
</el-main>
</el-container>
</template>
<script>
import AsideMenu from '../components/AsideMenu'
import SearchInput from '../components/SearchInput'
import SearchSort from '../components/SearchSort'
import SearchPagination from '../components/SearchPagination'
import PagerItems from '../components/PagerItems'
import GuidePage from '../components/GuidePage'
import PagerHeader from '../components/PagerHeader'
import DetailDialog from '../components/DetailDialog'
export default {
components: {
DetailDialog,
AsideMenu,
SearchSort,
SearchInput,
SearchPagination,
PagerItems,
GuidePage,
PagerHeader
},
data () {
return {
rule: null,
page: {
current: {},
items: null,
emptyMessage: null
},
activeRule: null,
loading: {
table: false,
page: false
},
guidePage: {
show: true,
type: 'success',
title: null,
message: null
},
detailDialog: {
show: false
},
windowKey: 'normal'
}
},
watch: {},
computed: {
getItemsCount () {
return this.page.originalCount - this.page.items.length
}
},
methods: {
handleRuleRefreshFinished (type, title, message) {
this.guidePage.title = title
this.guidePage.message = message
this.guidePage.type = type
console.info(title, message)
},
handleRuleChanged (active) {
if (this.activeRule && this.activeRule.id === active.id) {
return
}
this.activeRule = active
this.$localSetting.saveValue('last_rule_id', active.id)
const keys = Object.keys(active.paths)
this.page.current.id = active.id
this.page.current.sort = keys[0]
this.page.current.page = 1
this.page.current.url = active.url
this.handleRequestSearch()
},
handleClickSearch (keyword) {
this.page.current.keyword = keyword
this.page.current.page = 1
this.handleRequestSearch()
},
handlePageChanged (page) {
this.page.current.page = page
this.handleRequestSearch()
},
handleSortChanged (sortKey) {
this.page.current.sort = sortKey
this.page.current.page = 1
this.handleRequestSearch()
},
handleRequestSearch () {
// 发起请求
const params = this.page.current
if (params.keyword) {
console.info('搜索', JSON.stringify(params, '\t', 2))
this.guidePage.show = false
this.loading.table = true
this.$http.get('search', {
params: params
}).then((rsp) => {
this.page = rsp.data
}).catch((err) => {
this.page.emptyMessage = err.message
this.page.items = []
/*
this.$message({
message: err.message,
type: 'error'
})
*/
}).finally(() => {
this.loading.table = false
})
}
},
handleShowDetailDialog ({name, path}) {
this.detailDialog = {
show: true,
id: this.page.current.id,
name: name,
path: path
}
},
handleWindowChanged (key) {
this.windowKey = key
}
},
created () {
},
mounted () {
// this.handleRequestSearch()
},
head: {
title: function () {
const cur = this.page.current
return cur.keyword ? {
inner: cur.keyword ? `${cur.keyword} - ${cur.page}` : null,
complement: this.$app.appName
} : null
}
}
}
</script>
<style lang="scss" scoped>
.container {
max-width: 960px;
margin: auto;
}
.container-full {
max-width: inherit;
margin: auto;
}
.el-main {
position: relative;
padding: 0;
}
.pager-search-items {
margin-top: 20px;
}
.index-main {
padding: 0 !important;
position: relative;
.el-backtop {
position: absolute;
}
/deep/ .el-scrollbar__view {
position: relative;
}
}
.pager-search-items {
/deep/ .el-loading-spinner {
top: 230px !important;
}
}
.footer-search-option {
margin-top: 20px;
}
.search-option {
display: flex;
align-items: center;
.search-option-left {
flex: 1;
}
}
.pager-content {
padding: 20px;
}
.footer-search-pagination {
margin-top: 15px;
text-align: right;
}
.search-items-message {
color: $--color-success;
font-size: 14px;
line-height: 14px;
margin-top: 15px;
}
</style>
================================================
FILE: src/renderer/pages/Main.vue
================================================
<template>
<el-container>
<div class="header-placeholder drag" @dblclick="handleClickMaxWindow"></div>
<el-header class="drag">
<pager-header :dblclick="handleClickMaxWindow" :select="handleSelectMenu">
<!--搜索框与排序菜单-->
<search-input slot="center" class="no-drag" @search="handleSearch"></search-input>
</pager-header>
</el-header>
<el-main class="main">
<transition name="el-fade-in">
<index class="main-child" v-show="indexActivated" ref="index"></index>
</transition>
<transition name="el-fade-in">
<setting class="main-child" v-show="settingActivated"></setting>
</transition>
</el-main>
<github-badge></github-badge>
</el-container>
</template>
<script>
import {ipcRenderer, shell} from 'electron'
import PagerHeader from '../components/PagerHeader'
import SearchInput from '../components/SearchInput'
import GithubBadge from '../components/GithubBadge'
import Index from '../pages/Index'
import Setting from './Setting'
export default {
components: {
PagerHeader, SearchInput, GithubBadge, Index, Setting
},
data () {
return {
active: 'index'
}
},
computed: {
indexActivated () {
return this.active === 'index'
},
settingActivated () {
return this.active === 'setting'
}
},
methods: {
handleClickMaxWindow () {
ipcRenderer.send('window-max')
},
handleSelectMenu (index) {
if (index === 'index' || index === 'setting') {
this.active = index
}
},
handleSearch (keyword) {
this.$refs.index.handleClickSearch(keyword)
}
},
created () {
/**
* 有新版本
*/
ipcRenderer.on('new-version', (event, data) => {
this.$confirm(data.content, `有新版本 v${data.version}`, {
confirmButtonText: '去更新',
cancelButtonText: '取消',
dangerouslyUseHTMLString: true
}).then(() => {
shell.openExternal(data.url)
}).catch(() => {
})
})
},
mounted () {
// 检查更新
ipcRenderer.send('check-update')
}
}
</script>
<style lang="scss" scoped>
.header-placeholder {
height: 10px;
}
.main {
background-color: white;
padding: 0 !important;
height: 100%;
position: relative;
}
.el-header {
border-bottom: 1px solid $color-border;
}
.main-child {
position: absolute;
width: 100%;
height: 100%;
}
.pager-header-input {
padding-left: 30px;
padding-right: 30px;
max-width: 500px;
margin: auto;
}
.search-input {
margin-left: 15%;
margin-right: 15%;
width: 100%;
}
.el-fade-in-enter-active, .el-fade-in-leave-active {
transition-duration: 0.2s;
}
</style>
================================================
FILE: src/renderer/pages/Setting.vue
================================================
<template>
<el-scrollbar>
<div class="setting">
<server-config v-if="config"
ref="settingInfo"
:config="config"
class="server-config"></server-config>
<div class="setting-button-action">
<el-button :loading="loading.save" type="primary" :disabled="saveDisabled" size="mini"
class="setting-save-button"
@click="handleSaveSetting">
保存
</el-button>
<el-button size="mini" type="info" plain @click="handleResetConfig">重置</el-button>
</div>
</div>
</el-scrollbar>
</template>
<script>
import {ipcRenderer, remote, shell} from 'electron'
import ServerConfig from '../components/ServerConfig'
import BrowserLink from '../components/BrowserLink'
export default {
components: {BrowserLink, ServerConfig},
watch: {
config: {
handler () {
const after = JSON.stringify(this.config)
const local = JSON.stringify(this.localConfig)
// 如果设置改变
this.saveDisabled = after === local
},
deep: true
}
},
data () {
return {
config: null,
localConfig: null,
saveDisabled: true,
loading: {
full: false,
save: false
}
}
},
methods: {
handleSaveSetting () {
this.$refs.settingInfo.$refs.settingForm.validate((valid) => {
if (valid) {
this.loading.save = true
ipcRenderer.send('save-server-config', this.config)
this.localConfig = ipcRenderer.sendSync('get-server-config')
this.saveDisabled = true
}
})
},
handleResetConfig () {
this.config = ipcRenderer.sendSync('get-default-server-config')
}
},
created () {
// 保存配置的监听 - 保存后的配置对象,异常对象,是否通知重载
ipcRenderer.on('on-save-server-config', (event, config, oldConfig, err) => {
this.loading.save = false
this.config = config
if (err) {
this.$message({message: err, type: 'error'})
} else {
this.$resethttp()
this.$message({message: '保存成功', type: 'success', duration: 1000})
this.$emit('global:event-config-refreshed', config, oldConfig)
}
})
// 获取服务配置
this.config = ipcRenderer.sendSync('get-server-config')
this.localConfig = ipcRenderer.sendSync('get-server-config')
}
}
</script>
<style lang="scss" scoped>
.setting {
padding: 20px 40px 45px 40px;
}
.setting-save-button {
min-width: 80px;
}
.setting-button-action {
padding: 0 40px 20px 0;
position: absolute;
right: 0;
bottom: 0;
}
</style>
================================================
FILE: src/renderer/plugins/app.js
================================================
import Vue from 'vue'
import json from '../../../package.json'
Vue.use({
install: (Vue, options) => {
Vue.prototype.$app = {
name: json.name,
appName: json.build.productName,
version: json.version,
description: json.description,
author: json.author,
license: json.license
}
}
})
================================================
FILE: src/renderer/plugins/axios.js
================================================
import {ipcRenderer, remote} from 'electron'
import axios from 'axios'
import URI from 'urijs'
function create () {
const isDev = process.env.NODE_ENV === 'development'
const config = ipcRenderer.sendSync('get-server-config')
const localBaseURL = ipcRenderer.sendSync('api-base-url')
const baseURI = config.cloud && config.cloudUrl ? new URI(config.cloudUrl) : new URI(localBaseURL)
let http = axios.create({
baseURL: baseURI.directory('api').toString(),
timeout: 10000,
responseType: 'json'
})
http.interceptors.response.use(response => {
const data = response.data.data
response.data = data
return response
}, error => {
// 如果有success字段并且是false而且有提示 就替换提示
if (error.response) {
const rsp = error.response.data
if (rsp.hasOwnProperty('success') && rsp.success === false && rsp.message) {
error.message = rsp.message
}
}
return Promise.reject(error)
}
)
return http
}
export default create
================================================
FILE: src/renderer/plugins/clipboard.js
================================================
import Vue from 'vue'
import VueClipboard from 'vue-clipboard2'
Vue.use(VueClipboard)
================================================
FILE: src/renderer/plugins/config.js
================================================
import Vue from 'vue'
Vue.use({
install: (Vue, options) => {
const baseURL = 'https://magnetw.app'
Vue.prototype.$config = {
baseURL: baseURL,
docURL: `${baseURL}/guide`,
icons: {
baseUrl: `${baseURL}/favicon`,
extension: 'ico'
},
searchPlaceholder: ['火影忍者', '钢铁侠', '美国队长', '犬夜叉', '七龙珠', '奥特曼'],
proxyDocURL: `${baseURL}/guide/proxy.html`,
guide: {
content: []
},
menu: []
}
}
}
)
================================================
FILE: src/renderer/plugins/element-ui.js
================================================
import Vue from 'vue'
import Element from 'element-ui'
import 'element-ui/packages/theme-chalk/src/index.scss'
import '../assets/fonts/iconfont.css'
Vue.use(Element)
================================================
FILE: src/renderer/plugins/event-proxy.js
================================================
import Vue from 'vue'
import EventProxy from 'vue-event-proxy'
Vue.use(EventProxy)
================================================
FILE: src/renderer/plugins/filter.js
================================================
import Vue from 'vue'
import moment from 'moment'
function highlightWord (keyword, content) {
const idx = content.toLowerCase().indexOf(keyword)
let t = []
if (idx > -1) {
if (idx === 0) {
t = highlightWord(keyword, content.substring(keyword.length))
t.unshift({
key: true,
str: content.substring(idx, idx + keyword.length)
})
return t
}
if (idx > 0) {
t = highlightWord(keyword, content.substring(idx))
t.unshift({
key: false,
str: content.substring(0, idx)
})
return t
}
}
return [{
key: false,
str: content
}]
}
Vue.prototype.highlight = function (keyword, content, className) {
let str = ''
let array = highlightWord(keyword, content)
array.forEach((it) => {
if (it.key) {
str += `<span class='${className}'>${it.str}</span>`
} else {
str += it.str
}
})
return str
}
Vue.filter('size', function (size) {
if (!size || size <= 0) {
return '0 Bytes'
}
if (/^\d+$/.test(size)) {
let unit = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
let index = Math.floor(Math.log(size) / Math.log(1024))
return (size / Math.pow(1024, index)).toFixed(2) + ' ' + unit[index]
} else {
return size
}
})
Vue.filter('date', function (time) {
if (/^-?\d+$/.test(time)) {
const momentTime = moment(time)
return momentTime.format(momentTime.hour() === 0 && momentTime.minute() === 0 ? 'YYYY-MM-DD' : 'YYYY-MM-DD HH:mm')
} else {
return time
}
})
Vue.filter('date_interval', function (time) {
if (/^\d+$/.test(time)) {
let delta = moment().valueOf() - time
delta /= 60 * 1000
if (delta < 60) {
return Math.floor(delta) + ' 分钟前'
}
delta /= 60
if (delta < 24) {
return Math.floor(delta) + ' 小时前'
}
return moment(time).format('YYYY-MM-DD')
} else {
return time
}
})
Vue.filter('hash', function (magnet) {
return magnet.replace('magnet:?xt=urn:btih:', '')
})
Vue.filter('isNotEmpty', function (obj) {
if (Array.isArray(obj)) {
return obj.length > 0
}
return !!obj
})
Vue.filter('formatURL', function (url) {
if (url && url.startsWith('http')) {
const params = 'from=mw'
const symbol = url.indexOf('?') !== -1 ? '&' : '?'
return url + symbol + params
} else {
return url
}
})
================================================
FILE: src/renderer/plugins/ga.js
================================================
================================================
FILE: src/renderer/plugins/head.js
================================================
import Vue from 'vue'
import VueHead from 'vue-head'
Vue.use(VueHead, {
separator: '-'
})
================================================
FILE: src/renderer/plugins/index.js
================================================
import './config'
import './app'
import './event-proxy'
import './clipboard'
import './head'
import './filter'
import './localstorage'
import './localsetting'
import './element-ui'
import './ga'
import './menu'
================================================
FILE: src/renderer/plugins/localsetting.js
================================================
import Vue from 'vue'
function save (newSetting) {
const localSetting = get()
for (let key in newSetting) {
localSetting[key] = newSetting[key]
}
Vue.localStorage.set('local_setting', JSON.stringify(localSetting))
}
/**
* 获取设置信息
* @param key 如果null返回整个设置对象,否则返回对应属性值
* @returns {*}
*/
function get (key) {
let localSetting
try {
localSetting = JSON.parse(Vue.localStorage.get('local_setting'))
} catch (e) {
}
if (key) {
return localSetting && localSetting.hasOwnProperty(key) ? localSetting[key] : null
}
return localSetting || {}
}
Vue.use({
install: (Vue, options) => {
Vue.prototype.$localSetting = {
saveValue (key, value) {
const obj = {}
obj[key] = value
save(obj)
},
save: save,
get: get
}
}
})
================================================
FILE: src/renderer/plugins/localstorage.js
================================================
import Vue from 'vue'
import VueLocalStorage from 'vue-localstorage'
Vue.use(VueLocalStorage)
================================================
FILE: src/renderer/plugins/menu.js
================================================
import {remote} from 'electron'
const {Menu, MenuItem} = remote
const menu = new Menu()
menu.append(new MenuItem({label: '剪切', role: 'cut'}))
menu.append(new MenuItem({label: '复制', role: 'copy'}))
menu.append(new MenuItem({label: '粘贴', role: 'paste'}))
menu.append(new MenuItem({label: '删除', role: 'delete'}))
menu.append(new MenuItem({label: '全选', role: 'selectAll'}))
const textMenu = new Menu()
textMenu.append(new MenuItem({label: '复制', role: 'copy'}))
textMenu.append(new MenuItem({label: '全选', role: 'selectAll'}))
window.addEventListener('contextmenu', (e) => {
e.preventDefault()
if (e.target.localName
gitextract_nfmp_la9/
├── .babelrc
├── .editorconfig
├── .electron-vue/
│ ├── build.js
│ ├── dev-client.js
│ ├── dev-runner.js
│ ├── webpack.main.config.js
│ ├── webpack.renderer.config.js
│ └── webpack.web.config.js
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── ISSUE_TEMPLATE/
│ └── issue_template.md
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── appveyor.yml
├── build/
│ └── icons/
│ └── icon.icns
├── icns.sh
├── package.json
├── rule.json
├── scripts/
│ ├── build-service.js
│ └── merge-filter-db.js
├── src/
│ ├── index.ejs
│ ├── main/
│ │ ├── api.js
│ │ ├── axios.js
│ │ ├── cache.js
│ │ ├── defaultConfig.js
│ │ ├── electron-cache.js
│ │ ├── filter/
│ │ │ └── filter.js
│ │ ├── format-parser.js
│ │ ├── index.dev.js
│ │ ├── index.js
│ │ ├── ipc.js
│ │ ├── logger.js
│ │ ├── memory-cache.js
│ │ ├── menu.js
│ │ ├── middleware/
│ │ │ ├── block.js
│ │ │ └── response-template.js
│ │ ├── process-config.js
│ │ ├── repository.js
│ │ └── service.js
│ └── renderer/
│ ├── App.vue
│ ├── assets/
│ │ ├── .gitkeep
│ │ ├── fonts/
│ │ │ └── iconfont.css
│ │ └── scss/
│ │ ├── app.scss
│ │ └── element-variables.scss
│ ├── components/
│ │ ├── AsideMenu.vue
│ │ ├── BrowserButton.vue
│ │ ├── BrowserLink.vue
│ │ ├── DetailDialog.vue
│ │ ├── GithubBadge.vue
│ │ ├── GuidePage.vue
│ │ ├── HeaderVersion.vue
│ │ ├── HighlightName.vue
│ │ ├── ItemButtonGroup.vue
│ │ ├── NumberInput.vue
│ │ ├── PagerFooter.vue
│ │ ├── PagerHeader.vue
│ │ ├── PagerItems.vue
│ │ ├── QrcodePopover.vue
│ │ ├── Router.vue
│ │ ├── SearchInput.vue
│ │ ├── SearchPagination.vue
│ │ ├── SearchSort.vue
│ │ ├── ServerConfig.vue
│ │ ├── SettingGroup.vue
│ │ ├── SettingItem.vue
│ │ └── TooltipFormItem.vue
│ ├── main.js
│ ├── pages/
│ │ ├── Index.vue
│ │ ├── Main.vue
│ │ └── Setting.vue
│ ├── plugins/
│ │ ├── app.js
│ │ ├── axios.js
│ │ ├── clipboard.js
│ │ ├── config.js
│ │ ├── element-ui.js
│ │ ├── event-proxy.js
│ │ ├── filter.js
│ │ ├── ga.js
│ │ ├── head.js
│ │ ├── index.js
│ │ ├── localsetting.js
│ │ ├── localstorage.js
│ │ └── menu.js
│ └── router/
│ └── index.js
└── static/
├── .gitkeep
├── keywords.txt
└── robots.txt
SYMBOL INDEX (75 symbols across 21 files)
FILE: .electron-vue/build.js
function clean (line 26) | function clean () {
function build (line 32) | function build () {
function pack (line 73) | function pack (config) {
function web (line 101) | function web () {
function greeting (line 116) | function greeting () {
FILE: .electron-vue/dev-runner.js
function logStats (line 19) | function logStats (proc, data) {
function startRenderer (line 41) | function startRenderer () {
function startMain (line 80) | function startMain () {
function startElectron (line 116) | function startElectron () {
function electronLog (line 143) | function electronLog (data, color) {
function greeting (line 160) | function greeting () {
function init (line 178) | function init () {
FILE: .electron-vue/webpack.renderer.config.js
method templateParameters (line 136) | templateParameters (compilation, assets, options) {
FILE: scripts/build-service.js
function isIgnore (line 68) | function isIgnore (file, ignoreArray) {
function isMinify (line 83) | function isMinify (file) {
FILE: scripts/merge-filter-db.js
function unique (line 33) | function unique (arr) {
FILE: src/main/api.js
function getIPAddress (line 56) | function getIPAddress () {
function reload (line 69) | async function reload (config, preload) {
function start (line 81) | async function start (config, preload) {
function stop (line 102) | function stop (callback) {
function isStarting (line 112) | function isStarting () {
function getServerInfo (line 116) | function getServerInfo () {
FILE: src/main/axios.js
function create (line 9) | function create (appConfig) {
FILE: src/main/cache.js
function set (line 10) | function set (key, value, expired) {
function get (line 25) | function get (key) {
function clear (line 39) | function clear () {
FILE: src/main/electron-cache.js
function put (line 4) | function put (key, value) {
function get (line 8) | function get (key) {
function deleteValue (line 12) | function deleteValue (key) {
function clear (line 16) | function clear () {
FILE: src/main/filter/filter.js
function loadFilterData (line 9) | async function loadFilterData () {
function addWord (line 25) | function addWord (word) {
function isFilter (line 35) | function isFilter (s, cb) {
FILE: src/main/format-parser.js
function extractNumber (line 8) | function extractNumber (str) {
function extractFloat (line 13) | function extractFloat (str) {
function extractSizeText (line 18) | function extractSizeText (str) {
method extractTextByNode (line 30) | extractTextByNode (node) {
method extractResolution (line 46) | extractResolution (name) {
method extractMagnet (line 63) | extractMagnet (url) {
method extractFileSize (line 144) | extractFileSize (sizeText) {
method splitByFileSize (line 162) | splitByFileSize (str) {
FILE: src/main/index.js
function createWindow (line 27) | function createWindow () {
function registerServer (line 76) | async function registerServer () {
FILE: src/main/ipc.js
function registerServer (line 11) | async function registerServer () {
function getLocalConfig (line 27) | function getLocalConfig () {
function registerIPC (line 31) | function registerIPC (mainWindow) {
FILE: src/main/logger.js
function colorizeStart (line 40) | function colorizeStart (style) {
function colorizeEnd (line 44) | function colorizeEnd (style) {
function colorize (line 51) | function colorize (str, style) {
function getStackInfo (line 70) | function getStackInfo () {
FILE: src/main/memory-cache.js
function put (line 3) | function put (key, value) {
function get (line 7) | function get (key) {
function deleteValue (line 11) | function deleteValue (key) {
function clear (line 15) | function clear () {
FILE: src/main/process-config.js
function extractConfigVariable (line 8) | function extractConfigVariable (newConfig) {
function getConfig (line 35) | function getConfig (configVariable) {
FILE: src/main/repository.js
constant URI (line 2) | const URI = require('urijs')
function applyConfig (line 28) | function applyConfig (newConfig) {
function clearCache (line 37) | function clearCache () {
function makeupSearchOption (line 49) | function makeupSearchOption ({id, keyword, page, sort}) {
function getRuleById (line 61) | function getRuleById (id) {
function requestDocument (line 65) | async function requestDocument (url, clientHeaders) {
function obtainDetailResult (line 97) | async function obtainDetailResult ({id, path}, headers) {
function obtainSearchResult (line 120) | async function obtainSearchResult ({id, url}, headers) {
function asyncCacheSearchResult (line 160) | function asyncCacheSearchResult ({id, keyword, page, sort}, headers) {
function parseItemsDocument (line 187) | function parseItemsDocument (document, expression) {
function parseDetailDocument (line 223) | function parseDetailDocument (document, expression) {
function loadRuleByURL (line 251) | async function loadRuleByURL () {
function getRule (line 278) | async function getRule () {
FILE: src/main/service.js
function startServer (line 4) | async function startServer () {
FILE: src/renderer/plugins/axios.js
function create (line 5) | function create () {
FILE: src/renderer/plugins/filter.js
function highlightWord (line 4) | function highlightWord (keyword, content) {
FILE: src/renderer/plugins/localsetting.js
function save (line 3) | function save (newSetting) {
function get (line 16) | function get (key) {
method saveValue (line 32) | saveValue (key, value) {
Condensed preview — 89 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,515K chars).
[
{
"path": ".babelrc",
"chars": 668,
"preview": "{\n \"comments\": false,\n \"env\": {\n \"main\": {\n \"presets\": [\n [\n \"env\",\n {\n \"t"
},
{
"path": ".editorconfig",
"chars": 147,
"preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
},
{
"path": ".electron-vue/build.js",
"chars": 3058,
"preview": "'use strict'\n\nprocess.env.NODE_ENV = 'production'\n\nconst { say } = require('cfonts')\nconst chalk = require('chalk')\ncons"
},
{
"path": ".electron-vue/dev-client.js",
"chars": 1205,
"preview": "const hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true')\n\nhotClient.subscribe(event => {\n /**"
},
{
"path": ".electron-vue/dev-runner.js",
"chars": 4606,
"preview": "'use strict'\n\nconst chalk = require('chalk')\nconst electron = require('electron')\nconst path = require('path')\nconst { s"
},
{
"path": ".electron-vue/webpack.main.config.js",
"chars": 1746,
"preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'main'\n\nconst path = require('path')\nconst { dependencies } = require('../package."
},
{
"path": ".electron-vue/webpack.renderer.config.js",
"chars": 5629,
"preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'renderer'\n\nconst path = require('path')\nconst {dependencies} = require('../packag"
},
{
"path": ".electron-vue/webpack.web.config.js",
"chars": 3674,
"preview": "'use strict'\n\nprocess.env.BABEL_ENV = 'web'\n\nconst path = require('path')\nconst webpack = require('webpack')\n\nconst Mini"
},
{
"path": ".eslintignore",
"chars": 0,
"preview": ""
},
{
"path": ".eslintrc.js",
"chars": 1030,
"preview": "// https://eslint.org/docs/user-guide/configuring\n\nmodule.exports = {\n root: true,\n parserOptions: {\n 'parser': 'ba"
},
{
"path": ".github/ISSUE_TEMPLATE/issue_template.md",
"chars": 157,
"preview": "---\nname: issue_template\nabout: 提交issue前请先确认文档是否已有答案\ntitle: ''\nlabels: ''\nassignees: ''\n\n---\n\n>操作系统版本: \n>应用版本: \n\n#### "
},
{
"path": ".gitignore",
"chars": 241,
"preview": ".DS_Store\ndist/electron/*\ndist/web/*\nbuild/*\n!build/icons\nnode_modules/\nnpm-debug.log\nnpm-debug.log.*\nthumbs.db\n!.gitkee"
},
{
"path": ".travis.yml",
"chars": 930,
"preview": "osx_image: xcode8.3\nsudo: required\ndist: trusty\nlanguage: c\nmatrix:\n include:\n - os: osx\n - os: linux\n env: CC=cla"
},
{
"path": "LICENSE",
"chars": 35149,
"preview": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n\n Copyright (C) 2007 Free "
},
{
"path": "README.md",
"chars": 1499,
"preview": "# [已失效,不再维护]\n\n[](https://github.com/xiandanin/"
},
{
"path": "appveyor.yml",
"chars": 415,
"preview": "version: 0.1.{build}\n\nbranches:\n only:\n - master\n\nimage: Visual Studio 2017\nplatform:\n - x64\n\ncache:\n - node_modul"
},
{
"path": "icns.sh",
"chars": 822,
"preview": "#!/usr/bin/env bash\n# brew install icoutils\nfilepath=256x256.png\niconset=.icns.iconset\n\ncd build/icons\nif [ ! -d $iconse"
},
{
"path": "package.json",
"chars": 5102,
"preview": "{\n \"name\": \"magnetw\",\n \"version\": \"3.1.1\",\n \"description\": \"磁力链接聚合搜索\",\n \"license\": \"GNU General Public License v3.0\""
},
{
"path": "rule.json",
"chars": 13083,
"preview": "[\n {\n \"id\": \"zhongzisou\",\n \"name\": \"种子搜\",\n \"proxy\": false,\n \"url\": \"https://zhongzidi1.com\",\n \"paths\": {"
},
{
"path": "scripts/build-service.js",
"chars": 2221,
"preview": "/* 编译成node服务 */\n\nconst path = require('path')\nconst fs = require('fs-extra')\nconst Terser = require('terser')\n// 需要忽略的文件"
},
{
"path": "scripts/merge-filter-db.js",
"chars": 1814,
"preview": "// 合并过滤词库\n// https://github.com/fighting41love/funNLP/tree/master/data/%E6%95%8F%E6%84%9F%E8%AF%8D%E5%BA%93\n// https://r"
},
{
"path": "src/index.ejs",
"chars": 1009,
"preview": "<!DOCTYPE html>\n<html>\n <head>\n <meta charset=\"utf-8\">\n <title><%= htmlWebpackPlugin.options.title %></title>\n "
},
{
"path": "src/main/api.js",
"chars": 3028,
"preview": "const Koa = require('koa')\nconst Router = require('koa-router')\nconst app = new Koa()\nconst prefix = '/api'\nconst router"
},
{
"path": "src/main/axios.js",
"chars": 1641,
"preview": "const axios = require('axios')\nconst tunnel = require('tunnel')\nconst SocksProxyAgent = require('socks-proxy-agent')\n\n/*"
},
{
"path": "src/main/cache.js",
"chars": 918,
"preview": "const moment = require('moment')\nconst store = process.env.BUILD_TARGET === 'electron' ? require('./electron-cache') : r"
},
{
"path": "src/main/defaultConfig.js",
"chars": 824,
"preview": "module.exports = function () {\n return {\n checkUpdateURL: 'https://magnetw.app/update.json',\n // 云解析URL\n cloud"
},
{
"path": "src/main/electron-cache.js",
"chars": 323,
"preview": "const Store = require('electron-store')\nconst store = new Store()\n\nfunction put (key, value) {\n store.set(key, value)\n}"
},
{
"path": "src/main/filter/filter.js",
"chars": 1687,
"preview": "const path = require('path')\nconst fs = require('fs')\n\nconst map = {}\nlet load = false\n\nconst isDev = process.env.NODE_E"
},
{
"path": "src/main/format-parser.js",
"chars": 4578,
"preview": "const moment = require('moment')\n\nconst sizeUnit = ['B|bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']\nconst siz"
},
{
"path": "src/main/index.dev.js",
"chars": 715,
"preview": "/**\n * This file is used specifically and only for development. It installs\n * `electron-debug` & `vue-devtools`. There "
},
{
"path": "src/main/index.js",
"chars": 2871,
"preview": "'use strict'\n\nimport {app, BrowserWindow, session} from 'electron'\n\nconst registerMenu = require('./menu')\nconst {appNam"
},
{
"path": "src/main/ipc.js",
"chars": 4211,
"preview": "const moment = require('moment')\nconst logger = require('./logger')\nconst path = require('path')\nconst {ipcMain, app} = "
},
{
"path": "src/main/logger.js",
"chars": 2736,
"preview": "const logger = require('electron-log')\nconst moment = require('moment')\nconst util = require('util')\nconst path = requir"
},
{
"path": "src/main/memory-cache.js",
"chars": 292,
"preview": "const store = require('memory-cache')\n\nfunction put (key, value) {\n store.put(key, value)\n}\n\nfunction get (key) {\n ret"
},
{
"path": "src/main/menu.js",
"chars": 2003,
"preview": "const {Menu, app, session} = require('electron')\nconst is = require('electron-is')\n\nmodule.exports = function (mainWindo"
},
{
"path": "src/main/middleware/block.js",
"chars": 836,
"preview": "const blacklistRegx = ['googlebot', 'mediapartners-google', 'adsbot-google', 'baiduspider', '360spider', 'haosouspider',"
},
{
"path": "src/main/middleware/response-template.js",
"chars": 665,
"preview": "module.exports = async (ctx, next) => {\n try {\n ctx.success = function (data) {\n ctx.body = {\n success: "
},
{
"path": "src/main/process-config.js",
"chars": 1232,
"preview": "const defaultConfig = require('./defaultConfig')\n\n/**\n * 从修改后的配置对象中提取修改的变量\n * @param newConfig\n * @returns {null}\n */\nfu"
},
{
"path": "src/main/repository.js",
"chars": 7914,
"preview": "const format = require('./format-parser')\nconst URI = require('urijs')\nconst fs = require('fs')\nconst createAxios = requ"
},
{
"path": "src/main/service.js",
"chars": 795,
"preview": "const processConfig = require('./process-config')\nconst {start} = require('./api')\n\nasync function startServer () {\n le"
},
{
"path": "src/renderer/App.vue",
"chars": 538,
"preview": "<template>\n <div id=\"app\">\n <router-view></router-view>\n </div>\n</template>\n\n<script>\n\n export default {\n metho"
},
{
"path": "src/renderer/assets/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "src/renderer/assets/fonts/iconfont.css",
"chars": 2438,
"preview": "@font-face {font-family: \"iconfont\";\n src: url('iconfont.eot?t=1574010210992'); /* IE9 */\n src: url('iconfont.eot?t=15"
},
{
"path": "src/renderer/assets/scss/app.scss",
"chars": 706,
"preview": "@import \"./element-variables\";\n@import \"~element-ui/packages/theme-chalk/src/common/var\";\n\n$font-size: 14px;\n$color-bord"
},
{
"path": "src/renderer/assets/scss/element-variables.scss",
"chars": 126,
"preview": "/* 改变主题色变量 */\n$--color-primary: #6078ea !default;\n\n/* 改变 icon 字体路径变量,必需 */\n$--font-path: '~element-ui/lib/theme-chalk/fo"
},
{
"path": "src/renderer/components/AsideMenu.vue",
"chars": 6401,
"preview": "<template>\n <el-scrollbar>\n <div class=\"aside-menu\">\n <div class=\"menu-setting\">\n <div class=\"menu-setti"
},
{
"path": "src/renderer/components/BrowserButton.vue",
"chars": 524,
"preview": "<template>\n <el-button :size=\"size\" :icon='icon' :type=\"type\" :plain='plain' @click=\"handleClickButton\">\n <slot></sl"
},
{
"path": "src/renderer/components/BrowserLink.vue",
"chars": 1187,
"preview": "<template>\n <el-link :type=\"type\"\n :target=\"target||'_blank'\"\n :underline=\"underline||false\"\n "
},
{
"path": "src/renderer/components/DetailDialog.vue",
"chars": 3949,
"preview": "<template>\n <el-dialog :visible.sync=\"dialog.show\"\n width=\"80%\">\n <div slot=\"title\">{{dialog.name}}</div"
},
{
"path": "src/renderer/components/GithubBadge.vue",
"chars": 213,
"preview": "<template>\n <i class=\"github-badge\">\n </i>\n</template>\n\n<script>\n export default {}\n</script>\n\n<style scoped>\n .gith"
},
{
"path": "src/renderer/components/GuidePage.vue",
"chars": 3158,
"preview": "<template>\n <div class=\"guide-page\">\n <el-alert\n class=\"guide-page-alert\"\n v-show=\"title\"\n :type=\"typ"
},
{
"path": "src/renderer/components/HeaderVersion.vue",
"chars": 1450,
"preview": "<template>\n <browser-link target=\"_self\">\n <div class=\"header-version align-items-center\">\n <img src=\"../assets"
},
{
"path": "src/renderer/components/HighlightName.vue",
"chars": 1064,
"preview": "<template>\n <browser-link :href=\"url\" type=\"primary\">\n <el-tag size=\"mini\" v-show=\"resolution\"\n disable-t"
},
{
"path": "src/renderer/components/ItemButtonGroup.vue",
"chars": 2838,
"preview": "<template>\n <div>\n <qrcode-popover :text=\"item.magnet\" :title=\"item.name\" v-if=\"show('qrcode')\">\n <browser-butt"
},
{
"path": "src/renderer/components/NumberInput.vue",
"chars": 459,
"preview": "<template>\n <el-input class=\"number-input\" v-model=\"value\" size=\"mini\" :placeholder=\"placeholder\">\n <span slot=\"appe"
},
{
"path": "src/renderer/components/PagerFooter.vue",
"chars": 624,
"preview": "<template>\n <div class=\"pager-footer\">\n <span class=\"header-disclaimer-text\">{{$config.footerText}}</span>\n </div>\n"
},
{
"path": "src/renderer/components/PagerHeader.vue",
"chars": 2587,
"preview": "<template>\n <div class=\"pager-header\">\n <div class=\"header-left\" @dblclick=\"dblclick\">\n <header-version></heade"
},
{
"path": "src/renderer/components/PagerItems.vue",
"chars": 2804,
"preview": "<template>\n <el-table\n border\n default-expand-all=\"\"\n :data=\"items\"\n :empty-text=\"emptyMessage||'什么也没搜到'\"\n "
},
{
"path": "src/renderer/components/QrcodePopover.vue",
"chars": 730,
"preview": "<template>\n <el-popover\n placement=\"top\"\n :title=\"title\"\n width=\"150\"\n :close-delay=\"100\"\n popper-class="
},
{
"path": "src/renderer/components/Router.vue",
"chars": 544,
"preview": "<template>\n <router-link :to=\"router\" :tag=\"tag||'span'\">\n <slot></slot>\n </router-link>\n</template>\n\n<script>\nexpo"
},
{
"path": "src/renderer/components/SearchInput.vue",
"chars": 961,
"preview": "<template>\n <div class=\"search-input\">\n <el-input :placeholder=\"placeholder\"\n @keyup.enter.native=\"emit"
},
{
"path": "src/renderer/components/SearchPagination.vue",
"chars": 663,
"preview": "<template>\n <!--页码-->\n <el-pagination\n @current-change=\"emitPageChanged\"\n :current-page=\"page\"\n background\n "
},
{
"path": "src/renderer/components/SearchSort.vue",
"chars": 3508,
"preview": "<template>\n <div class=\"search-sort\">\n <!--源站按钮-->\n <browser-link v-show=\"config.showSourceLink\" :button=\"true\" s"
},
{
"path": "src/renderer/components/ServerConfig.vue",
"chars": 8502,
"preview": "<template>\n <div class=\"config\">\n <el-form ref=\"settingForm\" label-width=\"130px\" label-position=\"left\" :model='confi"
},
{
"path": "src/renderer/components/SettingGroup.vue",
"chars": 339,
"preview": "<template>\n <div>\n <h3 class=\"setting-title\">{{title}}</h3>\n <el-divider></el-divider>\n <slot></slot>\n </div>"
},
{
"path": "src/renderer/components/SettingItem.vue",
"chars": 616,
"preview": "<template>\n <el-form-item>\n <div slot=\"label\" class=\"setting-item-label\">\n <span>{{label}}</span>\n <el-too"
},
{
"path": "src/renderer/components/TooltipFormItem.vue",
"chars": 525,
"preview": "<template>\n <el-form-item :prop=\"prop\">\n <div slot=\"label\" class=\"setting-item-label\">\n <span>{{label}}</span>\n"
},
{
"path": "src/renderer/main.js",
"chars": 463,
"preview": "import Vue from 'vue'\nimport create from '@/plugins/axios'\n\nimport App from './App'\nimport router from './router'\nimport"
},
{
"path": "src/renderer/pages/Index.vue",
"chars": 7330,
"preview": "<template>\n <el-container>\n <el-aside ref=\"indexAside\" width=\"200px\">\n <aside-menu\n :active=\"page.curren"
},
{
"path": "src/renderer/pages/Main.vue",
"chars": 2833,
"preview": "<template>\n <el-container>\n <div class=\"header-placeholder drag\" @dblclick=\"handleClickMaxWindow\"></div>\n <el-hea"
},
{
"path": "src/renderer/pages/Setting.vue",
"chars": 2746,
"preview": "<template>\n <el-scrollbar>\n <div class=\"setting\">\n <server-config v-if=\"config\"\n ref=\"setti"
},
{
"path": "src/renderer/plugins/app.js",
"chars": 328,
"preview": "import Vue from 'vue'\nimport json from '../../../package.json'\n\nVue.use({\n install: (Vue, options) => {\n Vue.prototy"
},
{
"path": "src/renderer/plugins/axios.js",
"chars": 1007,
"preview": "import {ipcRenderer, remote} from 'electron'\nimport axios from 'axios'\nimport URI from 'urijs'\n\nfunction create () {\n c"
},
{
"path": "src/renderer/plugins/clipboard.js",
"chars": 87,
"preview": "import Vue from 'vue'\nimport VueClipboard from 'vue-clipboard2'\n\nVue.use(VueClipboard)\n"
},
{
"path": "src/renderer/plugins/config.js",
"chars": 512,
"preview": "import Vue from 'vue'\n\nVue.use({\n install: (Vue, options) => {\n const baseURL = 'https://magnetw.app'\n Vue."
},
{
"path": "src/renderer/plugins/element-ui.js",
"chars": 167,
"preview": "import Vue from 'vue'\nimport Element from 'element-ui'\nimport 'element-ui/packages/theme-chalk/src/index.scss'\nimport '."
},
{
"path": "src/renderer/plugins/event-proxy.js",
"chars": 84,
"preview": "import Vue from 'vue'\nimport EventProxy from 'vue-event-proxy'\n\nVue.use(EventProxy)\n"
},
{
"path": "src/renderer/plugins/filter.js",
"chars": 2355,
"preview": "import Vue from 'vue'\nimport moment from 'moment'\n\nfunction highlightWord (keyword, content) {\n const idx = content.toL"
},
{
"path": "src/renderer/plugins/ga.js",
"chars": 0,
"preview": ""
},
{
"path": "src/renderer/plugins/head.js",
"chars": 93,
"preview": "import Vue from 'vue'\nimport VueHead from 'vue-head'\n\nVue.use(VueHead, {\n separator: '-'\n})\n"
},
{
"path": "src/renderer/plugins/index.js",
"chars": 211,
"preview": "import './config'\nimport './app'\nimport './event-proxy'\nimport './clipboard'\nimport './head'\nimport './filter'\nimport '."
},
{
"path": "src/renderer/plugins/localsetting.js",
"chars": 805,
"preview": "import Vue from 'vue'\n\nfunction save (newSetting) {\n const localSetting = get()\n for (let key in newSetting) {\n loc"
},
{
"path": "src/renderer/plugins/localstorage.js",
"chars": 95,
"preview": "import Vue from 'vue'\nimport VueLocalStorage from 'vue-localstorage'\n\nVue.use(VueLocalStorage)\n"
},
{
"path": "src/renderer/plugins/menu.js",
"chars": 798,
"preview": "import {remote} from 'electron'\n\nconst {Menu, MenuItem} = remote\n\nconst menu = new Menu()\nmenu.append(new MenuItem({labe"
},
{
"path": "src/renderer/router/index.js",
"chars": 359,
"preview": "import Vue from 'vue'\nimport Router from 'vue-router'\nimport Main from '@/pages/Main'\n\nconst routerPush = Router.prototy"
},
{
"path": "static/.gitkeep",
"chars": 0,
"preview": ""
},
{
"path": "static/keywords.txt",
"chars": 1310836,
"preview": "77u/MTU3NjgKMzIyODMKNDA2OTgKNzE3NzYKOTg1OTg1CuOAgmdtCkBzc2hvbGUK4oiqUgriiKpS6Z2gCuKUu+KUvArimK0KMDJqYW0KMTAwMHkKMTPpu54K"
},
{
"path": "static/robots.txt",
"chars": 26,
"preview": "User-agent: *\nDisallow: /\n"
}
]
// ... and 1 more files (download for full content)
About this extraction
This page contains the full source code of the xiandanin/magnetW GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 89 files (1.4 MB), approximately 929.6k tokens, and a symbol index with 75 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.