init
commit
bf7b6b7993
|
@ -0,0 +1,17 @@
|
|||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
.idea
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
|
||||
|
||||
build
|
||||
build/**
|
||||
|
||||
node_modules
|
||||
node_modules/**
|
||||
|
||||
package-lock.json
|
|
@ -0,0 +1,53 @@
|
|||
/* app */
|
||||
const { app, session, Menu, ipcMain } = require('electron')
|
||||
const { join } = require('path')
|
||||
const fs = require('fs')
|
||||
|
||||
// const createTray = require('./tools/tray')
|
||||
const { createMainWindow } = require('./tools/windows')
|
||||
|
||||
const ROOT = __dirname
|
||||
const APP_DIR = join(app.getPath('appData'), './clash')
|
||||
const CONFIG_FILE = join(APP_DIR, './config.json')
|
||||
|
||||
/* ----------------------------------------------------- */
|
||||
|
||||
app.commandLine.appendSwitch('lang', 'zh-CN')
|
||||
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required')
|
||||
|
||||
Menu.setApplicationMenu(null)
|
||||
|
||||
/* ----------------------------------------------------- */
|
||||
|
||||
// 初始化应用
|
||||
app.once('ready', () => {
|
||||
let win = createMainWindow(join(ROOT, './images/app.png'))
|
||||
|
||||
// app.toggleTray = createTray(win)
|
||||
})
|
||||
|
||||
ipcMain.on('app', (ev, conn) => {
|
||||
switch (conn.type) {
|
||||
case 'saveToken':
|
||||
fs.writeFile(CONFIG_FILE, conn.data, function (err) {})
|
||||
ev.returnValue = true
|
||||
break
|
||||
|
||||
case 'readToken':
|
||||
{
|
||||
let cache = ''
|
||||
try {
|
||||
cache = fs.readFileSync(CONFIG_FILE).toString()
|
||||
} catch (err) {}
|
||||
ev.returnValue = cache
|
||||
}
|
||||
break
|
||||
|
||||
case 'toggleTray':
|
||||
{
|
||||
app.toggleTray(conn.data)
|
||||
ev.returnValue = true
|
||||
}
|
||||
break
|
||||
}
|
||||
})
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"name": "clash-manager",
|
||||
"version": "1.0.0",
|
||||
"description": "clash管理面板",
|
||||
"main": "src/main.js",
|
||||
"scripts": {
|
||||
"start": "electron ."
|
||||
},
|
||||
"author": {
|
||||
"name": "yutent",
|
||||
"email": "yutent.io@gmail.com"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"dependencies": {}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
html{width:840px;height:540px}body{width:100%;height:100%;font-size:14px;color:var(--color-dark-1)}.app{display:flex;width:100%;height:100%;background:#f7f8fb}.holder{flex:1}.aside{flex-shrink:0;display:flex;flex-direction:column;width:128px;box-shadow:4px 0 12px rgba(0,0,0,.03)}.aside .logo{width:64px;height:64px;margin:16px auto;background:url(../icons/128x128.png) no-repeat;background-size:100%}.aside .nav-list{display:flex;flex-direction:column;width:100%;padding:0 16px}.aside .nav-list .item{width:100%;height:32px;margin-top:16px;line-height:32px;border-radius:16px;text-align:center;cursor:pointer;transition:color .2s linear,background .2s linear}.aside .nav-list .item:hover{color:var(--color-blue-1)}.aside .nav-list .item.active{color:#fff;background:var(--color-blue-1)}.aside .version{line-height:36px;text-align:center}.tab-content{flex:1;flex-shrink:0;display:flex;flex-direction:column;height:540px;padding:16px}.tab-content .field{display:flex;align-items:center;width:100%;height:36px;margin-top:12px}.tab-content .field .label{width:128px;font-weight:bold;color:var(--color-grey-3)}.tab-content .field .full{flex:1}.tab-content.configs{width:49%;margin-left:1%}.tab-content.remote wc-button,.tab-content.rules wc-button{width:64px;min-width:64px;margin-left:16px}.tab-content.remote .scroll,.tab-content.rules .scroll{overflow:hidden;flex:1;margin-top:24px}.tab-content.remote .card,.tab-content.rules .card{margin:0 auto}.tab-content.remote .list,.tab-content.rules .list{word-break:break-all}.tab-content.remote .list .item,.tab-content.rules .list .item{display:flex;align-items:center;justify-content:space-between;height:36px;padding:0 16px;border-bottom:1px solid var(--color-plain-1);font-family:Menlo,"Courier New",Courier,monospace;transition:background .2s linear}.tab-content.remote .list .item:hover,.tab-content.rules .list .item:hover{background-color:var(--color-plain-1)}.tab-content.remote .list span.type,.tab-content.rules .list span.type{padding:2px 3px;border-radius:2px;background-color:var(--color-blue-1);color:#fff}.card{flex:1;flex-shrink:0;display:flex;flex-direction:column;flex-wrap:wrap;width:100%;max-height:100%;padding:12px;margin:0 auto 24px;border:0;border-radius:4px;background-color:#fff;box-shadow:0 0 8px rgba(0,0,0,.075)}.card legend{-webkit-touch-callout:none;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;color:var(--color-blue-1);font-weight:bold}
|
|
@ -0,0 +1,170 @@
|
|||
html {
|
||||
width: 840px;
|
||||
height: 540px;
|
||||
}
|
||||
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-size: 14px;
|
||||
color: var(--color-dark-1);
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #f7f8fb;
|
||||
}
|
||||
|
||||
.holder {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.aside {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 128px;
|
||||
box-shadow: 4px 0 12px rgba(0, 0, 0, 0.03);
|
||||
|
||||
.logo {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
margin: 16px auto;
|
||||
background: url(../icons/128x128.png) no-repeat;
|
||||
background-size: 100%;
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: 0 16px;
|
||||
|
||||
.item {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
margin-top: 16px;
|
||||
line-height: 32px;
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s linear, background 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
color: var(--color-blue-1);
|
||||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
background: var(--color-blue-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.version {
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 540px;
|
||||
padding: 16px;
|
||||
|
||||
.field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 36px;
|
||||
margin-top: 12px;
|
||||
|
||||
.label {
|
||||
width: 128px;
|
||||
font-weight: bold;
|
||||
color: var(--color-grey-3);
|
||||
}
|
||||
|
||||
.full {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.configs {
|
||||
width: 49%;
|
||||
margin-left: 1%;
|
||||
}
|
||||
|
||||
&.remote,
|
||||
&.rules {
|
||||
wc-button {
|
||||
width: 64px;
|
||||
min-width: 64px;
|
||||
margin-left: 16px;
|
||||
}
|
||||
|
||||
.scroll {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.list {
|
||||
word-break: break-all;
|
||||
|
||||
.item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 36px;
|
||||
padding: 0 16px;
|
||||
border-bottom: 1px solid var(--color-plain-1);
|
||||
font-family: Menlo, 'Courier New', Courier, monospace;
|
||||
transition: background 0.2s linear;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-plain-1);
|
||||
}
|
||||
}
|
||||
|
||||
span.type {
|
||||
padding: 2px 3px;
|
||||
border-radius: 2px;
|
||||
background-color: var(--color-blue-1);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
flex: 1;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
padding: 12px;
|
||||
margin: 0 auto 24px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
background-color: #fff;
|
||||
box-shadow: 0 0 8px rgba(0, 0, 0, 0.075);
|
||||
|
||||
legend {
|
||||
-webkit-touch-callout: none;
|
||||
user-select: none;
|
||||
color: var(--color-blue-1);
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
|
@ -0,0 +1,126 @@
|
|||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no">
|
||||
<title>Clash Web Panel</title>
|
||||
<link rel="favicon" href="favicon.ico">
|
||||
<link rel="icon" type="image/x-icon" href="icons/192x192.png">
|
||||
<link href="//unpkg.yutent.top/@bytedo/wcui/dist/css/reset-basic.css" rel="stylesheet">
|
||||
<link href="css/app.css" rel="stylesheet">
|
||||
<script src="js/app.js" type="module"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app noselect" anot="app">
|
||||
<aside class="aside">
|
||||
<a class="logo"></a>
|
||||
|
||||
<nav class="nav-list">
|
||||
<a
|
||||
class="item"
|
||||
:for="i it in navs"
|
||||
:class="{active: i === tab}"
|
||||
@click="changeTab(i)"
|
||||
:text="it">
|
||||
</a>
|
||||
</nav>
|
||||
|
||||
<span class="holder"></span>
|
||||
<span class="version">{{version}}</span>
|
||||
</aside>
|
||||
|
||||
<main class="tab-content" :if="tab === 0"></main>
|
||||
|
||||
<main class="tab-content rules" :if="tab === 1">
|
||||
<fieldset class="card">
|
||||
<legend>规则列表</legend>
|
||||
|
||||
<wc-scroll class="scroll">
|
||||
<ul class="list">
|
||||
<li class="item" :for="i it in rules">
|
||||
<span>{{it.type}}</span>
|
||||
<span>{{it.payload}}</span>
|
||||
<span class="type">{{it.proxy}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</wc-scroll>
|
||||
</fieldset>
|
||||
</main>
|
||||
|
||||
<main class="tab-content" :if="tab === 2"></main>
|
||||
|
||||
<main class="tab-content remote" :if="tab === 3">
|
||||
<fieldset class="card">
|
||||
<legend>订阅配置</legend>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">订阅地址</span>
|
||||
<wc-input class="full" :duplex="remote.link"></wc-input>
|
||||
<wc-button type="info" :disabled="!remote.link" @click="updateRemote">更新</wc-button>
|
||||
</section>
|
||||
|
||||
<wc-scroll class="scroll">
|
||||
<ul class="list">
|
||||
<li class="item" :for="i it in remote.list">
|
||||
{{i + 1}}.
|
||||
<span>{{it[0]}}</span>
|
||||
<span class="type">{{it[1]}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</wc-scroll>
|
||||
</fieldset>
|
||||
</main>
|
||||
|
||||
<main class="tab-content configs" :if="tab === 4">
|
||||
<fieldset class="card">
|
||||
<legend>系统设置</legend>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">开机启动Clash</span>
|
||||
<wc-switch disabled></wc-switch>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">设置为系统代理</span>
|
||||
<wc-switch disabled></wc-switch>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">允许局域网的连接</span>
|
||||
<wc-switch :duplex="configs.allowLan"></wc-switch>
|
||||
</section>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="card">
|
||||
<legend>Clash设置</legend>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">代理模式</span>
|
||||
<wc-radio-group :duplex="configs.proxy">
|
||||
<wc-radio value="global">全局</wc-radio>
|
||||
<wc-radio value="rule">规则</wc-radio>
|
||||
<wc-radio value="direct">直连</wc-radio>
|
||||
</wc-radio-group>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">Socks5 代理端口</span>
|
||||
<wc-input size="small" :duplex="configs.socks5"></wc-input>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">HTTP 代理端口</span>
|
||||
<wc-input size="small" :duplex="configs.http"></wc-input>
|
||||
</section>
|
||||
|
||||
<section class="field">
|
||||
<span class="label">混合代理端口</span>
|
||||
<wc-input size="small" :duplex="configs.mixed"></wc-input>
|
||||
</section>
|
||||
|
||||
</fieldset>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,172 @@
|
|||
/**
|
||||
*
|
||||
* @author yutent<yutent.io@gmail.com>
|
||||
* @date 2022/05/16 00:38:48
|
||||
*/
|
||||
|
||||
import Anot from '//unpkg.yutent.top/anot/dist/anot.js'
|
||||
import fetch from '//unpkg.yutent.top/@bytedo/fetch/dist/index.js'
|
||||
import '//unpkg.yutent.top/@bytedo/wcui/dist/layer/index.js'
|
||||
import '//unpkg.yutent.top/@bytedo/wcui/dist/form/switch.js'
|
||||
import '//unpkg.yutent.top/@bytedo/wcui/dist/form/radio.js'
|
||||
import '//unpkg.yutent.top/@bytedo/wcui/dist/form/button.js'
|
||||
|
||||
fetch.inject.response(r => r.json())
|
||||
|
||||
Anot({
|
||||
$id: 'app',
|
||||
|
||||
state: {
|
||||
version: '1.10.0',
|
||||
navs: ['代理', '规则', '连接', '订阅', '设置'],
|
||||
tab: +Anot.ls('tab') || 0,
|
||||
contrl: {
|
||||
host: '//127.0.0.1:',
|
||||
port: 6767,
|
||||
key: ''
|
||||
},
|
||||
rules: [],
|
||||
remote: {
|
||||
link: Anot.ls('remote_link') || '',
|
||||
list: []
|
||||
},
|
||||
configs: {
|
||||
proxy: 'rule',
|
||||
http: 0,
|
||||
socks5: 0,
|
||||
mixed: 7890,
|
||||
allowLan: false
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.contrl.port) {
|
||||
fetch.BASE_URL = this.contrl.host + this.contrl.port
|
||||
} else {
|
||||
return layer
|
||||
.prompt('请输入Clash本地管理端口', (v, done) => {
|
||||
let n = +v.trim()
|
||||
if (n === n) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
.then(v => {
|
||||
Anot.ls('web_port', v)
|
||||
location.reload()
|
||||
})
|
||||
.catch(e => {
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
this.getVersion()
|
||||
this.getConfig()
|
||||
this.getRule()
|
||||
},
|
||||
|
||||
methods: {
|
||||
changeTab(idx) {
|
||||
this.tab = idx
|
||||
Anot.ls('tab', idx)
|
||||
},
|
||||
getVersion() {
|
||||
fetch('/version').then(r => {
|
||||
this.version = r.version
|
||||
})
|
||||
},
|
||||
|
||||
getConfig() {
|
||||
fetch('/configs').then(r => {
|
||||
console.log(r)
|
||||
Object.assign(this.configs, {
|
||||
proxy: r.mode,
|
||||
http: r.port,
|
||||
socks5: r['socks-port'],
|
||||
mixed: r['mixed-port'],
|
||||
allowLan: r['allow-lan']
|
||||
})
|
||||
// this.version = r.version
|
||||
})
|
||||
},
|
||||
|
||||
getRule() {
|
||||
fetch('/rules').then(r => {
|
||||
console.log(r)
|
||||
this.rules = r.rules
|
||||
})
|
||||
},
|
||||
|
||||
updateRemote() {
|
||||
if (this.remote.link) {
|
||||
if (!/^(https?:)?\/\//.test(this.remote.link)) {
|
||||
return layer.toast('订阅地址格式不正确', 'error')
|
||||
}
|
||||
Anot.ls('remote_link', this.remote.link)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
let txt = ''
|
||||
let names = ['DIRECT']
|
||||
let r = (Anot.ss('temp') || '').trim()
|
||||
|
||||
this.remote.list = r.split('\n').map(it => {
|
||||
let tmp = decodeURIComponent(it).split('#')
|
||||
let tmp2 = new URL(tmp[0])
|
||||
let protocol = tmp2.protocol.slice(0, -1)
|
||||
let path = tmp2.pathname.slice(2)
|
||||
let info = { name: tmp.pop(), type: protocol, udp: true }
|
||||
|
||||
path = path.split(':')
|
||||
|
||||
info.port = path.pop()
|
||||
|
||||
path = path[0].split('@')
|
||||
info.server = path.pop()
|
||||
|
||||
switch (protocol) {
|
||||
case 'ss':
|
||||
path = atob(path[0]).split(':')
|
||||
console.log(path)
|
||||
Object.assign(info, {
|
||||
cipher: path[0],
|
||||
password: path[1]
|
||||
})
|
||||
break
|
||||
|
||||
case 'trojan':
|
||||
Object.assign(info, {
|
||||
password: path[0],
|
||||
sni: tmp2.searchParams.get('sni'),
|
||||
'skip-cert-verify': true
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
names.push(info.name)
|
||||
txt += ` - { name: ${info.name}, type: ${info.type}, server: ${info.server}, port: ${
|
||||
info.port
|
||||
}, password: ${info.password}, udp: true, ${info.type === 'ss' ? 'cipher' : 'sni'}: ${
|
||||
info.type === 'ss' ? info.cipher : info.sni
|
||||
}}\n`
|
||||
return [info.name, info.type]
|
||||
})
|
||||
|
||||
// window.foo = txt
|
||||
// window.bar = names.join(', ')
|
||||
// // console.log(txt)
|
||||
|
||||
return
|
||||
window
|
||||
.fetch(this.remote.link, { cors: true })
|
||||
.then(r => r.text())
|
||||
.then(r => {
|
||||
r = atob(r).trim()
|
||||
Anot.ss('temp', r)
|
||||
layer.toast('订阅更新成功', 'success')
|
||||
this.remote.list = r.split('\n').map(it => {
|
||||
let tmp = decodeURIComponent(it).split('#')
|
||||
return [tmp.pop(), tmp[0]?.split('://').shift()]
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
Loading…
Reference in New Issue