commit bf7b6b79932621d582625ec4a87ad6b9470c825d Author: yutent Date: Wed Sep 28 15:03:46 2022 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00a9fb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.DS_Store +.AppleDouble +.LSOverride +.idea +._* +.Spotlight-V100 +.Trashes + + + +build +build/** + +node_modules +node_modules/** + +package-lock.json \ No newline at end of file diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..eae1f5a --- /dev/null +++ b/Readme.md @@ -0,0 +1,2 @@ +# Clash Web Panel +> 完整功能版。 \ No newline at end of file diff --git a/electron/main.js b/electron/main.js new file mode 100644 index 0000000..5c13b51 --- /dev/null +++ b/electron/main.js @@ -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 + } +}) diff --git a/package.json b/package.json new file mode 100644 index 0000000..62680ed --- /dev/null +++ b/package.json @@ -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": {} +} diff --git a/src/css/app.css b/src/css/app.css new file mode 100644 index 0000000..c1a1754 --- /dev/null +++ b/src/css/app.css @@ -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} \ No newline at end of file diff --git a/src/css/app.scss b/src/css/app.scss new file mode 100644 index 0000000..0849d9d --- /dev/null +++ b/src/css/app.scss @@ -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; + } +} diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000..74a0704 Binary files /dev/null and b/src/favicon.ico differ diff --git a/src/icons/128x128.png b/src/icons/128x128.png new file mode 100644 index 0000000..4202d42 Binary files /dev/null and b/src/icons/128x128.png differ diff --git a/src/icons/192x192.png b/src/icons/192x192.png new file mode 100644 index 0000000..b09ea8f Binary files /dev/null and b/src/icons/192x192.png differ diff --git a/src/icons/256x256.png b/src/icons/256x256.png new file mode 100644 index 0000000..489b280 Binary files /dev/null and b/src/icons/256x256.png differ diff --git a/src/icons/512x512.png b/src/icons/512x512.png new file mode 100644 index 0000000..18363a1 Binary files /dev/null and b/src/icons/512x512.png differ diff --git a/src/index.html b/src/index.html new file mode 100644 index 0000000..9e56542 --- /dev/null +++ b/src/index.html @@ -0,0 +1,126 @@ + + + + + + Clash Web Panel + + + + + + + +
+ + +
+ +
+
+ 规则列表 + + +
    +
  • + {{it.type}} + {{it.payload}} + {{it.proxy}} +
  • +
+
+
+
+ +
+ +
+
+ 订阅配置 + +
+ 订阅地址 + + 更新 +
+ + +
    +
  • + {{i + 1}}. + {{it[0]}} + {{it[1]}} +
  • +
+
+
+
+ +
+
+ 系统设置 + +
+ 开机启动Clash + +
+ +
+ 设置为系统代理 + +
+ +
+ 允许局域网的连接 + +
+
+ +
+ Clash设置 + +
+ 代理模式 + + 全局 + 规则 + 直连 + +
+ +
+ Socks5 代理端口 + +
+ +
+ HTTP 代理端口 + +
+ +
+ 混合代理端口 + +
+ +
+
+ +
+ + \ No newline at end of file diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..a5defbc --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,172 @@ +/** + * + * @author yutent + * @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()] + }) + }) + } + } +})