├── config.json
├── images
├── bg.png
├── maho.gif
├── maho.ico
├── maho_72x72.ico
├── maho_96x96.ico
└── maho_144x144.ico
├── README.md
├── fonts
├── CascadiaMono.ttf
├── CascadiaMonoPL.ttf
├── CascadiaMonoItalic.ttf
├── GoogleSansText-Bold.ttf
├── CascadiaMonoPLItalic.ttf
├── GoogleSansText-Italic.ttf
├── GoogleSansText-Medium.ttf
├── GoogleSansText-Regular.ttf
├── GoogleSansText-BoldItalic.ttf
└── GoogleSansText-MediumItalic.ttf
├── assets
├── logging.js
├── index.js
├── sw.js
├── pages
│ ├── 404.js
│ ├── auth.js
│ └── home.js
├── Initialization.js
├── setting
│ ├── outbound-provider.js
│ ├── outbounds.js
│ ├── general.js
│ ├── setting.js
│ ├── ntp.js
│ ├── dns.js
│ ├── log.js
│ ├── package.js
│ └── mystery.js
├── route.js
├── task.js
├── document.js
├── api.js
└── index.css
├── app.webmanifest
├── sw.js
└── index.html
/config.json:
--------------------------------------------------------------------------------
1 | {"log":true,"speed":false,"shortly":false,"YiYan":true}
--------------------------------------------------------------------------------
/images/bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/bg.png
--------------------------------------------------------------------------------
/images/maho.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/maho.gif
--------------------------------------------------------------------------------
/images/maho.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/maho.ico
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # TPanel
2 | 一个不知道是什么的透明面板。
3 |
4 | ## 免责申明
5 | 所有用户行为均与本人无关,使用面板即视为同意。
6 |
--------------------------------------------------------------------------------
/images/maho_72x72.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/maho_72x72.ico
--------------------------------------------------------------------------------
/images/maho_96x96.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/maho_96x96.ico
--------------------------------------------------------------------------------
/fonts/CascadiaMono.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/CascadiaMono.ttf
--------------------------------------------------------------------------------
/fonts/CascadiaMonoPL.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/CascadiaMonoPL.ttf
--------------------------------------------------------------------------------
/images/maho_144x144.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/images/maho_144x144.ico
--------------------------------------------------------------------------------
/fonts/CascadiaMonoItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/CascadiaMonoItalic.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-Bold.ttf
--------------------------------------------------------------------------------
/fonts/CascadiaMonoPLItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/CascadiaMonoPLItalic.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-Italic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-Italic.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-Medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-Medium.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-Regular.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-BoldItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-BoldItalic.ttf
--------------------------------------------------------------------------------
/fonts/GoogleSansText-MediumItalic.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xireiki/tpanel/HEAD/fonts/GoogleSansText-MediumItalic.ttf
--------------------------------------------------------------------------------
/assets/logging.js:
--------------------------------------------------------------------------------
1 | export const logging = {
2 | info: function(...args){
3 | console.info(...args);
4 | },
5 | warn: function(...args){
6 | console.warn(...args);
7 | },
8 | error: function(...args){
9 | console.error(...args);
10 | }
11 | }
--------------------------------------------------------------------------------
/assets/index.js:
--------------------------------------------------------------------------------
1 | import { goto, loadPage } from "./route.js";
2 | import { verifyAuthorizationCode } from "./document.js";
3 |
4 | if(localStorage.getItem("mystery") == undefined){
5 | localStorage.setItem("mystery", JSON.stringify({
6 | log: true,
7 | speed: false,
8 | shortly: false,
9 | YiYan: true
10 | }));
11 | }
12 |
13 | window.addEventListener("load", async () => {
14 | window.addEventListener("popstate", e => {
15 | if (e.state) {
16 | loadPage(e.state.path);
17 | }
18 | })
19 | const path = window.location.pathname;
20 | history.replaceState({path: path}, null, document.location.href);
21 | loadPage(path);
22 | })
23 |
--------------------------------------------------------------------------------
/app.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "神秘",
3 | "short_name": "神秘",
4 | "start_url": "/",
5 | "description": "神秘啊神秘~",
6 | "icons": [
7 | {
8 | "src": "/images/maho.ico",
9 | "sizes": "32x32",
10 | "type": "image/x-icon"
11 | },
12 | {
13 | "src": "/images/maho_72x72.ico",
14 | "sizes": "72x72",
15 | "type": "image/x-icon"
16 | },
17 | {
18 | "src": "/images/maho_96x96.ico",
19 | "sizes": "96x96",
20 | "type": "image/x-icon"
21 | },
22 | {
23 | "src": "/images/maho_144x144.ico",
24 | "sizes": "144x144",
25 | "type": "image/x-icon"
26 | }
27 | ],
28 | "display": "standalone",
29 | "background_color": "#ffffff"
30 | }
--------------------------------------------------------------------------------
/assets/sw.js:
--------------------------------------------------------------------------------
1 | if ("serviceWorker" in navigator) {
2 | window.addEventListener("load", () => {
3 | // 注册 service worker
4 | navigator.serviceWorker.register("/sw.js", {scope: "/"}).then(registeration => {
5 | console.info("PWA Registered!scope: " + registeration.scope);
6 | }).catch (err => {
7 | console.error("PWA Registration Failed.");
8 | });
9 | // 提醒用户页面已更新
10 | navigator.serviceWorker.oncontrollerchange = e => {
11 | alert("页面已更新,当前版本 v0.0.1-alpha.5");
12 | }
13 | // 提示使用离线版本
14 | if(!navigator.onLine){
15 | console.warn("Is currently offline.");
16 | // 提示离线逻辑
17 | window.addEventListener("online", e => {
18 | console.info("Network is connected.")
19 | })
20 | }
21 | });
22 | }
--------------------------------------------------------------------------------
/assets/pages/404.js:
--------------------------------------------------------------------------------
1 | import { doc } from "../document.js";
2 | import { goto } from "../route.js";
3 |
4 | export function notFound(){
5 | doc.createElement("style", null, document.querySelector("#app"))
6 | .then(style => {
7 | style.textContent = `
8 | .notFound {
9 | max-width: 90%;
10 | max-height: 90%;
11 | margin: 2% 2%;
12 | padding: 2%;
13 | border: var(--border-size) solid var(--border-color);
14 | box-shadow: 0 0 10px var(--light-color);
15 | overflow: scroll;
16 | border-radius: 25px;
17 | }
18 | `
19 | })
20 | doc.createElement("div", {id: "notFound", class: ["notFound"]}, document.querySelector("#app"))
21 | .then(div => {
22 | div.innerHTML = "
404 Not Found
那个...那个⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄,其实这个页面她跑了... (*꒦ິ⌓꒦ີ)
"
23 | })
24 | }
--------------------------------------------------------------------------------
/assets/Initialization.js:
--------------------------------------------------------------------------------
1 | (function(){
2 |
3 | if(false || !! window.MSInpoutMethodContext || !!document.documentMode){
4 | alert( '请勿使用 IE 浏览器!' )
5 | throw( new Error( '请勿使用 IE 浏览器!' ) )
6 | }
7 |
8 | var Sys = {}
9 | var ua = navigator.userAgent.toLowerCase()
10 | var s = void 0;
11 | ( s = ua.match( /edg\/([\d.]+)/ ) )
12 | ? ( Sys.edg = s[ 1 ] )
13 | : ( s = ua.match( /firefox\/([\d.]+)/ ) )
14 | ? ( Sys.firefox = s[ 1 ] )
15 | : ( s = ua.match( /chrome\/([\d.]+)/ ) )
16 | ? ( Sys.chrome = s[ 1 ] )
17 | : ( s = ua.match( /opera\.([\d.]+)/ ) )
18 | ? ( Sys.opera = s[ 1 ] )
19 | : ( s = ua.match( /version\/([\d.]+).*safari/ ) )
20 | ? ( Sys.safari = s[ 1 ] )
21 | : void 0
22 |
23 | var version = parseInt( Object.values( Sys )[ 0 ].split( '.' )[ 0 ], 10 )
24 |
25 | if ( version < 42 ) {
26 | var message = '检测到您的浏览器内核版本为 ' + version + ',请将其升级到 42 以上,否则将出现无法预料的问题!!'
27 | alert( message )
28 | throw( new Error( message ) )
29 | }
30 | })()
--------------------------------------------------------------------------------
/sw.js:
--------------------------------------------------------------------------------
1 | const CacheVersion = 7;
2 | const CacheName = "shenmi_v" + CacheVersion;
3 | const CacheList = [
4 | "/",
5 | "/index.html",
6 | "/assets/index.js",
7 | "/assets/index.css",
8 | "/images/maho.gif"
9 | ]
10 |
11 | this.addEventListener("install", e => {
12 | e.waitUntil(
13 | caches.open(CacheName).then(cache => {
14 | return cache.addAll(CacheList)
15 | }).then(this.skipWaiting)
16 | )
17 | });
18 |
19 | this.addEventListener("activate", e => {
20 | e.waitUntil(
21 | Promise.all([
22 | this.clients.claim(),
23 | caches.keys().then(cacheList => {
24 | return Promise.all(
25 | cacheList.map(cacheName => {
26 | if(cacheName !== CacheName){
27 | return caches.delete(cacheName)
28 | }
29 | })
30 | );
31 | })
32 | ])
33 | );
34 | });
35 |
36 | this.addEventListener("fetch", e => {
37 | const url = new URL(e.request.url);
38 | if(url.origin !== self.origin){
39 | return
40 | }
41 | if(url.pathname.startsWith("/auth") || url.pathname.startsWith("/setting") || url.pathname.startsWith("/404") || url.pathname === "/"){
42 | let pageRequest = new Request("/");
43 | e.respondWith(caches.match(pageRequest, { ignoreSearch: true }).then(response => {
44 | return response || fetch(pageRequest)
45 | }))
46 | } else {
47 | e.respondWith(fetch(e.request).catch(() => {
48 | return caches.match(e.request)
49 | }));
50 | }
51 | });
52 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | 神秘
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/assets/setting/outbound-provider.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function outboundProvider(){
6 | if(!window.panel){
7 | return getPanel(outboundProvider)
8 | }
9 | doc.createElement("div")
10 | .then(div => {
11 | div.id = "subscribe";
12 | div.classList.add("settingListBox");
13 | // 标题
14 | doc.createElement("p")
15 | .then(p => {
16 | p.innerText = "订阅";
17 | p.addEventListener("click", (event) => {
18 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
19 | setTimeout(() => {
20 | goto("/setting");
21 | }, 100);
22 | });
23 | div.append(p);
24 | });
25 | panel.subsList().then(req => req.json()).then(json => {
26 | doc.createElement("div").then(allsub => {
27 | allsub.classList.add("allsub");
28 | for(let s of json){
29 | doc.createElement("div")
30 | .then(everysub => {
31 | everysub.classList.add("everysub");
32 | doc.createElement("p").then(p => {
33 | p.innerText = s.name;
34 | everysub.append(p);
35 | });
36 | doc.createElement("p").then(p => {
37 | p.innerText = s.type == "http" ? "机场订阅" : "本地配置";
38 | everysub.append(p);
39 | });
40 | doc.createElement("p").then(p => {
41 | p.innerText = s.enabled == undefined ? "已启用" : s.enabled ? "已启用" : "已停用";
42 | everysub.append(p);
43 | });
44 | allsub.append(everysub);
45 | });
46 | }
47 | div.append(allsub);
48 | });
49 | }).catch(err => logging.error(ert));
50 | document.querySelector('#app').append(div);
51 | });
52 | }
53 |
54 | export function outboundProviderEdit(id){
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/assets/setting/outbounds.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function outbounds(){
6 | if(!window.panel){
7 | return getPanel(outbounds)
8 | }
9 | doc.createElement("div", {id: "outbounds", class: ["settingListBox"]}, document.querySelector('#app'))
10 | .then(div => {
11 | doc.createElement("p", {innerText: "出站", class: ["subsTitle"]}, div)
12 | .then(p => {
13 | p.addEventListener("click", (event) => {
14 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
15 | setTimeout(() => {
16 | goto("/setting");
17 | }, 100);
18 | });
19 | });
20 | doc.createElement("div", {class: ["allsub"]}, div)
21 | .then(div2 => {
22 | panel.outbounds()
23 | .then(response => response.json())
24 | .then(outbounds => {
25 | outbounds.forEach(outbound => {
26 | doc.createElement("div", {id: "out_" + outbound.tag, class: ["everysub"]}, div2)
27 | .then(outElement => {
28 | doc.createElement("p", {id: "out_" + outbound.tag + "_name", class: ["outboundName"], innerText: outbound.tag}, outElement)
29 | doc.createElement("p", {id: "out_" + outbound.tag + "_type", class: ["outboundType"], innerText: outbound.type === "urltest" ? "自动选择" : outbound.type === "selector" ? "手动选择" : "未适配类型"}, outElement)
30 | doc.createElement("p", {id: "out_" + outbound.tag + "_type", class: ["outboundType"], innerText: outbound.enabled == undefined ? "已启用" : outbound.enabled ? "已启用" : "已停用"}, outElement)
31 | })
32 | })
33 | }).catch(err => {
34 | goto("/auth")
35 | })
36 | })
37 | });
38 | }
39 |
40 | export function outboundEdit(id){
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/assets/pages/auth.js:
--------------------------------------------------------------------------------
1 | import { doc } from "../document.js";
2 | import { maho } from "../api.js";
3 | import { goto } from "../route.js";
4 |
5 | export function auth(){
6 | return new Promise((resolve, reject) => {
7 | doc.createElement("div")
8 | .then(div => {
9 | div.id = "authorization";
10 | div.classList.add("authorization");
11 | return div;
12 | })
13 | .then(div => doc.query("#app").append(div));
14 | doc.createElement("p")
15 | .then(p => {
16 | p.innerText = "请输入授权码";
17 | p.id = "tip";
18 | return p
19 | })
20 | .then(p => doc.query("#authorization").append(p));
21 | doc.createElement("input")
22 | .then(input => {
23 | input.type = "text";
24 | input.id = "inputAuth";
25 | input.addEventListener("keypress", (event) => {
26 | if(event.key == "Enter"){
27 | function switchError(text, time = 3000){
28 | let tip = doc.query("#tip");
29 | if(window.tipText == undefined){
30 | window.tipText = tip.innerText;
31 | }
32 | tip.innerText = text;
33 | tip.setAttribute("style", "color: #ff" + getComputedStyle(doc.query('html')).getPropertyValue('--authorization-tip-color').slice(3));
34 | setTimeout(() => {
35 | tip.removeAttribute("style");
36 | tip.innerText = window.tipText;
37 | }, time);
38 | }
39 | if(input.value != "" && input.value){
40 | let check = new maho(input.value);
41 | check.check()
42 | .then(() => {
43 | localStorage.auth = input.value;
44 | doc.query("#authorization").remove();
45 | window.panel = new maho(localStorage.auth);
46 | goto("/");
47 | })
48 | .catch(err => {
49 | console.log(err)
50 | if("Unauthorization" === err.message){
51 | switchError("授权码错误!");
52 | } else if(err){
53 | switchError("无法连接神秘后端");
54 | }
55 | })
56 | } else if(input.value == ""){
57 | switchError("授权码不能为空!");
58 | }
59 | }
60 | });
61 | return input
62 | })
63 | .then(input => doc.query("#authorization").append(input));
64 | })
65 | }
66 |
--------------------------------------------------------------------------------
/assets/route.js:
--------------------------------------------------------------------------------
1 | import { maho } from "./api.js";
2 | import { verifyAuthorizationCode } from "./document.js";
3 |
4 | import { index } from "./pages/home.js"
5 | import { notFound } from "./pages/404.js"
6 | import { auth } from "./pages/auth.js";
7 | import { setting } from "./setting/setting.js";
8 | import { general } from "./setting/general.js";
9 | import { mystery } from "./setting/mystery.js";
10 | import { packageListOption } from "./setting/package.js";
11 | import { log } from "./setting/log.js";
12 | import { ntp } from "./setting/ntp.js";
13 | import { outboundProvider, outboundProviderEdit } from "./setting/outbound-provider.js";
14 | import { outbounds, outboundEdit } from "./setting/outbounds.js";
15 | import { dns, dnsServer, fakeip } from "./setting/dns.js";
16 |
17 | const pages = {
18 | "/": index,
19 | "/404": notFound,
20 | "/auth": auth,
21 | "/setting": setting,
22 | "/setting/general": general,
23 | "/setting/mystery": mystery,
24 | "/setting/package": packageListOption,
25 | "/setting/log": log,
26 | "/setting/ntp": ntp,
27 | "/setting/provider": outboundProvider,
28 | "/setting/provider/:id": outboundProviderEdit,
29 | "/setting/outbounds": outbounds,
30 | "/setting/outbounds/:id": outboundEdit,
31 | "/setting/dns": dns,
32 | "/setting/dns/server": dnsServer,
33 | "/setting/dns/fakeip": fakeip
34 | }
35 |
36 | function getTargetRoute(path) {
37 | for (const [key, value] of Object.entries(pages)) {
38 | const regex = new RegExp(`^${key.replace(/:.+/g, '([^/]+)')}$`);
39 | const match = path.match(regex);
40 | if (match) {
41 | const args = match.slice(1);
42 | return [key, value, args];
43 | }
44 | }
45 | return [];
46 | }
47 |
48 | export function loadPage(path){
49 | document.querySelector('#app').innerHTML= "";
50 | let [newPath, func, args] = getTargetRoute(path);
51 | if(newPath === path){
52 | func()
53 | } else if(!newPath){
54 | goto("/404", true)
55 | } else {
56 | func(...args)
57 | }
58 | }
59 |
60 | export function goto(path, replace){
61 | if(replace){
62 | history.replaceState({path: path}, '', path);
63 | } else {
64 | history.pushState({path: path}, '', path);
65 | }
66 | loadPage(path);
67 | }
68 |
--------------------------------------------------------------------------------
/assets/task.js:
--------------------------------------------------------------------------------
1 | export class SuperTask {
2 | constructor(parallelCount = 2){
3 | this.parallelCount = parallelCount;
4 | this.tasks = [];
5 | this.runningTask = 0;
6 | }
7 |
8 | add(task){
9 | return new Promise((resolve, reject) => {
10 | this.tasks.push({task, resolve, reject});
11 | this.#run();
12 | });
13 | }
14 |
15 | #run(){
16 | while(this.runningTask < this.parallelCount && this.tasks.length > 0){
17 | const {task, resolve, reject} = this.tasks.shift();
18 | this.runningTask++;
19 | try {
20 | task()
21 | .then(resolve, reject)
22 | .finally(() => {
23 | this.runningTask--;
24 | this.#run();
25 | });
26 | } catch(err){
27 | reject(err);
28 | }
29 | }
30 | }
31 | }
32 |
33 | export function processTasks(...tasks){
34 | let isRunning = false;
35 | const result = [];
36 | let i = 0;
37 | return {
38 | start(){
39 | return new Promise(async (resolve, reject) => {
40 | if(isRunning) return;
41 | isRunning = true;
42 | while(i < tasks.length){
43 | try {
44 | result.push(await tasks[i]());
45 | } catch(err){
46 | result.push(err);
47 | }
48 | i++
49 | if(!isRunning){
50 | return;
51 | }
52 | }
53 | isRunning = false;
54 | resolve(result);
55 | });
56 | },
57 | pause(){
58 | isRunning = false;
59 | }
60 | };
61 | }
62 |
63 | function _runTask(task, callback){
64 | const start = Date.now();
65 | requestAnimatiomFrame(() => {
66 | if(Date.now() - start < 16.6){
67 | task();
68 | callback();
69 | } else {
70 | _runTask();
71 | }
72 | });
73 | }
74 |
75 | export function runTask(task){
76 | return new Promise((resolve, reject) => {
77 | _runTask(task, resolve);
78 | });
79 | }
80 |
81 | export const makeWorker = f => {
82 | let pendingJobs = {};
83 | const worker = new Worker (
84 | URL.createObjectURL (new Blob ([`(${f.toString ()})()`]))
85 | );
86 | worker.onmessage = ({data: {result, jobId}}) => {
87 | // 调用resolve,改变Promise状态
88 | pendingJobs[jobId] (result);
89 | // 删掉,防止key冲突
90 | delete pendingJobs[jobId];
91 | };
92 | return (...message) =>
93 | new Promise (resolve => {
94 | const jobId = String (Math.random ());
95 | pendingJobs[jobId] = resolve;
96 | worker.postMessage ({jobId, message});
97 | });
98 | };
99 |
--------------------------------------------------------------------------------
/assets/setting/general.js:
--------------------------------------------------------------------------------
1 | import { doc, config } from "../document.js";
2 | import { goto } from "../route.js";
3 |
4 | export function general(e){
5 | if(document.getElementById("general")) return;
6 | // 通用盒子
7 | doc.createElement("div")
8 | .then(div => {
9 | div.id = "general";
10 | div.classList.add("settingBox");
11 | // 通用标题
12 | doc.createElement("p")
13 | .then(p => {
14 | p.innerText = "通用";
15 | p.addEventListener("click", (event) => {
16 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
17 | setTimeout(() => {
18 | goto("/setting");
19 | }, 100);
20 | });
21 | div.append(p);
22 | });
23 | let toggleOptionList = [
24 | {
25 | id: "speed",
26 | key: "speed",
27 | target: "#speed",
28 | name: "在主页显示速率",
29 | action: function(event, selfObj){
30 | config(selfObj.key, event.target.checked);
31 | }
32 | },
33 | {
34 | id: "log",
35 | key: "log",
36 | target: "#log",
37 | name: "在主页显示日志",
38 | action: function(event, selfObj){
39 | config(selfObj.key, event.target.checked);
40 | }
41 | },
42 | {
43 | id: "yiyan",
44 | key: "YiYan",
45 | target: "#jinrishici-sentence",
46 | name: "启用一言",
47 | action: function(event, selfObj){
48 | config(selfObj.key, event.target.checked);
49 | }
50 | },
51 | {
52 | id: "subsOrder",
53 | key: "subsOrder",
54 | name: "订阅栏长按修改顺序",
55 | disabled: true,
56 | action: function(event, selfObj){
57 | config(selfObj.key, event.target.checked);
58 | }
59 | },
60 | {
61 | id: "functionControl",
62 | key: "function",
63 | name: "主页功能中心",
64 | disabled: true,
65 | action: function(event, selfObj){
66 | setTimeout(event => {
67 | doc.query("#" + selfObj.id + "Checked").checked = false;
68 | }, 1000);
69 | // config(selfObj.key, event.target.checked);
70 | if(config("shortly")){
71 |
72 | }
73 | }
74 | },
75 | {
76 | id: "hideNamelessApp",
77 | key: "hideNameless",
78 | name: "仅显示有名称应用",
79 | action: function(event, selfObj){
80 | config(selfObj.key, event.target.checked);
81 | }
82 | }
83 | ];
84 | for(let o of toggleOptionList){
85 | doc.createElement("div")
86 | .then(option => {
87 | doc.createElement("p")
88 | .then(p => {
89 | p.classList.add("settingName")
90 | p.innerText = o.name;
91 | option.append(p);
92 | });
93 | doc.createElement("div")
94 | .then(labelBox => {
95 | doc.createElement("input")
96 | .then(input => {
97 | input.id = o.id + "Checked"
98 | input.type = "checkbox";
99 | input.checked = config(o.key) ? true : false;
100 | if(o.disabled) input.disabled = true;
101 | input.addEventListener("change", event => o.action(event, o));
102 | input.setAttribute("style", "width: 0; height: 0;");
103 | labelBox.append(input);
104 | });
105 | doc.createElement("label")
106 | .then(label => {
107 | label.htmlFor = o.id + "Checked";
108 | label.classList.add("toggle");
109 | labelBox.append(label);
110 | });
111 | option.append(labelBox);
112 | });
113 | div.append(option)
114 | });
115 | }
116 | document.querySelector('#app').append(div);
117 | });
118 | }
--------------------------------------------------------------------------------
/assets/setting/setting.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function setting(){
6 | if(!window.panel){
7 | return getPanel(setting)
8 | }
9 | let options = [
10 | {
11 | id: 1,
12 | name: "通用",
13 | url: "/setting/general",
14 | description: "本面板的设置。快点我!"
15 | },
16 | {
17 | id: 2,
18 | name: "神秘",
19 | url: "/setting/mystery",
20 | description: "神秘啊神秘~启动!"
21 | },
22 | {
23 | id: 3,
24 | name: "订阅",
25 | url: "/setting/provider",
26 | description: "订阅列表,你有几个机场?",
27 | extra: function(description){
28 | panel.subsList().then(req => req.json()).then(json => {
29 | if(json.length == 0) description.innerText += "啥玩意儿?你一个机场都没?!";
30 | else if(json.length <= 3) description.innerText += `咦~你怎么才${json.length}个机场啊!`;
31 | else if(json.length > 3 && json.length <= 10) description.innerText += `你有${json.length}个机场。`;
32 | else description.innerText += `哇!!你居然有${json.length}个机场!`;
33 | }).catch(err => {
34 | logging.error(err);
35 | description.innerText += "你能告诉我吗?";
36 | });
37 | }
38 | },
39 | {
40 | id: 4,
41 | name: "出站",
42 | url: "/setting/outbounds",
43 | description: "订阅分组?传送阵!喵",
44 | extra: function(description){
45 | panel.outbounds().then(req => req.json()).then(json => {
46 | if(json.length == 0) description.innerText += "~ 你怎么一个传送阵都没呀? 喵~";
47 | else if(json.length < 3) description.innerText += "~ 要坏掉了! 喵"
48 | else if(json.length == 3) description.innerText += "~ 你是小白吗?居然只有1个新建的传送阵! 喵";
49 | else if(json.length > 3 && json.length <= 10) description.innerText += "~ 是正常的! 喵~";
50 | else description.innerText += "~ 你要这么多出站干什么呀? 喵~";
51 | }).catch(err => {
52 | logging.error(err);
53 | description.innerText += "~";
54 | });
55 | }
56 | },
57 | {
58 | id: 5,
59 | name: "DNS",
60 | url: "/setting/dns",
61 | description: "书灵身上一定有墨香!因为“腹有诗书气自华”。"
62 | },
63 | {
64 | id: 6,
65 | name: "NTP",
66 | url: "/setting/ntp",
67 | description: "NTP 时间服务器,用于校准时间。"
68 | },
69 | {
70 | id: 7,
71 | name: "日志",
72 | url: "/setting/log",
73 | description: "获取神秘日志,聆听神秘之音,奏响神秘乐章!"
74 | },
75 | {
76 | id: 8,
77 | name: "黑/白名单",
78 | url: "/setting/package",
79 | description: "将垃圾应用加入指定名单,实现应用分流!"
80 | }
81 | ]
82 | doc.createElement("div")
83 | .then(div => {
84 | div.id = "setting";
85 | div.classList.add("setting");
86 | doc.createElement("p")
87 | .then(title => {
88 | title.innerText = "设置";
89 | title.classList.add("settingTitle");
90 | title.addEventListener("click", event => {
91 | goto("/");
92 | });
93 | doc.createElement("div")
94 | .then(inner => {
95 | inner.id = "inner";
96 | inner.classList.add("inner");
97 | for(let o of options){
98 | doc.createElement("div")
99 | .then(option => {
100 | option.classList.add("option");
101 | option.addEventListener("click", () => {
102 | goto(o.url);
103 | });
104 | doc.createElement("div")
105 | .then(optionTitle => {
106 | optionTitle.id = `optionTitle${o.id}`;
107 | optionTitle.innerText = o.name;
108 | optionTitle.classList.add("optionTitle");
109 | option.append(optionTitle);
110 | });
111 | doc.createElement("div")
112 | .then(optionDescription => {
113 | optionDescription.id = `optionDescription${o.id}`;
114 | optionDescription.innerText = o.description;
115 | if(o.extra != undefined) o.extra(optionDescription);
116 | optionDescription.classList.add("optionDescription");
117 | option.append(optionDescription);
118 | });
119 | inner.append(option)
120 | });
121 | }
122 | div.append(inner);
123 | })
124 | div.append(title);
125 | });
126 | document.querySelector('#app').append(div);
127 | });
128 | }
129 |
--------------------------------------------------------------------------------
/assets/setting/ntp.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function ntp(){
6 | if(!window.panel){
7 | return getPanel(ntp)
8 | }
9 | doc.createElement("div")
10 | .then(div => {
11 | div.id = "ntpOption";
12 | div.classList.add("settingBox");
13 | // 标题
14 | doc.createElement("p")
15 | .then(p => {
16 | p.innerText = "NTP";
17 | p.addEventListener("click", (event) => {
18 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
19 | setTimeout(() => {
20 | goto("/setting");
21 | }, 100);
22 | });
23 | div.append(p);
24 | });
25 | panel.boxNTP().then(req => req.json()).then(json => {
26 | doc.createElement("div")
27 | .then(option => {
28 | doc.createElement("p")
29 | .then(p => {
30 | p.classList.add("settingName")
31 | p.innerText = "开启";
32 | option.append(p);
33 | });
34 | doc.createElement("div")
35 | .then(labelBox => {
36 | doc.createElement("input")
37 | .then(input => {
38 | input.id = "ntpChecked"
39 | input.type = "checkbox";
40 | input.checked = json.enabled ? true : false;
41 | input.addEventListener("change", event => {
42 | json.enabled = event.target.checked;
43 | });
44 | input.setAttribute("style", "width: 0; height: 0;");
45 | labelBox.append(input);
46 | });
47 | doc.createElement("label")
48 | .then(label => {
49 | label.htmlFor = "ntpChecked";
50 | label.classList.add("toggle");
51 | labelBox.append(label);
52 | });
53 | option.append(labelBox);
54 | });
55 | div.append(option);
56 | });
57 | let ntpOptions = [
58 | {
59 | id: "serverOption",
60 | name: "服务器地址",
61 | value: json.server == undefined ? "" : json.server,
62 | input: function(event){
63 | json.server = event.target.value;
64 | },
65 | action: function(event, selfObj){
66 | json.server = event.target.value;
67 | }
68 | },
69 | {
70 | id: "serverPortOption",
71 | name: "服务器端口",
72 | type: "number",
73 | pattern: "\d+",
74 | value: json.server_port == undefined ? "" : json.server_port,
75 | input: function(event){
76 | event.target.value = event.target.value.replace(/[\+\-e\.]+/g, "");
77 | let port = Number(event.target.value);
78 | if(event.target.value == ""){
79 | port = undefined;
80 | event.target.value = "";
81 | } else if(port < 1){
82 | port = 1;
83 | event.target.value = "1"
84 | } else if(port > 65535){
85 | port = 65535;
86 | event.target.value = "65535";
87 | }
88 | json.server_port = port;
89 | },
90 | action: function(event, selfObj){
91 | json.server_port = event.target.value;
92 | }
93 | },
94 | {
95 | id: "ntpCalibrationIntervalOption",
96 | name: "时间校准间隔",
97 | pattern: "[smh0-9]+",
98 | value: json.interval == undefined ? "" : json.interval,
99 | input: function(event){
100 | event.target.value = event.target.value.replace(/[^0-9smh]/, "");
101 | let duration = event.target.value.match(/^\d+[smh]/);
102 | if(duration instanceof Array){
103 | duration = duration[0];
104 | }
105 | json.interval = duration ? duration : undefined;
106 | },
107 | action: function(event, selfObj){
108 | json.interval = event.target.value;
109 | }
110 | }
111 | ]
112 | for(let o of ntpOptions){
113 | doc.createElement("div")
114 | .then(option => {
115 | doc.createElement("p")
116 | .then(p => {
117 | p.classList.add("settingName");
118 | p.classList.add("settingNameInput");
119 | p.innerText = o.name;
120 | option.append(p);
121 | });
122 | doc.createElement("div")
123 | .then(ntp => {
124 | ntp.id = o.id + "Box";
125 | ntp.classList.add("settingInputBox");
126 | doc.createElement("input")
127 | .then(input => {
128 | input.id = o.id + "Input";
129 | input.type = o.type ? o.type : "text";
130 | if(o.pattern) input.pattern = o.pattern;
131 | input.value = o.value;
132 | if(o.placeholder) input.placeholder = o.placeholder;
133 | if(o.input) input.addEventListener("input", o.input);
134 | input.addEventListener("keypress", event => o.action(event, o));
135 | ntp.append(input);
136 | });
137 | option.append(ntp);
138 | });
139 | div.append(option)
140 | });
141 | }
142 | doc.createElement("button")
143 | .then(button => {
144 | button.id = "submit";
145 | button.innerText = "提交";
146 | button.addEventListener("click", event => {
147 | panel.boxNTP(json, "PUT").then(req => {
148 | if(!req.ok) alert("修改失败,请刷新重试");
149 | }).catch(err => {
150 | logging.error(err);
151 | alert("无法连接到神秘后端!");
152 | });
153 | });
154 | div.append(button);
155 | });
156 | }).catch(err => {
157 | logging.error(err);
158 | alert("无法连接到神秘后端!请刷新网页重试!");
159 | });
160 | document.querySelector('#app').append(div);
161 | });
162 | }
163 |
--------------------------------------------------------------------------------
/assets/setting/dns.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function dns(){
6 |
7 | doc.createElement("div", {id: "dns", class: ["setting"]}, document.querySelector('#app'))
8 | .then(div => {
9 | doc.createElement("p", {innerText: "DNS", class: ["settingTitle"]}, div)
10 | .then(p => {
11 | p.addEventListener("click", () => {
12 | goto("/setting");
13 | });
14 | });
15 | const pages = [
16 | {
17 | url: "/setting/dns/server",
18 | name: "DNS 服务",
19 | description: "这就是书灵~"
20 | },
21 | {
22 | url: "/setting/dns/rules",
23 | name: "DNS 规则",
24 | description: "骚年~掌控规则把~"
25 | },
26 | {
27 | url: "/setting/dns/fakeip",
28 | name: "FakeIP",
29 | description: "FakeIP 可以减少 DNS 请求。"
30 | }
31 | ]
32 | pages.forEach(page => {
33 | doc.createElement("div", {class: ["option"], onclick: () => {goto(page.url)}}, div)
34 | .then(div3 => {
35 | doc.createElement("p", {id: "dnsName", class: ["optionTitle"], innerText: page.name}, div3)
36 | doc.createElement("p", {id: "dnsDescription", class: ["optionDescription"], innerText: page.description}, div3)
37 | })
38 | })
39 | })
40 | }
41 |
42 | export function dnsServer(){
43 | if(!window.panel){
44 | return getPanel(dnsServer)
45 | }
46 | doc.createElement("div", {id: "dnsServer", class: ["settingListBox"]}, document.querySelector('#app'))
47 | .then(div => {
48 | doc.createElement("p", {innerText: "DNS 服务", class: ["subsTitle"]}, div)
49 | .then(p => {
50 | p.addEventListener("click", () => {
51 | goto("/setting/dns");
52 | });
53 | });
54 | doc.createElement("div", {class: ["allsub"]}, div)
55 | .then(div2 => {
56 | panel.dns()
57 | .then(response => response.json())
58 | .then(dnses => {
59 | dnses.forEach(dns => {
60 | doc.createElement("div", {id: "dns_" + dns.tag, class: ["everysub"]}, div2)
61 | .then(dnsElement => {
62 | doc.createElement("p", {id: "dns_" + dns.tag + "_name", class: ["dnsName"], innerText: dns.tag}, dnsElement)
63 | doc.createElement("p", {id: "dns_" + dns.tag + "_type", class: ["dnsType"], innerText: dns.type === "urltest" ? "自动选择" : dns.type === "selector" ? "手动选择" : "未适配类型"}, dnsElement)
64 | doc.createElement("p", {id: "dns_" + dns.tag + "_enabled", class: ["dnsEnabled"], innerText: dns.enabled == undefined ? "已启用" : dns.enabled ? "已启用" : "已停用"}, dnsElement)
65 | })
66 | })
67 | }).catch(err => {
68 | goto("/auth")
69 | })
70 | })
71 | });
72 | }
73 |
74 | export function fakeip(){
75 | if(!window.panel){
76 | return getPanel(fakeip)
77 | }
78 | doc.createElement("div", {id: "fakeip", class: ["settingBox"]}, document.querySelector('#app'))
79 | .then(div => {
80 | doc.createElement("p", {innerText: "FakeIP", onclick: ()=>{goto("/setting/dns")}}, div)
81 | panel.fakeip()
82 | .then(response => response.json())
83 | .then(fakeip => {
84 | doc.createElement("div", null, div)
85 | .then(option => {
86 | doc.createElement("p", {innerText: "启用 FakeIP", class: ["settingName"]}, option)
87 | doc.createElement("div", null, option)
88 | .then(labelBox => {
89 | doc.createElement("input", {id: "fakeipChecked", type: "checkbox", checked: fakeip.enabled ? true : false}, labelBox)
90 | .then(input => {
91 | input.setAttribute("style", "width: 0; height: 0;");
92 | });
93 | doc.createElement("label", {class: ["toggle"]}, labelBox)
94 | .then(label => {
95 | label.htmlFor = "fakeipChecked";
96 | });
97 | });
98 | });
99 | doc.createElement("div", {id: "inet4_range"}, div)
100 | .then(option => {
101 | doc.createElement("p", {innerText: "inet4_range", id: "inet4_range_name", class: ["settingName"]}, option)
102 | doc.createElement("div", {id: "inet4_range_box"}, option)
103 | .then(auth => {
104 | doc.createElement("input", {id: "inet4_range_input", type: "text", value: fakeip["inet4_range"]}, auth)
105 | });
106 | });
107 | doc.createElement("div", {id: "inet6_range"}, div)
108 | .then(option => {
109 | doc.createElement("p", {innerText: "inet6_range", id: "inet6_range_name", class: ["settingName"]}, option)
110 | doc.createElement("div", {id: "inet6_range_box"}, option)
111 | .then(auth => {
112 | doc.createElement("input", {id: "inet6_range_input", type: "text", value: fakeip["inet6_range"]}, auth)
113 | });
114 | });
115 | doc.createElement("button", {id: "submit", innerText: "提交"}, div)
116 | .then(button => {
117 | button.addEventListener("click", event => {
118 | const newFakeip = {
119 | enabled: doc.query("#fakeipChecked").checked,
120 | "inet4_range": doc.query("#inet4_range_input").value,
121 | "inet6_range": doc.query("#inet6_range_input").value
122 | }
123 | panel.fakeip(newFakeip, "PUT").then(req => {
124 | if(!req.ok){
125 | alert("修改失败,请刷新重试!");
126 | return goto("/auth")
127 | }
128 | fakeip = newFakeip;
129 | }).catch(err => {
130 | alert("无法连接到神秘后端!");
131 | });
132 | });
133 | })
134 | })
135 | })
136 | }
137 |
--------------------------------------------------------------------------------
/assets/setting/log.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function log(){
6 | if(!window.panel){
7 | return getPanel(log)
8 | }
9 | doc.createElement("div")
10 | .then(div => {
11 | div.id = "logOption";
12 | div.classList.add("settingBox");
13 | // 标题
14 | doc.createElement("p")
15 | .then(p => {
16 | p.innerText = "日志";
17 | p.addEventListener("click", (event) => {
18 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
19 | setTimeout(() => {
20 | goto("/setting");
21 | }, 100);
22 | });
23 | div.append(p);
24 | });
25 | panel.boxLog().then(req => req.json()).then(json => {
26 | doc.createElement("div")
27 | .then(option => {
28 | doc.createElement("p")
29 | .then(p => {
30 | p.classList.add("settingName")
31 | p.innerText = "开启";
32 | option.append(p);
33 | });
34 | doc.createElement("div")
35 | .then(labelBox => {
36 | doc.createElement("input")
37 | .then(input => {
38 | input.id = "logChecked"
39 | input.type = "checkbox";
40 | input.checked = json.disabled ? false : true;
41 | input.addEventListener("change", event => {
42 | json.disabled = event.target.checked ? false : true;
43 | });
44 | input.setAttribute("style", "width: 0; height: 0;");
45 | labelBox.append(input);
46 | });
47 | doc.createElement("label")
48 | .then(label => {
49 | label.htmlFor = "logChecked";
50 | label.classList.add("toggle");
51 | labelBox.append(label);
52 | });
53 | option.append(labelBox);
54 | });
55 | div.append(option);
56 | });
57 | doc.createElement("div")
58 | .then(option => {
59 | option.id = "level"; // 日志等级
60 | doc.createElement("p")
61 | .then(p => {
62 | p.id = "levelName";
63 | p.classList.add("settingName")
64 | p.innerText = "日志等级";
65 | option.append(p);
66 | });
67 | doc.createElement("div")
68 | .then(levelForm => {
69 | levelForm.id = "levelForm"; //
70 | levelForm.classList.add("modeForm");
71 | doc.createElement("select")
72 | .then(select => {
73 | select.addEventListener("change", event => {
74 | json.level = select.value;
75 | });
76 | for(let o of [{type:"trace"},{type:"debug"},{type:"info"},{type:"warn"},{type:"error"},{type:"fatal"},{type:"panic"}]){
77 | doc.createElement("option")
78 | .then(option2 => {
79 | option2.innerText = o.type
80 | option2.value = o.type;
81 | if(json.level == o.type) option2.selected = true;
82 | select.append(option2);
83 | });
84 | }
85 | levelForm.append(select);
86 | });
87 | option.append(levelForm);
88 | });
89 | div.append(option)
90 | });
91 | doc.createElement("div")
92 | .then(option => {
93 | option.id = "output";
94 | doc.createElement("p")
95 | .then(p => {
96 | p.id = "outputName";
97 | p.classList.add("settingName");
98 | p.classList.add("settingNameInput");
99 | p.innerText = "log 保存地址";
100 | option.append(p);
101 | });
102 | doc.createElement("div")
103 | .then(output => {
104 | output.id = "outputBox";
105 | output.classList.add("settingInputBox");
106 | doc.createElement("input")
107 | .then(input => {
108 | input.id = "outputInput";
109 | input.type = "text";
110 | input.value = json.output;
111 | input.addEventListener("input", event => {
112 | json.output = input.value;
113 | });
114 | output.append(input);
115 | });
116 | option.append(output);
117 | });
118 | div.append(option)
119 | });
120 | doc.createElement("div")
121 | .then(option => {
122 | doc.createElement("p")
123 | .then(p => {
124 | p.classList.add("settingName")
125 | p.innerText = "日志输出带时间";
126 | option.append(p);
127 | });
128 | doc.createElement("div")
129 | .then(labelBox => {
130 | doc.createElement("input")
131 | .then(input => {
132 | input.id = "timestampChecked"
133 | input.type = "checkbox";
134 | input.checked = json.timestamp ? true : false;
135 | input.addEventListener("change", event => {
136 | json.timestamp = event.target.checked ? true : false;
137 | });
138 | input.setAttribute("style", "width: 0; height: 0;");
139 | labelBox.append(input);
140 | });
141 | doc.createElement("label")
142 | .then(label => {
143 | label.htmlFor = "timestampChecked";
144 | label.classList.add("toggle");
145 | labelBox.append(label);
146 | });
147 | option.append(labelBox);
148 | });
149 | div.append(option);
150 | });
151 | doc.createElement("button")
152 | .then(button => {
153 | button.id = "submit";
154 | button.innerText = "提交";
155 | button.addEventListener("click", event => {
156 | if(json.notice == undefined){
157 | json.notice = "";
158 | }
159 | panel.boxLog(json, "PUT").then(req => {
160 | if(!req.ok) alert("修改失败,请刷新重试!");
161 | }).catch(err => {
162 | logging.error(err);
163 | alert("无法连接到神秘后端!");
164 | });
165 | });
166 | div.append(button);
167 | });
168 | }).catch(err => {
169 | logging.error(err);
170 | alert("无法连接到神秘后端,请刷新网页重试!");
171 | });
172 | document.querySelector('#app').append(div);
173 | });
174 | }
--------------------------------------------------------------------------------
/assets/document.js:
--------------------------------------------------------------------------------
1 | import { maho } from "./api.js";
2 | import { goto } from "./route.js";
3 |
4 | export const doc = {
5 | createElement: function(tagName, attrs, target) {
6 | return new Promise(resolve => {
7 | const element = document.createElement(tagName);
8 | if(attrs){
9 | for (const attr in attrs){
10 | if(attr === "class"){
11 | for(const cls of attrs[attr]){
12 | element.classList.add(cls)
13 | }
14 | } else {
15 | element[attr] = attrs[attr]
16 | }
17 | }
18 | }
19 | if(target){
20 | target.append(element)
21 | }
22 | resolve(element);
23 | });
24 | },
25 | getElement: function(select, context){
26 | return new Promise(resolve => {
27 | context = context || document;
28 | const element = context.querySelectorAll(select);
29 | if(element.length == 1){
30 | resolve(Array.prototype.slice.call(element)[0]);
31 | } else {
32 | resolve(Array.prototype.slice.call(element));
33 | }
34 | });
35 | },
36 | query: function(select, context){
37 | context = context || document;
38 | const element = context.querySelectorAll(select);
39 | if(element.length == 1){
40 | return Array.prototype.slice.call(element)[0];
41 | }
42 | return Array.prototype.slice.call(element);
43 | },
44 | jsonp: function jsonp(url, callback) {
45 | const fnName = 'jsonpCallback_' + Math.round(100000 * Math.random());
46 | window[fnName] = function(data) {
47 | callback(data);
48 | delete window[fnName];
49 | }
50 | const fullUrl = `${url}?callback=${fnName}`;
51 | const script = document.createElement('script');
52 | script.src = fullUrl;
53 | document.body.appendChild(script);
54 | }
55 | }
56 |
57 | export function toMemory(memory){
58 | let Reversal = false;
59 | if(memory < 0){
60 | Reversal = true;
61 | memory = -1 * memory;
62 | }
63 | if(memory < 1024)
64 | return (Reversal ? "-" : "") + memory.toFixed(2) + "B";
65 | else if(memory < 1048576)
66 | return (Reversal ? "-" : "") + (memory / 1024).toFixed(2) + "KB";
67 | else if(memory < 1073741824)
68 | return (Reversal ? "-" : "") + (memory / 1048576).toFixed(2) + "MB";
69 | else if(memory < 1099511627776)
70 | return (Reversal ? "-" : "") + (memory / 1073741824).toFixed(2) + "GB";
71 | else if(memory < 1125899906842624)
72 | return (Reversal ? "-" : "") + (memory / 1099511627776).toFixed(2) + "TB";
73 | else if(memory < 1152921504606846976)
74 | return (Reversal ? "-" : "") + (memory / 1125899906842624).toFixed(2) + "PB";
75 | else
76 | return (Reversal ? "-" : "") + (memory / 1152921504606846976).toFixed(2) + "EB";
77 | }
78 |
79 | export function config(key, value){
80 | if(value != undefined && key != undefined){
81 | let tmpObj = JSON.parse(localStorage.getItem("mystery") || "{}");
82 | tmpObj[key] = value;
83 | return localStorage.setItem("mystery", JSON.stringify(tmpObj));
84 | } else if(key != undefined){
85 | return JSON.parse(localStorage.getItem("mystery") || '{}')[key];
86 | } else {
87 | return JSON.parse(localStorage.getItem("mystery") || '{}');
88 | }
89 | }
90 |
91 | export function verifyAuthorizationCode(auth = localStorage.auth || "node"){
92 | return new Promise((resolve, reject) => {
93 | let p = new maho(auth);
94 | p.check().then(() => {
95 | resolve(p);
96 | })
97 | .catch(err => {
98 | console.log(err);
99 | reject(err);
100 | });
101 | });
102 | }
103 |
104 | function generateThumbnails(imageFile) {
105 | return new Promise((resolve, reject) => {
106 | createImageBitmap(imageFile).then(imageBitmap => {
107 | const canvas = document.createElement('canvas')
108 | canvas.width = imageBitmap.height > 1000 ? (imageBitmap.height / 4) : (imageBitmap.height / 2)
109 | canvas.height = imageBitmap.width > 1000 ? (imageBitmap.width / 4) : (imageBitmap.width / 2)
110 | const ctx = canvas.getContext('2d')
111 | ctx.drawImage(imageBitmap, 0, 0, canvas.width, canvas.height)
112 | const url = canvas.toDataURL('image/jpeg')
113 | // console.log('缩略图')
114 | // console.log('%c ', 'background:url(' + url + ') no-repeat ;line-height:' + canvas.height + 'px;font-size:' + canvas.height + 'px')
115 | if (url.length > 80 * 1024) {
116 | generateThumbnails(convertBase64UrlToBlob(url)).then(res => {
117 | resolve(res)
118 | })
119 | } else {
120 | resolve(url)
121 | }
122 | }).catch((err) => {
123 | reject('缩略图生成失败:' + err)
124 | })
125 | })
126 | }
127 |
128 | function convertBase64UrlToBlob(base64) {
129 | const arr = base64.split(',')
130 | const mime = arr[0].match(/:(.*?);/)[1]
131 | const str = atob(arr[1])
132 | let n = str.length
133 | const u8arr = new Uint8Array(n)
134 | while (n--) {
135 | u8arr[n] = str.charCodeAt(n)
136 | }
137 | return new Blob([u8arr], { type: mime })
138 | }
139 |
140 | function getBase64ImageUrl(url, callBack, imgType) {
141 | if(!imgType){
142 | imgType = "image/png";
143 | }
144 | let img = new Image();
145 | img.src= url;
146 | img.crossOrigin = "anonymous";
147 | img.onload = function(){
148 | let canvas = document.createElement("canvas");
149 | let ctx = canvas.getContext("2d");
150 | ctx.drawImage(img, 0, 0);
151 | let dataURL = canvas.toDataURL(imgType);
152 | callBack(dataURL);
153 | }
154 | }
155 |
156 | function getColors(image) {
157 | return new Promise((resolve) => {
158 | const canvas = document.createElement('canvas');
159 | const context = canvas.getContext('2d');
160 | canvas.width = image.width;
161 | canvas.height = image.height;
162 | context.drawImage(image, 0, 0);
163 | const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
164 | const data = imageData.data;
165 | const colors = [];
166 | for(let i = 0; i < data.length; i += 4) {
167 | const red = data[i];
168 | const green = data[i + 1];
169 | const blue = data[i + 2];
170 | colors.push([red, green, blue]);
171 | }
172 | const sortedColors = colors.sort((a, b) =>
173 | colors.filter(v => v===a).length - colors.filter(v => v===b).length
174 | );
175 | const mostFrequentColor = sortedColors[sortedColors.length - 1];
176 | resolve('rgb(' + mostFrequentColor.join(',') + ')');
177 | });
178 | }
179 |
180 | export function getDominantColor(){
181 | return new Promise((resolve, reject) => {
182 | getBase64ImageUrl("/images/bg.png", dataUrl => {
183 | generateThumbnails(convertBase64UrlToBlob(dataUrl))
184 | .then(img => {
185 | doc.createElement("img")
186 | .then(i => {
187 | i.src = img;
188 | i.style.display = "none";
189 | i.onload = function(){
190 | getColors(i).then(color => {
191 | // window.bgDominantColor = color;
192 | resolve(color);
193 | i.remove();
194 | });
195 | }
196 | document.body.append(i);
197 | })
198 | })
199 | .catch(err => {
200 | logging.error(err);
201 | reject(err)
202 | });
203 | });
204 | });
205 | }
206 |
207 | export function getPanel(func){
208 | if(localStorage.getItem("auth") != null){
209 | verifyAuthorizationCode()
210 | .then(pan => {
211 | window.panel = pan;
212 | return func();
213 | })
214 | .catch(err => {
215 | return goto("/auth", true)
216 | })
217 | } else {
218 | return goto("/auth", true)
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/assets/setting/package.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { logging } from "../logging.js";
3 | import { goto } from "../route.js";
4 |
5 | export function packageListOption(e){
6 | if(!window.panel){
7 | return getPanel(packageListOption)
8 | }
9 | doc.createElement("div")
10 | .then(div => {
11 | div.id = "packageListOption";
12 | div.classList.add("settingBox");
13 | // 标题
14 | doc.createElement("p")
15 | .then(p => {
16 | p.innerText = "黑/白名单";
17 | p.addEventListener("click", () => {
18 | goto("/setting");
19 | })
20 | div.append(p);
21 | });
22 | panel.list().then(json => json.json())
23 | .then(json => {
24 | // 总开关
25 | doc.createElement("div")
26 | .then(option => {
27 | doc.createElement("p")
28 | .then(p => {
29 | p.classList.add("settingName")
30 | p.innerText = "总开关";
31 | option.append(p);
32 | });
33 | doc.createElement("div")
34 | .then(labelBox => {
35 | doc.createElement("input")
36 | .then(input => {
37 | input.id = "listChecked"
38 | input.type = "checkbox";
39 | input.checked = json.enabled ? true : false;
40 | input.addEventListener("change", event => {
41 | setTimeout(() => {
42 | input.checked = json.enabled ? true : false;
43 | }, 500);
44 | });
45 | input.setAttribute("style", "width: 0; height: 0;");
46 | labelBox.append(input);
47 | });
48 | doc.createElement("label")
49 | .then(label => {
50 | label.htmlFor = "listChecked";
51 | label.classList.add("toggle");
52 | labelBox.append(label);
53 | });
54 | option.append(labelBox);
55 | });
56 | div.append(option)
57 | });
58 | // 黑白名单
59 | let package_list = json.mode == "white" ? json.white : json.black;
60 | doc.createElement("div")
61 | .then(option => {
62 | doc.createElement("p")
63 | .then(p => {
64 | p.classList.add("settingName")
65 | p.innerText = "黑名单/白名单";
66 | option.append(p);
67 | });
68 | doc.createElement("div")
69 | .then(labelBox => {
70 | doc.createElement("input")
71 | .then(input => {
72 | input.id = "listModeChecked"
73 | input.type = "checkbox";
74 | input.checked = json.mode == "white" ? true : false;
75 | input.addEventListener("change", event => {
76 | panel.listMode({mode: input.checked ? "white" : "black"}, "PATCH").then(req => {
77 | if(!req.ok){
78 | alert("修改失败,请刷新重试!");
79 | setTimeout(() => input.checked = json.mode == "white" ? true : false, 500);
80 | } else {
81 | for(let p of package_list){
82 | const element = document.getElementById(p)
83 | if(element) element.checked = false;
84 | }
85 | json.mode = json.mode == "white" ? "black" : "white";
86 | package_list = json.mode == "white" ? json.white : json.black;
87 | for(let p of package_list){
88 | const element = document.getElementById(p);
89 | if(element) element.checked = true;
90 | }
91 | }
92 | }).catch(err => {
93 | logging.error(err);
94 | alert("无法连接神秘后端,请检查 node 状态!");
95 | setTimeout(() => input.checked = json.mode == "white" ? true : false, 500);
96 | });
97 | });
98 | input.setAttribute("style", "width: 0; height: 0;");
99 | labelBox.append(input);
100 | });
101 | doc.createElement("label")
102 | .then(label => {
103 | label.htmlFor = "listModeChecked";
104 | label.classList.add("toggle");
105 | labelBox.append(label);
106 | });
107 | option.append(labelBox);
108 | });
109 | div.append(option)
110 | });
111 | // 分割线
112 | doc.createElement("div")
113 | .then(PartitionLine => {
114 | PartitionLine.id = "PartitionLine";
115 | div.append(PartitionLine)
116 | })
117 | // 应用
118 | panel.labels()
119 | .then(json => json.json())
120 | .then(packages => {
121 | packages = Object.entries(packages).sort((a, b) => {
122 | const valueA = a[1].toLowerCase();
123 | const valueB = b[1].toLowerCase();
124 | if (valueA < valueB) {
125 | return -1;
126 | } else if (valueA > valueB) {
127 | return 1;
128 | } else {
129 | return 0;
130 | }
131 | }).reduce((acc, [key, value]) => {
132 | acc[key] = value;
133 | return acc;
134 | }, {});
135 | for(const pn of package_list){
136 | if(config("hideNameless") && !packages[pn]){
137 | continue
138 | }
139 | doc.createElement("div")
140 | .then(option => {
141 | doc.createElement("p")
142 | .then(p => {
143 | p.classList.add("settingName")
144 | p.innerText = packages[pn] ? packages[pn] : pn;
145 | option.append(p);
146 | });
147 | doc.createElement("div")
148 | .then(labelBox => {
149 | doc.createElement("input")
150 | .then(input => {
151 | input.id = pn;
152 | input.type = "checkbox";
153 | input.checked = true;
154 | input.addEventListener("change", event => {
155 | if(event.target.checked){
156 | package_list.push(event.target.id);
157 | } else {
158 | package_list = package_list.filter(item => item != event.target.id);
159 | }
160 | json.mode == "white" ? json.white = package_list : json.black = package_list;
161 | panel.listList({mode: json.mode, list: package_list})
162 | .then(req => {
163 | if(!req.ok){
164 | alert("失败");
165 | setTimeout(() => {
166 | event.target.checked = true;
167 | }, 500);
168 | }
169 | })
170 | });
171 | input.setAttribute("style", "width: 0; height: 0;");
172 | labelBox.append(input);
173 | });
174 | doc.createElement("label")
175 | .then(label => {
176 | label.htmlFor = pn;
177 | label.classList.add("toggle");
178 | labelBox.append(label);
179 | });
180 | option.append(labelBox);
181 | });
182 | div.append(option)
183 | });
184 | }
185 | for(let pn in packages){
186 | if(config("hideNameless") && !packages[pn]){
187 | continue
188 | }
189 | if(package_list.some(item => item == pn)) continue;
190 | doc.createElement("div")
191 | .then(option => {
192 | doc.createElement("p")
193 | .then(p => {
194 | p.classList.add("settingName")
195 | p.innerText = packages[pn] ? packages[pn] : pn;
196 | option.append(p);
197 | });
198 | doc.createElement("div")
199 | .then(labelBox => {
200 | doc.createElement("input")
201 | .then(input => {
202 | input.id = pn;
203 | input.type = "checkbox";
204 | input.checked = false;
205 | input.addEventListener("change", event => {
206 | if(event.target.checked){
207 | package_list.push(event.target.id);
208 | } else {
209 | package_list = package_list.filter(item => item != event.target.id);
210 | }
211 | json.mode == "white" ? json.white = package_list : json.black = package_list;
212 | panel.listList({mode: json.mode, list: package_list})
213 | .then(req => {
214 | if(!req.ok){
215 | alert("失败");
216 | setTimeout(() => {
217 | event.target.checked = false;
218 | }, 500);
219 | }
220 | })
221 | });
222 | input.setAttribute("style", "width: 0; height: 0;");
223 | labelBox.append(input);
224 | });
225 | doc.createElement("label")
226 | .then(label => {
227 | label.htmlFor = pn;
228 | label.classList.add("toggle");
229 | labelBox.append(label);
230 | });
231 | option.append(labelBox);
232 | });
233 | div.append(option)
234 | });
235 | }
236 | })
237 | })
238 | document.querySelector('#app').append(div);
239 | });
240 | }
241 |
--------------------------------------------------------------------------------
/assets/api.js:
--------------------------------------------------------------------------------
1 | export class maho {
2 | #api = [
3 | "/api", // 0, get
4 | "/api/log", // 1, get
5 | "/api/log/details", // 2, get
6 | "/api/kernel", // 3, get/post
7 | "/api/subs/list/:id", // 4, get
8 | "/api/subs/list", // 5, get/patch
9 | "/api/subs", // 6, put
10 | "/api/subs/:id", // 7, delete
11 | "/api/subs/infos", // 8, get/post
12 | "/api/ctx/:id", // 9, get/put
13 | "/api/proxies", // 10, get
14 | "/api/maho", // 11, get/put/patch
15 | "/api/maho/labels", // 12, get
16 | "/api/maho/list", // 13, get
17 | "/api/maho/list/mode", // 14, patch
18 | "/api/maho/list/list", // 15, patch
19 | "/api/mode", // 16, patch
20 | "/api/box", // 17, get
21 | "/api/box/log", // 18, get/put
22 | "/api/box/ntp", // 19, get/put
23 | "/api/box/dns", // 20, get/patch
24 | "/api/box/dns/fakeip", // 21, get/put
25 | "/api/box/dns/servers", // 22, get/patch/put
26 | "/api/box/dns/servers/:id", // 23, get/delete
27 | "/api/box/dns/rules", // 24, get/patch
28 | "/api/box/inbounds", // 25, get/patch
29 | "/api/box/outbounds/:id", // 26, get/delete
30 | "/api/box/outbounds", // 27, get/put/patch
31 | "/api/box/route", // 28, get/patch
32 | "/api/box/route/rules", // 29, get
33 | "/api/box/exp", // 30, get
34 | "/api/box/exp/clash", // 31, get
35 | "/api/box/exp/clash/modes", // 32, get
36 | "/api/box/exp/v2ray", // 33, get
37 | "/api/box/config" // 34, put
38 | ]
39 | #API(id, {method = "GET", data = null, promise = true, apid = null, callback = undefined} = {}){
40 | const headers = new Headers({
41 | "Content-Type": "application/json",
42 | "Authorization": this.auth
43 | });
44 | let apiPath = `http://${this.address}:${this.port}${this.#api[id]}`;
45 | if(apid){
46 | apiPath = apiPath.replace(/:id/, apid);
47 | }
48 | let req;
49 | if(method == "GET"){
50 | req = fetch(apiPath, {method: "GET", headers: headers});
51 | } else if(method == "POST"){
52 | if(data == null || data == undefined){
53 | req = fetch(apiPath, {method: "POST", headers: headers});
54 | } else {
55 | req = fetch(apiPath, {method: "POST", headers: headers, body: JSON.stringify(data)});
56 | }
57 | } else if(method == "PATCH"){
58 | if(data == null || data == undefined){
59 | req = fetch(apiPath, {method: "PATCH", headers: headers});
60 | } else {
61 | req = fetch(apiPath, {method: "PATCH", headers: headers, body: JSON.stringify(data)});
62 | }
63 | } else if(method == "PUT"){
64 | if(data == null || data == undefined){
65 | req = fetch(apiPath, {method: "PUT", headers: headers});
66 | } else {
67 | req = fetch(apiPath, {method: "PUT", headers: headers, body: JSON.stringify(data)});
68 | }
69 | } else if(method == "DELETE"){
70 | req = fetch(apiPath, {method: "DELETE", headers: headers});
71 | } else {
72 | req = fetch(apiPath, {method: "GET", headers: headers});
73 | }
74 | if(promise){
75 | return req;
76 | }
77 | req
78 | .then(response => response.text())
79 | .then(text => callback(undefined, text))
80 | .catch(err => {
81 | callback(err);
82 | });
83 | }
84 | constructor(auth = "node", address = "127.0.0.1", port = 23333) {
85 | this.auth = auth;
86 | this.address = address;
87 | this.port = port;
88 | }
89 | check(promise = true, callback = undefined){
90 | const req = fetch(`http://${this.address}:${this.port}${this.#api[0]}`, {method: "GET", headers: new Headers({
91 | "Content-Type": "application/json",
92 | "Authorization": this.auth
93 | })});
94 | if(promise){
95 | return new Promise((resolve, reject) => {
96 | req.then(r => {
97 | if(r.ok){
98 | resolve(r);
99 | } else {
100 | reject(new Error("Unauthorization"));
101 | }
102 | }).catch(err => {
103 | reject(err)
104 | })
105 | });
106 | }
107 | req.then(r => {
108 | if(r.ok){
109 | callback(true, r);
110 | } else {
111 | callback(false, r);
112 | }
113 | });
114 | }
115 | log(promise = true, callback = undefined){
116 | return this.#API(1, {method: "GET", promise: promise, callback: callback});
117 | }
118 | logDetails(promise = true, callback = undefined){
119 | return this.#API(2, {method: "GET", promise: promise, callback: callback});
120 | }
121 | kernel(data = null, method = "GET", promise = true, callback = undefined){
122 | // GET/POST
123 | return this.#API(3, {method: method, data: data, promise: promise, callback: callback});
124 | }
125 | subsListId(id, promise = true, callback = undefined){
126 | return this.#API(4, {method: "GET", promise: promise, id, callback: callback});
127 | }
128 | subsList(data = null, method = "GET", promise = true, callback = undefined){
129 | // GET/PATCH
130 | return this.#API(5, {method: method, data: data, promise: promise, callback: callback});
131 | }
132 | subs(data = null, promise = true, callback = undefined){
133 | return this.#API(6, {method: "PUT", data: data, promise: promise, callback: callback});
134 | }
135 | subsId(id, promise = true, callback = undefined){
136 | return this.#API(7, {method: "DELETE", promise: promise, id, callback: callback});
137 | }
138 | subsInfos(data = null, method = "GET", promise = true, callback = undefined){
139 | // GET/POST
140 | return this.#API(8, {method: method, data: data, promise: promise, callback: callback});
141 | }
142 | ctxId(id, data = null, promise = true, method = "GET", callback = undefined){
143 | // GET/PUT
144 | return this.#API(9, {method: method, data: data, promise: promise, id, callback: callback});
145 | }
146 | proxies(promise = true, callback = undefined){
147 | return this.#API(10, {method: "GET", promise: promise, callback: callback});
148 | }
149 | maho(data = null, method = "GET", promise = true, callback = undefined){
150 | // GET/PATCH/PUT
151 | return this.#API(11, {method: method, data: data, promise: promise, callback: callback});
152 | }
153 | labels(promise = true, callback){
154 | return this.#API(12, {method: "GET", promise: promise, callback: callback});
155 | }
156 | list(promise = true, callback){
157 | return this.#API(13, {method: "GET", promise: promise, callback: callback});
158 | }
159 | listMode(data, promise = true, callback){
160 | return this.#API(14, {method: "PATCH", data: data, promise: promise, callback: callback});
161 | }
162 | listList(data, promise = true, callback){
163 | return this.#API(15, {method: "PATCH", data: data, promise: promise, callback: callback});
164 | }
165 | mode(data, promise = true, callback){
166 | return this.#API(16, {method: "PATCH", data: data, promise: promise, callback: callback});
167 | }
168 | box(promise = true, callback){
169 | // return: 喵
170 | return this.#API(17, {method: "GET", promise: promise, callback: callback});
171 | }
172 | boxLog(data = null, method = "GET", promise = true, callback){
173 | // GET/PUT
174 | return this.#API(18, {method: method, data: data, promise: promise, callback: callback});
175 | }
176 | boxNTP(data = null, method = "GET", promise = true, callback){
177 | // GET/PUT
178 | return this.#API(19, {method: method, data: data, promise: promise, callback: callback});
179 | }
180 | boxDNS(data = null, method = "GET", promise = true, callback){
181 | // GET/PATCH
182 | return this.#API(20, {method: method, data: data, promise: promise, callback: callback});
183 | }
184 | fakeip(data = null, method = "GET", promise = true, callback){
185 | // GET/PUT
186 | return this.#API(21, {method: method, data: data, promise: promise, callback: callback});
187 | }
188 | dns(data = null, method = "GET", promise = true, callback){
189 | // GET/PUT/PATCH
190 | return this.#API(22, {method: method, data: data, promise: promise, callback: callback});
191 | }
192 | dnsID(id, data = null, method = "GET", promise = true, callback){
193 | // GET/DELETE
194 | return this.#API(23, {method: method, data: data, promise: promise, id, callback: callback});
195 | }
196 | dnsRules(data = null, method = "GET", promise = true, callback){
197 | // GET/PATCH
198 | return this.#API(24, {method: method, data: data, promise: promise, callback: callback});
199 | }
200 | inbounds(data = null, method = "GET", promise = true, callback){
201 | // GET/PATCH
202 | return this.#API(25, {method: method, data: data, promise: promise, callback: callback});
203 | }
204 | outboundsID(id, data = null, method = "GET", promise = true, callback){
205 | // GET/DELETE
206 | return this.#API(26, {method: method, data: data, promise: promise, id, callback: callback});
207 | }
208 | outbounds(data = null, method = "GET", promise = true, callback){
209 | // GET/PUT/PATCH
210 | return this.#API(27, {method: method, data: data, promise: promise, callback: callback});
211 | }
212 | route(data = null, method = "GET", promise = true, callback){
213 | // GET/PATCH
214 | return this.#API(28, {method: method, data: data, promise: promise, callback: callback});
215 | }
216 | routeRules(promise = true, callback){
217 | return this.#API(29, {method: "GET", promise: promise, callback: callback});
218 | }
219 | exp(promise = true, callback){
220 | return this.#API(30, {method: "GET", promise: promise, callback: callback});
221 | }
222 | expClash(promise = true, callback){
223 | return this.#API(31, {method: "GET", promise: promise, callback: callback});
224 | }
225 | expClashModes(promise = true, callback){
226 | return this.#API(32, {method: "GET", promise: promise, callback: callback});
227 | }
228 | expV2ray(promise = true, callback){
229 | return this.#API(33, {method: "GET", promise: promise, callback: callback});
230 | }
231 | config(data = null, promise = true, callback){
232 | return this.#API(34, {method: "PUT", data: data, promise: promise, callback: callback});
233 | }
234 | }
235 |
236 | export class clash {
237 | #api = [
238 | /*
239 | ID: 0,
240 | METHOD: GET,
241 | RETURN: JSON
242 | */
243 | "/traffic",
244 | /*
245 | ID: 1,
246 | METHOD: GET,
247 | RETURN: JSON
248 | */
249 | "/logs",
250 | /*
251 | ID: 2,
252 | METHOD: GET,
253 | RETURN: JSON
254 | */
255 | "/proxies",
256 | /*
257 | ID: 3,
258 | METHOD: GET/PUT,
259 | RETURN: JSON
260 | */
261 | "/proxies/:name",
262 | /*
263 | ID: 4,
264 | METHOD: GET,
265 | RETURN: JSON
266 | */
267 | "/proxies/:name/delay",
268 | /*
269 | ID: 5,
270 | METHOD: GET/PATCH/PUT,
271 | RETURN: JSON
272 | */
273 | "/configs",
274 | /*
275 | ID: 6,
276 | METHOD: GET,
277 | RETURN: JSON
278 | */
279 | "/rules",
280 | /*
281 | ID: 7,
282 | METHOD: GET,
283 | RETURN: JSON
284 | */
285 | "/connections"
286 | ]
287 | #API(api, {promise = true, callback = null, data = null, type = "application/json", method = "GET", rep = null, reptext = ""} = {}){
288 | const headers = new Headers({
289 | "Content-Type": type,
290 | "Authorization": "Bearer " + this.secret
291 | });
292 | let apiPath = `http://${this.address}:${this.port}${this.#api[api]}`;
293 | if(rep){
294 | apiPath = apiPath.replace(rep, reptext);
295 | }
296 | let request;
297 | switch(method){
298 | case "GET":
299 | request = fetch(apiPath, {
300 | method: method,
301 | headers: headers
302 | });
303 | break;
304 | default:
305 | break;
306 | }
307 | if(promise){
308 | return request;
309 | }
310 | request.then(req => req.text())
311 | .then(text => callback(undefined, text))
312 | .catch(err => callback(err));
313 | }
314 | constructor(secret = "singBox", address = "127.0.0.1", port = 9909) {
315 | this.secret = secret;
316 | this.address = address;
317 | this.port = port;
318 | this.lastStatusTimes = 0;
319 | }
320 | connections(){
321 | return this.#API(7);
322 | }
323 | }
324 |
325 | export class v2ray {
326 | #api = []
327 | constructor(address = "127.0.0.1", port = 9909) {
328 | this.address = address;
329 | this.port = port;
330 | }
331 | }
--------------------------------------------------------------------------------
/assets/pages/home.js:
--------------------------------------------------------------------------------
1 | import { clash } from "../api.js";
2 | import { doc, config, toMemory, getPanel } from "../document.js";
3 | import { goto } from "../route.js";
4 | import { logging } from "../logging.js";
5 | import { processTasks, SuperTask, runTask, makeWorker } from "../task.js";
6 | import { verifyAuthorizationCode } from "../document.js";
7 |
8 | function generationLog(log){
9 | return new Promise(resolve => {
10 | doc.createElement("p")
11 | .then(p => {
12 | p.id = "log_" + log.id.split("-").join("");
13 | let logTime = log.time;
14 | let logType = log.level;
15 | let logText = log.contents[0];
16 | doc.createElement("span")
17 | .then(span => {
18 | span.classList.add("logSpace");
19 | switch(logType){
20 | case "info":
21 | span.innerText = "INFO";
22 | span.classList.add("infoLog");
23 | break;
24 | case "warn":
25 | span.innerText = "WARN";
26 | span.classList.add("warnLog");
27 | break;
28 | case " err":
29 | span.innerText = "ERRO";
30 | span.classList.add("errLog");
31 | break;
32 | default:
33 | break;
34 | }
35 | p.append("[" + logTime + "]", span, logText);
36 | });
37 | resolve(p);
38 | });
39 | });
40 | }
41 |
42 | const statusTable = {
43 | "working": "工作中",
44 | "starting": "启动中",
45 | "restarting": "重启中",
46 | "stopping": "停止中",
47 | "stopped": "已停止"
48 | }
49 |
50 | const buttonStatusTable = {
51 | "working": [true, false, false],
52 | "starting": [true, true, true],
53 | "restarting": [true, true, true],
54 | "stopping": [true, true, true],
55 | "stopped": [false, true, true]
56 | }
57 |
58 | function buttonSwitchStatus(select, animation, enable, timeout = 1000){
59 | let s = doc.query(select);
60 | if(s.disabled != enable){
61 | s.disabled = enable;
62 | }
63 | }
64 |
65 | function updateStatus(status, runMode, apMode, cpu, button = []){
66 | doc.query("#status").innerText = "神秘状态: " + statusTable[status];
67 | doc.query("#runMode").innerText = "运行模式: " + (runMode ? runMode : "?")
68 | doc.query("#apMode").innerText = "热点模式: " + (apMode == true ? "已开启" : apMode == false ? "已关闭" : "?")
69 | doc.query("#cpu").innerText = "CPU占用率: " + (cpu ? cpu + "%" : 0 + "%")
70 | buttonSwitchStatus("#start", "buttonDisable", button[0] ? button[0] : false, 500);
71 | buttonSwitchStatus("#restart", "buttonDisable", button[1] ? button[1] : false, 500);
72 | buttonSwitchStatus("#stop", "buttonDisable", button[2] ? button[2] : false, 500);
73 | }
74 |
75 | function setDrag(father, children){
76 | for(const child of children){
77 | child.setAttribute("draggable", true);
78 | }
79 |
80 | }
81 |
82 | function createSubInfo(cls, icon, data, father){
83 | doc.createElement("p", {class: [cls]}, father)
84 | .then(subinfo => {
85 | doc.createElement("span", {class: ["infoIcon"], innerText: icon}, subinfo)
86 | doc.createElement("span", {class: ["infoText"], innerText: data}, subinfo)
87 | });
88 | }
89 |
90 | function createContainer(info, father){
91 | const expData = info.subInfo.info.expire == 0 ? "不限时" : (function(){
92 | const date = new Date(info.subInfo.info.expire * 1000);
93 | return `${String(date.getFullYear()).slice(2)}/${date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1}/${date.getDate() < 10 ? '0' + date.getDate() : date.getDate()}`;
94 | })()
95 | const date2 = new Date(info.subInfo.timeStamp);
96 | const updData = `${date2.getHours() < 10 ? '0' + date2.getHours() : date2.getHours()}:${date2.getMinutes() < 10 ? '0' + date2.getMinutes() : date2.getMinutes()}`;
97 | createSubInfo("uploadFlowRate", "↑", toMemory(info.subInfo.info.upload), father)
98 | createSubInfo("downloadFlowRate", "↓", toMemory(info.subInfo.info.download), father)
99 | createSubInfo("usedFlowRate", "⇵", toMemory(info.subInfo.info.upload + info.subInfo.info.download), father)
100 | createSubInfo("totalFlowRate", "◔", toMemory(info.subInfo.info.total), father)
101 | createSubInfo("expireDate", "↹", expData, father)
102 | createSubInfo("updateTime", "↺", updData, father)
103 | }
104 |
105 | function refreshProviders(infos){
106 | const subs = doc.query("#subs")
107 | const subList = []
108 | for(let i of infos.providers){
109 | doc.createElement("div", {class: ["subs"]}, subs)
110 | .then(div => {
111 | subList.push(div)
112 | doc.createElement("p", {innerText: i.name, class: ["subsTitle"]}, div)
113 | doc.createElement("div", {class: ["container"]}, div)
114 | .then(container => {
115 | if(i.type == "remote" && i.subInfo.support){
116 | createContainer(i, container)
117 | } else if(i.type == "remote" && !i.subInfo.support){
118 | doc.createElement("p", {innerText: "无法查询使用情况", class: ["httpFlowRate"]}, container)
119 | const date = new Date(i.subInfo.timeStamp);
120 | createSubInfo("httpUpdateTime", "↺", `${date.getHours() < 10 ? '0' + date.getHours() : date.getHours()}:${date.getMinutes() < 10 ? '0' + date.getMinutes() : date.getMinutes()}`, container)
121 | } else {
122 | doc.createElement("p", {innerText: "本地配置", class: ["fileFlowRate"]}, container)
123 | }
124 | })
125 | })
126 | }
127 | if(config("subsOrder")){
128 | setDrag(subs, doc.query(".subs"))
129 | }
130 | }
131 |
132 | function refreshStatus(){
133 | panel.kernel()
134 | .then(json => json.json())
135 | .then(status => {
136 | if(status.status === "working") {
137 | window.clashapi = new clash(status.secret)
138 | try {
139 | clashapi.connections().then(req => req.json()).then(connects => {
140 | doc.query("#res").innerText = `内存占用: ${toMemory(connects.memory)}`;
141 | doc.query("#connect").innerText = `连接数量: ${connects.connections != undefined ? connects.connections.length : 0}`;
142 | if(window.uploadTotal != undefined && window.downloadTotal){
143 | doc.query("#speedUpload").innerText = `${toMemory(connects.uploadTotal - window.uploadTotal)}`;
144 | doc.query("#speedDownload").innerText = `${toMemory(connects.downloadTotal - window.downloadTotal)}`;
145 | }
146 | window.uploadTotal = connects.uploadTotal;
147 | window.downloadTotal = connects.downloadTotal;
148 | })
149 | } catch(err){
150 | doc.query("#res").innerText = "内存占用: 0B";
151 | doc.query("#connect").innerText = "连接数量: 0";
152 | doc.query("#speedUpload").innerText = "0B";
153 | doc.query("#speedDownload").innerText = "0B";
154 | }
155 | } else {
156 | doc.query("#res").innerText = "内存占用: 0B";
157 | doc.query("#connect").innerText = "连接数量: 0";
158 | doc.query("#speedUpload").innerText = "0B";
159 | doc.query("#speedDownload").innerText = "0B";
160 | }
161 | updateStatus(status.status, status.workMode, status.apMode, status.cpu, buttonStatusTable[status.status])
162 | })
163 | .catch(err => {
164 | return;
165 | })
166 | }
167 |
168 | export function index(){
169 | if(!window.panel){
170 | return getPanel(index)
171 | }
172 | // 第一个信息栏
173 | doc.createElement("div", {id: "info", class: ["outside"]}, doc.query("#app"))
174 | .then(div => {
175 | doc.createElement("div", {class: ["img_box"]}, div)
176 | .then(div2 => {
177 | doc.createElement("img", {src: "/images/maho.gif", onclick: () => {goto("/setting")}}, div2)
178 | })
179 | doc.createElement("div", {class: ["controlCenter"]}, div)
180 | .then(div2 => {
181 | doc.createElement("div", {id: "statusbar", class: ["statusbar"]}, div2)
182 | .then(div3 => {
183 | doc.createElement("div", null, div3)
184 | .then(div4 => {
185 | doc.createElement("p", {id: "status", innerText: "神秘状态: ?"}, div4)
186 | doc.createElement("p", {id: "runMode", innerText: "运行模式: ?"}, div4)
187 | doc.createElement("p", {id: "res", innerText: "内存占用: ?"}, div4)
188 | doc.createElement("p", {id: "cpu", innerText: "CPU占用率: 0%"}, div4)
189 | doc.createElement("p", {id: "connect", innerText: "连接数量: 0"}, div4)
190 | doc.createElement("p", {id: "apMode", innerText: "热点模式: ?"}, div4)
191 | })
192 | doc.createElement("div", {id: "controller", class: ["controller"]}, div2)
193 | .then(div3 => {
194 | doc.createElement("button", {id: "start", class: ["button"], innerText: "启动"}, div3)
195 | .then(button => {
196 | button.addEventListener("click", (event) => {
197 | panel.kernel({method: "start"}, "POST").then(req => req.text()).then(text => logging.info(text)).catch(err => console.log(err));
198 | });
199 | })
200 | doc.createElement("button", {id: "restart", class: ["button"], innerText: "重启"}, div3)
201 | .then(button => {
202 | button.addEventListener("click", () => {
203 | panel.kernel({method: "restart"}, "POST").then(req => req.text()).then(text => logging.info(text)).catch(err => console.log(err));
204 | });
205 | });
206 | doc.createElement("button", {id: "stop", class: ["button"], innerText: "停止"}, div3)
207 | .then(button => {
208 | button.addEventListener("click", () => {
209 | panel.kernel({method: "stop"}, "POST").then(req => req.text()).then(text => logging.info(text)).catch(err => console.log(err));
210 | });
211 | })
212 | });
213 | });
214 | doc.createElement("div", {id: "speed", class: ["speed"]}, div)
215 | .then(speed => {
216 | if(!config("speed")) speed.style.display = "none";
217 | doc.createElement("p", null, speed)
218 | .then(p => {
219 | doc.createElement("span", {innerText: "↑", class: ["speedIcon"]}, p)
220 | doc.createElement("span", {innerText: "0.00B", class: ["speedText"], id: "speedUpload"}, p)
221 | });
222 | doc.createElement("p", null, speed)
223 | .then(p => {
224 | doc.createElement("span", {innerText: "↓", class: ["speedIcon"]}, p)
225 | doc.createElement("span", {innerText: "0.00B", class: ["speedText"], id: "speedDownload"}, p)
226 | })
227 | })
228 | })
229 | })
230 | // 第二个信息栏
231 | doc.createElement("div", {id: "subs", class: ["outside"]}, document.getElementById("app"))
232 | .then(div => {
233 | doc.createElement("p", {id: "jinrishici-sentence", innerText: "点我拉取所有机场", class: ["yiyan"]}, div)
234 | .then(p => {
235 | if(!config("YiYan")) p.style.display = "none";
236 | p.addEventListener("click", event => {
237 | panel.subsInfos({name: "all"}, "POST").then(req => req.text()).then(text => logging.info(text)).catch(err => console.log(err));
238 | });
239 | if(!document.querySelector('#yiyan')){
240 | doc.createElement("script")
241 | .then(s => {
242 | s.src = "https://sdk.jinrishici.com/v2/browser/jinrishici.js";
243 | s.id = "yiyan"
244 | document.body.append(s);
245 | });
246 | }
247 | })
248 | window.jinrishiciInterval = setInterval(() => {
249 | if(window.jirishici == undefined) return;
250 | if(location.pathname !=="/") clearInterval(window.jinrishiciInterval)
251 | jinrishici.load(result => {
252 | doc.query("#jinrishici-sentence").innerText = result.data.content;
253 | });
254 | }, 30000);
255 | })
256 | panel.subsInfos().then(json => json.json()).then(infos => {
257 | refreshProviders(infos);
258 | }).catch(err => console.error(err))
259 | // log 显示栏
260 | doc.createElement("div", {id: "log", class: ["outside"]}, document.getElementById("app"))
261 | .then(div => {
262 | if(!config("log")) div.style.display = "none";
263 | doc.createElement("div", {id: "logBox", class: ["logs"]}, div)
264 | .then(log => {
265 | panel.logDetails().then(req => req.json()).then(json => {
266 | for(let l of json){
267 | generationLog(l).then(p => log.append(p))
268 | }
269 | }).catch(err => console.error(err))
270 | window.logUpdateInterval = setInterval(() => {
271 | if(location.pathname != "/"){
272 | clearInterval(window.logUpdateInterval)
273 | return
274 | }
275 | panel.log().then(req => req.json()).then(text => {
276 | if(window.logUpdatetimeStamp != text){
277 | window.logUpdatetimeStamp = text;
278 | panel.logDetails().then(req => req.json()).then(json => {
279 | for(let l of json){
280 | if(doc.query(`#log_${l.id.split("-").join("")}`).length == 0){
281 | generationLog(l)
282 | .then(p => {
283 | log.append(p);
284 | log.scrollTop = log.scrollHeight + log.offsetHeight;
285 | })
286 | }
287 | }
288 | }).catch(err => {
289 | console.log(err)
290 | });
291 | log.scrollTop = log.scrollHeight + log.offsetHeight;
292 | }
293 | }).catch(err => {
294 | logging.error(err);
295 | });
296 | }, 1000);
297 | })
298 | })
299 | // 刷新第一个信息栏
300 | refreshStatus();
301 | window.refreshStatusInterval = setInterval(() => {
302 | if(location.pathname != "/"){
303 | clearInterval(window.refreshStatusInterval)
304 | return
305 | }
306 | refreshStatus();
307 | }, 1000);
308 | }
309 |
--------------------------------------------------------------------------------
/assets/setting/mystery.js:
--------------------------------------------------------------------------------
1 | import { doc, config, getPanel } from "../document.js";
2 | import { maho } from "../api.js";
3 | import { logging } from "../logging.js";
4 | import { goto } from "../route.js";
5 |
6 | export function mystery(){
7 | if(!window.panel){
8 | return getPanel(mystery)
9 | }
10 | if(document.getElementById("settingBox")) return;
11 | panel.maho().then(req => req.json()).then(json => {
12 | doc.createElement("div")
13 | .then(div => {
14 | div.id = "settingBox";
15 | div.classList.add("settingBox");
16 | doc.createElement("p")
17 | .then(p => {
18 | p.innerText = "神秘";
19 | p.addEventListener("click", (event) => {
20 | div.setAttribute("style", "animation: FadeOut 0.1s ease-in forwards;");
21 | setTimeout(() => {
22 | goto("/setting");
23 | }, 100);
24 | });
25 | div.append(p);
26 | });
27 | if(json.platform == "android"){
28 | doc.createElement("div")
29 | .then(option => {
30 | option.id = "apModeBox"; // 热点模式
31 | doc.createElement("p")
32 | .then(p => {
33 | p.id = "apModeName";
34 | p.classList.add("settingName")
35 | p.innerText = "热点模式";
36 | option.append(p);
37 | });
38 | doc.createElement("div")
39 | .then(labelBox => {
40 | labelBox.id = "apModeToggle";
41 | doc.createElement("input")
42 | .then(input => {
43 | input.id = "apModeChecked"
44 | input.type = "checkbox";
45 | input.checked = json.apMode.enabled;
46 | input.addEventListener("change", (event) => {
47 | // 热点模式设置逻辑
48 | let lastChecked = input.checked;
49 | panel.maho({"apMode.enabled": input.checked}, "PATCH").then(req => {
50 | if(!req.ok) setTimeout(() => {
51 | input.checked = lastChecked;
52 | }, 500);
53 | }).catch(err => {
54 | logging.error(err);
55 | setTimeout(() => {
56 | input.checked = lastChecked;
57 | }, 500);
58 | });
59 | });
60 | input.setAttribute("style", "width: 0; height: 0;");
61 | labelBox.append(input);
62 | });
63 | doc.createElement("label")
64 | .then(label => {
65 | label.htmlFor = "apModeChecked";
66 | label.classList.add("toggle");
67 | labelBox.append(label);
68 | });
69 | option.append(labelBox);
70 | });
71 | div.append(option)
72 | });
73 | doc.createElement("div")
74 | .then(option => {
75 | option.id = "compatibilityModeBox"; // 兼容模式
76 | doc.createElement("p")
77 | .then(p => {
78 | p.id = "compatibilityModeName";
79 | p.classList.add("settingName")
80 | p.innerText = "兼容模式";
81 | option.append(p);
82 | });
83 | doc.createElement("div")
84 | .then(labelBox => {
85 | labelBox.id = "compatibilityModeToggle";
86 | doc.createElement("input")
87 | .then(input => {
88 | input.id = "compatibilityModeChecked"
89 | input.type = "checkbox";
90 | input.checked = json.apMode.compatibilityMode;
91 | input.addEventListener("change", (event) => {
92 | // 兼容模式设置逻辑
93 | let lastChecked = input.checked;
94 | panel.maho({"apMode.compatibilityMode": input.checked}, "PATCH").then(req => {
95 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
96 | }).catch(err => {
97 | logging.error(err);
98 | setTimeout(() => input.checked = lastChecked, 500);
99 | });
100 | });
101 | input.setAttribute("style", "width: 0; height: 0;");
102 | labelBox.append(input);
103 | });
104 | doc.createElement("label")
105 | .then(label => {
106 | label.htmlFor = "compatibilityModeChecked";
107 | label.classList.add("toggle");
108 | labelBox.append(label);
109 | });
110 | option.append(labelBox);
111 | });
112 | div.append(option)
113 | });
114 | }
115 | doc.createElement("div")
116 | .then(option => {
117 | option.id = "bindHost"; // 绑定地址
118 | doc.createElement("p")
119 | .then(p => {
120 | p.id = "bindHostName";
121 | p.classList.add("settingName")
122 | p.innerText = "全局访问";
123 | option.append(p);
124 | });
125 | doc.createElement("div")
126 | .then(labelBox => {
127 | doc.createElement("input")
128 | .then(input => {
129 | input.id = "bindHostChecked"
130 | input.type = "checkbox";
131 | input.checked = json.bindHost == "127.0.0.1" ? false : true;
132 | input.addEventListener("change", (event) => {
133 | // 全局访问
134 | let lastValue = input.checked;
135 | panel.maho({"bindHost": input.checked ? "0.0.0.0" : "127.0.0.1"}, "PATCH").then(req => {
136 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
137 | }).catch(err => {
138 | logging.error(err);
139 | setTimeout(() => input.checked = lastChecked, 500);
140 | });
141 | });
142 | input.setAttribute("style", "width: 0; height: 0;");
143 | labelBox.append(input);
144 | });
145 | doc.createElement("label")
146 | .then(label => {
147 | label.htmlFor = "bindHostChecked";
148 | label.classList.add("toggle");
149 | labelBox.append(label);
150 | });
151 | option.append(labelBox);
152 | });
153 | div.append(option)
154 | });
155 | doc.createElement("div")
156 | .then(option => {
157 | option.id = "startWithSingBox"; // 核心开机自启
158 | doc.createElement("p")
159 | .then(p => {
160 | p.id = "startWithSingBoxName";
161 | p.classList.add("settingName")
162 | p.innerText = "核心开机自启";
163 | option.append(p);
164 | });
165 | doc.createElement("div")
166 | .then(labelBox => {
167 | labelBox.id = "startWithSingBoxToggle";
168 | doc.createElement("input")
169 | .then(input => {
170 | input.id = "startWithSingBoxChecked"
171 | input.type = "checkbox";
172 | input.checked = json.startWithSingBox;
173 | input.addEventListener("change", (event) => {
174 | // 开机自启设置逻辑
175 | let lastValue = input.checked;
176 | panel.maho({"startWithSingBox": input.checked}, "PATCH").then(req => {
177 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
178 | }).catch(err => {
179 | logging.error(err);
180 | setTimeout(() => input.checked = lastChecked, 500);
181 | });
182 | });
183 | input.setAttribute("style", "width: 0; height: 0;");
184 | labelBox.append(input);
185 | });
186 | doc.createElement("label")
187 | .then(label => {
188 | label.htmlFor = "startWithSingBoxChecked";
189 | label.classList.add("toggle");
190 | labelBox.append(label);
191 | });
192 | option.append(labelBox);
193 | });
194 | div.append(option)
195 | });
196 | doc.createElement("div")
197 | .then(option => {
198 | option.id = "autoDownSub"; // 自动更新订阅
199 | doc.createElement("p")
200 | .then(p => {
201 | p.id = "autoDownSubName";
202 | p.classList.add("settingName")
203 | p.innerText = "自动更新订阅";
204 | option.append(p);
205 | });
206 | doc.createElement("div")
207 | .then(labelBox => {
208 | labelBox.id = "autoDownSubToggle";
209 | doc.createElement("input")
210 | .then(input => {
211 | input.id = "autoDownSubChecked"
212 | input.type = "checkbox";
213 | input.checked = json.autoDownSub;
214 | input.addEventListener("change", (event) => {
215 | // 自动更新订阅设置逻辑
216 | let lastValue = input.checked;
217 | panel.maho({"autoDownSub": input.checked}, "PATCH").then(req => {
218 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
219 | }).catch(err => {
220 | logging.error(err);
221 | setTimeout(() => input.checked = lastChecked, 500);
222 | });
223 | });
224 | input.setAttribute("style", "width: 0; height: 0;");
225 | labelBox.append(input);
226 | });
227 | doc.createElement("label")
228 | .then(label => {
229 | label.htmlFor = "autoDownSubChecked";
230 | label.classList.add("toggle");
231 | labelBox.append(label);
232 | });
233 | option.append(labelBox);
234 | });
235 | div.append(option)
236 | });
237 | doc.createElement("div")
238 | .then(option => {
239 | option.id = "autoDownGeo"; // 自动更新 Geo 数据库
240 | doc.createElement("p")
241 | .then(p => {
242 | p.id = "autoDownGeoName";
243 | p.classList.add("settingName")
244 | p.innerText = "自动更新地理数据库";
245 | option.append(p);
246 | });
247 | doc.createElement("div")
248 | .then(labelBox => {
249 | labelBox.id = "autoDownGeoToggle";
250 | doc.createElement("input")
251 | .then(input => {
252 | input.id = "autoDownGeoChecked"
253 | input.type = "checkbox";
254 | input.checked = json.autoDownGeo.enabled;
255 | input.addEventListener("change", (event) => {
256 | // 自动更新 Geo 数据库设置逻辑
257 | let lastValue = input.checked;
258 | panel.maho({"autoDownGeo.enabled": input.checked}, "PATCH").then(req => {
259 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
260 | }).catch(err => {
261 | logging.error(err);
262 | setTimeout(() => input.checked = lastChecked, 500);
263 | });
264 | });
265 | input.setAttribute("style", "width: 0; height: 0;");
266 | labelBox.append(input);
267 | });
268 | doc.createElement("label")
269 | .then(label => {
270 | label.htmlFor = "autoDownGeoChecked";
271 | label.classList.add("toggle");
272 | labelBox.append(label);
273 | });
274 | option.append(labelBox);
275 | });
276 | div.append(option)
277 | });
278 | doc.createElement("div")
279 | .then(option => {
280 | option.id = "immediateProtect"; // 自动更新订阅
281 | doc.createElement("p")
282 | .then(p => {
283 | p.classList.add("settingName")
284 | p.innerText = "核心启动立即守护";
285 | option.append(p);
286 | });
287 | doc.createElement("div")
288 | .then(labelBox => {
289 | doc.createElement("input")
290 | .then(input => {
291 | input.id = "immediateProtectChecked"
292 | input.type = "checkbox";
293 | input.checked = json.immediateProtect;
294 | input.addEventListener("change", (event) => {
295 | // 自动更新订阅设置逻辑
296 | let lastValue = input.checked;
297 | panel.maho({"immediateProtect": input.checked}, "PATCH").then(req => {
298 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
299 | }).catch(err => {
300 | logging.error(err);
301 | setTimeout(() => input.checked = lastChecked, 500);
302 | });
303 | });
304 | input.setAttribute("style", "width: 0; height: 0;");
305 | labelBox.append(input);
306 | });
307 | doc.createElement("label")
308 | .then(label => {
309 | label.htmlFor = "immediateProtectChecked";
310 | label.classList.add("toggle");
311 | labelBox.append(label);
312 | });
313 | option.append(labelBox);
314 | });
315 | div.append(option)
316 | });
317 | doc.createElement("div")
318 | .then(option => {
319 | option.id = "mode"; // 代理分流模式
320 | doc.createElement("p")
321 | .then(p => {
322 | p.id = "modeName";
323 | p.classList.add("settingName")
324 | p.innerText = "代理连接模式";
325 | option.append(p);
326 | });
327 | doc.createElement("div")
328 | .then(modeForm => {
329 | modeForm.id = "modeForm"; //
330 | modeForm.classList.add("modeForm");
331 | doc.createElement("select")
332 | .then(select => {
333 | select.addEventListener("change", event => {
334 | // 代理分流模式设置逻辑
335 | let lastValue = select.value;
336 | panel.maho({"mode": select.value}, "PATCH").then(req => {
337 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
338 | }).catch(err => {
339 | logging.error(err);
340 | setTimeout(() => input.checked = lastChecked, 500);
341 | });
342 | });
343 | panel.expClashModes(true)
344 | .then(response => response.json())
345 | .then(modes => {
346 | for(let mode of modes){
347 | doc.createElement("option")
348 | .then(option2 => {
349 | option2.innerText = mode;
350 | option2.value = mode;
351 | if(json.mode == mode) option2.selected = true;
352 | select.append(option2);
353 | });
354 | }
355 | })
356 | modeForm.append(select);
357 | });
358 | option.append(modeForm);
359 | });
360 | div.append(option)
361 | });
362 | doc.createElement("div")
363 | .then(option => {
364 | option.id = "authorizationKey"; // 授权码
365 | doc.createElement("p")
366 | .then(p => {
367 | p.id = "authorizationKeyName";
368 | p.classList.add("settingName");
369 | p.innerText = "后端授权码";
370 | option.append(p);
371 | });
372 | doc.createElement("div")
373 | .then(auth => {
374 | auth.id = "authorizationKeyBox";
375 | doc.createElement("input")
376 | .then(input => {
377 | input.id = "authorizationKeyInput";
378 | input.type = "text";
379 | input.value = json.authorizationKey;
380 | input.addEventListener("keypress", event => {
381 | // 授权码更新逻辑
382 | let lastValue = input.value;
383 | panel.maho({"authorizationKey": input.value}, "PATCH").then(req => {
384 | if(!req.ok){
385 | setTimeout(() => input.checked = lastChecked, 500);
386 | } else {
387 | localStorage.auth = input.value;
388 | window.panel = new maho(localStorage.auth);
389 | }
390 | }).catch(err => {
391 | logging.error(err);
392 | setTimeout(() => input.checked = lastChecked, 500);
393 | });
394 | });
395 | auth.append(input);
396 | });
397 | option.append(auth);
398 | });
399 | div.append(option)
400 | });
401 | doc.createElement("div")
402 | .then(option => {
403 | option.id = "boxConfig";
404 | doc.createElement("p")
405 | .then(p => {
406 | p.id = "boxConfigName";
407 | p.classList.add("settingName");
408 | p.innerText = "重载配置响应修改";
409 | option.append(p);
410 | });
411 | doc.createElement("div")
412 | .then(func => {
413 | func.id = "boxConfigBox";
414 | doc.createElement("button")
415 | .then(button => {
416 | button.id = "boxConfigButton";
417 | button.type = "button";
418 | button.innerText = "重载";
419 | button.addEventListener("click", event => {
420 | // 重载基础术式逻辑
421 | panel.config().then(req => {
422 | if(!req.ok) alert("重载基础术式失败: " + response.status + ", 请刷新网页重试");
423 | }).catch(err => {
424 | logging.error(err);
425 | alert("重载基础术式失败: 无法连接到神秘后端");
426 | });
427 | });
428 | func.append(button);
429 | });
430 | option.append(func);
431 | });
432 | div.append(option)
433 | });
434 | doc.createElement("div")
435 | .then(option => {
436 | option.id = "createConfig";
437 | doc.createElement("p")
438 | .then(p => {
439 | p.id = "createConfigName";
440 | p.classList.add("settingName");
441 | p.innerText = "生成分享配置";
442 | option.append(p);
443 | });
444 | doc.createElement("div")
445 | .then(func => {
446 | func.id = "createConfigBox";
447 | doc.createElement("button")
448 | .then(button => {
449 | button.id = "createConfigButton";
450 | button.type = "button";
451 | button.innerText = "生成";
452 | button.addEventListener("click", event => {
453 | // 生成兼容配置逻辑
454 | panel.maho(null, "PUT").then(req => {
455 | if(!req.ok) alert("生成分享配置失败: " + response.status + ", 请刷新网页重试");
456 | }).catch(err => {
457 | logging.error(err);
458 | alert("生成分享配置失败: 无法连接到神秘后端");
459 | });
460 | });
461 | func.append(button);
462 | });
463 | option.append(func);
464 | });
465 | div.append(option)
466 | });
467 | doc.createElement("div")
468 | .then(option => {
469 | option.id = "ipv6"; // 开启IPv6
470 | doc.createElement("p")
471 | .then(p => {
472 | p.id = "ipv6Name";
473 | p.classList.add("settingName")
474 | p.innerText = "开启IPv6";
475 | option.append(p);
476 | });
477 | doc.createElement("div")
478 | .then(labelBox => {
479 | doc.createElement("input")
480 | .then(input => {
481 | input.id = "ipv6Checked"
482 | input.type = "checkbox";
483 | input.checked = json.ipv6;
484 | input.addEventListener("change", (event) => {
485 | // 全局访问
486 | let lastValue = input.checked;
487 | panel.maho({"ipv6": input.checked}, "PATCH").then(req => {
488 | if(!req.ok) setTimeout(() => input.checked = lastChecked, 500);
489 | }).catch(err => {
490 | logging.error(err);
491 | setTimeout(() => input.checked = lastChecked, 500);
492 | });
493 | });
494 | input.setAttribute("style", "width: 0; height: 0;");
495 | labelBox.append(input);
496 | });
497 | doc.createElement("label")
498 | .then(label => {
499 | label.htmlFor = "ipv6Checked";
500 | label.classList.add("toggle");
501 | labelBox.append(label);
502 | });
503 | option.append(labelBox);
504 | });
505 | div.append(option)
506 | });
507 | document.querySelector('#app').append(div);
508 | });
509 | }).catch(err => {
510 | logging.error(ert);
511 | alert("连接错误: ", err);
512 | });
513 | }
514 |
--------------------------------------------------------------------------------
/assets/index.css:
--------------------------------------------------------------------------------
1 | /* 网页初始化 */
2 | @font-face {
3 | font-family:CascadiaMono;
4 | src:url(/fonts/CascadiaMono.ttf);
5 | font-weight:400;
6 | font-style:normal
7 | }
8 | @font-face {
9 | font-family:CascadiaMono;
10 | src:url(/fonts/CascadiaMonoPL.ttf);
11 | font-weight:700;
12 | font-style:normal
13 | }
14 | @font-face {
15 | font-family:CascadiaMono;
16 | src:url(/fonts/CascadiaMonoItalic.ttf);
17 | font-weight:400;
18 | font-style:italic
19 | }
20 | @font-face {
21 | font-family:CascadiaMono;
22 | src:url(/fonts/CascadiaMonoPLItalic.ttf);
23 | font-weight:700;
24 | font-style:italic
25 | }
26 | @font-face {
27 | font-family:GoogleSansText;
28 | src:url(/fonts/GoogleSansText-Regular.ttf);
29 | font-weight:400;
30 | font-style:normal
31 | }
32 | @font-face {
33 | font-family:GoogleSansText;
34 | src:url(/fonts/GoogleSansText-Bold.ttf);
35 | font-weight:700;
36 | font-style:normal
37 | }
38 | @font-face {
39 | font-family:GoogleSansText;
40 | src:url(/fonts/GoogleSansText-Italic.ttf);
41 | font-weight:400;
42 | font-style:italic
43 | }
44 | @font-face {
45 | font-family:GoogleSansText;
46 | src:url(/fonts/GoogleSansText-BoldItalic.ttf);
47 | font-weight:700;
48 | font-style:italic
49 | }
50 | * {
51 | margin: 0;
52 | padding: 0;
53 | font-family: GoogleSansText;
54 | }
55 | :root {
56 | /* Color */
57 | --main-color: #66ccff;
58 | --light-color: #66ccff;
59 | --border-color: #66ccff;
60 | --button-color: #ffffff;
61 | --button-background-color: #66ccff;
62 | --button-disable-color: #111111;
63 | --button-disable-background-color: #E0E0E0;
64 | --transparent-background-color: #ffffff40;
65 | --info-color: #ffffff;
66 | --info-background-color: #00ff00;
67 | --warn-color: #ffffff;
68 | --warn-background-color: #ffa500;
69 | --err-color: #ffffff;
70 | --err-background-color: #ff0000;
71 | --input-bottom-color: #66ccff;
72 | --select-input-bottom-color: #88eeff;
73 | --authorization-tip-color: #66ccff;
74 | --authorization-input-bottom-color: #66ccff;
75 | --authorization-select-input-bottom-color: #88eeff;
76 | --options-description-color: #878787;
77 | --options-color: #000000;
78 | --options-stroke-color: #ffffff;
79 |
80 | /* Size */
81 | --border-size: 1px;
82 | --options-stroke-size: 0.3px;
83 | }
84 | html {
85 | width: 100%;
86 | margin: 0;
87 | padding: 0;
88 | font-size: 16px;
89 | background-size: cover;
90 | -webkit-background-size: cover 100%;
91 | -o-background-size: cover 100%;
92 | background-position: center center;
93 | background-repeat: no-repeat;
94 | background-attachment: fixed;
95 | background-position: cover;
96 | /* background-image: url("/images/bg.png"); */
97 | }
98 | body {
99 | width: 100%;
100 | }
101 | /* 全局设置 */
102 | input[type="checkbox"] {
103 | visibility: hidden;
104 | }
105 | button {
106 | width: 50px;
107 | height: 25px;
108 | background-color: var(--button-background-color);
109 | color: var(--button-color);
110 | margin: 2px;
111 | border-radius: 5px;
112 | text-align: center;
113 | text-decoration: none;
114 | display: inline-block;
115 | padding: 0px 0px;
116 | border: var(--border-size) solid var(--transparent-background-color);
117 | box-shadow: 0 0 5px var(--light-color);
118 | backdrop-filter: blur(6px);
119 | transition: color 0.5s, background-color 0.5s;
120 | -webkit-user-select: none;
121 | -khtml-user-select: none;
122 | -moz-user-select: none;
123 | -ms-user-select: none;
124 | user-select: none
125 | }
126 | button:disabled {
127 | background-color: var(--button-disable-background-color);
128 | color: var(--button-disable-color);
129 | box-shadow: 0 0 5px var(--button-disable-background-color);
130 | }
131 | button:active {
132 | background-color: #44aadd;
133 | color: #dddddd;
134 | box-shadow: 0 0 5px #44aadd;
135 | }
136 | input[type="text"] {
137 | line-height: 1.1rem;
138 | font-size: 1.0rem;
139 | color: var(--main-color);
140 | text-align: center;
141 | border: none;
142 | border-bottom: 2px solid var(--input-bottom-color);
143 | background: none;
144 | outline: none;
145 | margin: 5px auto;
146 | }
147 | input[type="text"]:focus {
148 | border-bottom: 2px solid var(--select-input-bottom-color);
149 | outline: none;
150 | }
151 | input[type="number"] {
152 | line-height: 1.1rem;
153 | font-size: 1.0rem;
154 | color: white;
155 | border: none;
156 | border-bottom: 2px solid var(--input-bottom-color);
157 | background: none;
158 | outline: none;
159 | margin: 5px auto;
160 | }
161 | input[type="number"]:focus {
162 | border-bottom: 2px solid var(--select-input-bottom-color);
163 | outline: none;
164 | }
165 | .no-scroll {
166 | overflow: hidden;
167 | }
168 | /* label 开关 */
169 | .toggle {
170 | position: relative;
171 | display: inline-block;
172 | width: 50px;
173 | height: 25px;
174 | background-color: #ccc;
175 | border-radius: 20px;
176 | transition: all 0.3s;
177 | }
178 | .toggle:after {
179 | content: "";
180 | position: absolute;
181 | width: 22.5px;
182 | height: 22.5px;
183 | border-radius: 18px;
184 | background-color: #fff;
185 | top: 1px;
186 | left: 1px;
187 | transition: all 0.3s;
188 | }
189 | input[type="checkbox"]:checked+.toggle::after{
190 | transform: translateX(25px);
191 | }
192 | input[type="checkbox"]:checked+.toggle {
193 | background-color: var(--main-color);
194 | }
195 | /* select 选择 */
196 | select {
197 | cursor: pointer;
198 | padding: 3px;
199 | width: 100%;
200 | border: none;
201 | background: transparent;
202 | background-image: none;
203 | -webkit-appearance: none;
204 | -moz-appearance: none;
205 | }
206 | select:focus {
207 | outline: none;
208 | }
209 | /* app 框架 */
210 | .app {
211 | width: 100%;
212 | }
213 | /* 输入框 */
214 | .authorization {
215 | width: 90%;
216 | margin: 0 auto;
217 | text-align: center;
218 | position: relative;
219 | top: 150px;
220 | min-height: 50px;
221 | border-radius: 25px;
222 | border: var(--border-size) solid var(--border-color);
223 | box-shadow: 3px 3px 2px var(--light-color);
224 | backdrop-filter: blur(6px);
225 | opacity: 0;
226 | animation: FadeIn 0.5s ease-in forwards;
227 | }
228 | .authorization > p {
229 | color: var(--authorization-tip-color);
230 | text-align: center;
231 | width: 60%;
232 | margin: 10px auto;
233 | box-shadow: 0px 0px 5px var(--light-color);
234 | border-radius: 25px;
235 | background-color: var(--transparent-background-color);
236 | }
237 | .authorization > input {
238 | width: 65%;
239 | transition: width 0.5s;
240 | caret-color: var(--authorization-tip-color);
241 | text-align: center;
242 | color: var(--authorization-tip-color);
243 | }
244 | .authorization > input:hover {
245 | width: 70%;
246 | }
247 | /* 信息栏 */
248 | .outside {
249 | width: 95%;
250 | /* height: auto; */
251 | min-height: 30px;
252 | margin-top: 10px;
253 | margin-bottom: 10px;
254 | border-radius: 25px;
255 | display: black;
256 | margin: 15px auto;
257 | position: relative;
258 | border: var(--border-size) solid var(--border-color);
259 | box-shadow: 0px 0px 10px var(--light-color);
260 | backdrop-filter: blur(6px);
261 | }
262 | /* 第一个信息栏 */
263 | #info {
264 | margin-top: 20px;
265 | }
266 | .img_box {
267 | display: black;
268 | width: 65%;
269 | max-width: 200px;
270 | min-height: 150px;
271 | max-height: 200px;
272 | margin: 0 auto;
273 | }
274 | .img_box > img {
275 | display: grid;
276 | width: 100%;
277 | height: cover;
278 | margin: 0 auto;
279 | grid-template-columns: 1fr;
280 | animation: FadeIn 0.5s ease-in forwards;
281 | }
282 | .controlCenter {
283 | display: black;
284 | width: 90%;
285 | margin: 5% auto;
286 | min-height: 130px;
287 | max-height: 200px;
288 | border-radius: 25px;
289 | opacity: 0;
290 | animation: FadeIn 0.5s ease-in forwards;
291 | border: var(--border-size) solid var(--border-color);
292 | word-break: break-all;
293 | box-shadow: 0 0 10px var(--light-color);
294 | background-color: var(--transparent-background-color);
295 | }
296 | .statusbar {
297 | display: inline-block;
298 | width: 70%;
299 | min-height: 175px;
300 | height: auto;
301 | font-size: 1em;
302 | position: relative;
303 | vertical-align: middle;
304 | }
305 | .statusbar > div {
306 | display: black;
307 | width: 80%;
308 | height: 100%;
309 | margin: 0 auto;
310 | position: relative;
311 | top: 12.5px;
312 | }
313 | .statusbar > div > p {
314 | text-align: center;
315 | line-height: 25px;
316 | }
317 | .controller {
318 | display: inline-block;
319 | width: 30%;
320 | position: relative;
321 | top: 0;
322 | height: auto;
323 | font-size: 1em;
324 | vertical-align: middle;
325 | }
326 | .controller > button {
327 | width: 50%;
328 | margin: 10px 30px;
329 | padding: 5px;
330 | min-height: 30px;
331 | }
332 | .speed {
333 | width: 90%;
334 | margin: 10px auto;
335 | border-radius: 15px;
336 | opacity: 0;
337 | animation: FadeIn 0.5s ease-in forwards;
338 | border: var(--border-size) solid var(--border-color);
339 | word-break: break-all;
340 | box-shadow: 0 0 10px var(--light-color);
341 | background-color: var(--transparent-background-color);
342 | }
343 | .speed > p {
344 | display: inline-block;
345 | width: 50%;
346 | margin: 0 auto;
347 | text-align: center;
348 | }
349 | /* 第二个信息栏 */
350 | .yiyan {
351 | font-family: CascadiaMono;
352 | font-size: 0.8em;
353 | border-radius: 20px;
354 | border: var(--border-size) solid white;
355 | word-break: break-all;
356 | box-shadow: 0 0 10px var(--light-color);
357 | margin: 5px auto;
358 | margin-top: 20px;
359 | word-break: break-all;
360 | text-align: center;
361 | width: 90%;
362 | min-height: 20px;
363 | background-color: var(--transparent-background-color);
364 | animation: FadeIn 0.5s ease-in forwards;
365 | }
366 | #jinrishici-sentence {
367 | opacity: 0;
368 | animation: FadeIn 0.5s ease-in forwards;
369 | }
370 | #subs {
371 | max-height: 50vh;
372 | overflow: scroll;
373 | }
374 | .subs {
375 | width: 90%;
376 | min-height: 30px;
377 | margin: 15px auto;
378 | border: var(--border-size) solid var(--border-color);
379 | border-radius: 5px;
380 | box-shadow: 0px 0px 10px var(--light-color);
381 | opacity: 0;
382 | animation: FadeIn 0.5s ease-in forwards;
383 | background-color: var(--transparent-background-color);
384 | }
385 | .subsTitle {
386 | text-align: center;
387 | width: 80%;
388 | overflow: scroll;
389 | margin: 0 auto;
390 | }
391 | .container {
392 | min-height: 30px;
393 | width: 100%;
394 | }
395 | .container > p {
396 | text-align: center;
397 | }
398 | .uploadFlowRate {
399 | display: inline-block;
400 | min-height: 25px;
401 | width: 30%;
402 | margin: 2px 1.6%;
403 | }
404 | .downloadFlowRate {
405 | display: inline-block;
406 | min-height: 25px;
407 | width: 30%;
408 | margin: 2px 1.6%;
409 | }
410 | .usedFlowRate {
411 | display: inline-block;
412 | min-height: 25px;
413 | width: 30%;
414 | margin: 2px 1.6%;
415 | }
416 | .totalFlowRate {
417 | display: inline-block;
418 | min-height: 25px;
419 | width: 30%;
420 | margin: 2px 1.6%;
421 | }
422 | .totalFlowRate > .infoIcon {
423 | font-size: 1.2em;
424 | }
425 | .expireDate {
426 | display: inline-block;
427 | min-height: 25px;
428 | width: 30%;
429 | margin: 2px 1.6%;
430 | }
431 | .updateTime {
432 | display: inline-block;
433 | min-height: 25px;
434 | width: 30%;
435 | margin: 2px 1.6%;
436 | }
437 | .infoIcon {
438 | font-size: 0.8em;
439 | line-height: 25px;
440 | margin: 0 3px;
441 | width: 20%
442 | text-align: center;
443 | }
444 | .infoText {
445 | font-size: 0.8em;
446 | line-height: 25px;
447 | width: 80%;
448 | text-align: center;
449 | }
450 | .httpFlowRate {
451 | display: inline-block;
452 | min-height: 30px;
453 | width: 61%;
454 | margin: 2px 2%;
455 | }
456 | .httpUpdateTime {
457 | display: inline-block;
458 | min-height: 30px;
459 | width: 31%;
460 | margin: 2px 2%;
461 | }
462 | .fileFlowRate {
463 | min-height: 30px;
464 | width: 92%;
465 | margin: 2px 4%;
466 | }
467 | /* 第三个信息栏 */
468 | #log {
469 | height: 50vh;
470 | }
471 | .logs {
472 | margin: 8px 20px;
473 | height: 95%;
474 | overflow: scroll;
475 | backdrop-filter: blur(0px);
476 | animation: FadeIn 0.5s ease-in forwards;
477 | }
478 | .logs > p {
479 | font-family: CascadiaMono;
480 | font-size: 0.8em;
481 | border-radius: 5px;
482 | border: var(--border-size) solid var(--border-color);
483 | background-color: var(--transparent-background-color);
484 | margin: 5px auto;
485 | word-break: break-all;
486 | box-shadow: 0 0 5px var(--light-color);
487 | padding: 5px;
488 | }
489 | .logs span {
490 | font-family: CascadiaMono;
491 | font-size: 1em;
492 | }
493 | .logSpace:before {
494 | content: " ";
495 | }
496 | .logSpace:after {
497 | content: " ";
498 | }
499 | .infoLog {
500 | color: var(--info-color);
501 | background-color: var(--info-background-color);
502 | margin: 2px 5px;
503 | max-width: 30px;
504 | border-radius: 8px;
505 | white-space: pre;
506 | }
507 | .warnLog {
508 | color: var(--warn-color);
509 | background-color: var(--warn-background-color);
510 | margin: 2px 5px;
511 | max-width: 30px;
512 | border-radius: 8px;
513 | white-space: pre;
514 | }
515 | .errLog {
516 | color: var(--err-color);
517 | background-color: var(--err-background-color);
518 | margin: 2px 5px;
519 | max-width: 30px;
520 | border-radius: 8px;
521 | white-space: pre;
522 | }
523 | /* 设置弹窗 */
524 | .setting {
525 | position: fixed;
526 | width: 95%;
527 | min-height: 30px;
528 | max-height: 95%;
529 | left: 2%;
530 | right: 2%;
531 | top: 0%;
532 | bottom: 0%;
533 | margin: auto;
534 | overflow: scroll;
535 | border-radius: 25px;
536 | border: var(--border-size) solid var(--border-color);
537 | box-shadow: 0px 0px 25px var(--light-color);
538 | backdrop-filter: blur(10px);
539 | animation: FadeIn 0.2s ease-in forwards;
540 | background-color: var(--transparent-background-color);
541 | }
542 | .settingTitle {
543 | width: 80%;
544 | font-size: 1.8em;
545 | margin: 10px auto;
546 | text-align: center;
547 | font-weight: bold;
548 | }
549 | .inner {
550 | width: 95%;
551 | max-height: 90%;
552 | margin: 15px auto;
553 | overflow: scroll;
554 | position: relative;
555 | }
556 | .option {
557 | width: 91%;
558 | margin: 10px 2%;
559 | border-radius: 5px;
560 | border: var(--border-size) solid var(--border-color);
561 | background-color: var(--transparent-background-color);
562 | word-break: break-all;
563 | box-shadow: 0 0 10px var(--light-color);
564 | padding: 2%;
565 | }
566 | .optionTitle {
567 | font-size: 1.6em;
568 | color: var(--options-color);
569 | }
570 | .optionDescription {
571 | font-size: .8em;
572 | color: var(--options-description-color);
573 | text-stroke: var(--options-stroke-size) var(--options-stroke-color);
574 | -webkit-text-stroke: var(--options-stroke-size) var(--options-stroke-color);
575 | font-weight: bold;
576 | }
577 | .settingBox {
578 | width: 93%;
579 | min-height: 30px;
580 | max-height: 95%;
581 | padding: 1%;
582 | position: fixed;
583 | top: 2%;
584 | bottom: 2%;
585 | margin: auto;
586 | left: 2.5%;
587 | right: 2.5%;
588 | border-radius: 25px;
589 | border: var(--border-size) solid var(--border-color);
590 | background-color: var(--transparent-background-color);
591 | word-break: break-all;
592 | box-shadow: 0 0 35px var(--light-color);
593 | backdrop-filter: blur(10px);
594 | overflow: scroll;
595 | animation: FadeIn 0.2s ease-in forwards;
596 | }
597 | .settingBox > p {
598 | width: 80%;
599 | font-size: 1.6em;
600 | margin: 10px auto;
601 | text-align: center;
602 | font-weight: bold;
603 | }
604 | /* 设置选项 */
605 | .settingBox > div {
606 | width: 90%;
607 | margin: 2px auto;
608 | }
609 | .settingName {
610 | width: 80%;
611 | margin: 2px 0;
612 | display: inline-block;
613 | vertical-align: middle;
614 | font-size: 1.4em
615 | }
616 | .settingNameInput {
617 | width: 50%;
618 | }
619 | .settingBox > button {
620 | display: inline-block;
621 | position: relative;
622 | width: 88%;
623 | padding: 10px;
624 | min-height: 50px;
625 | margin-left: 6%;
626 | margin-top: 1%;
627 | font-size: 1em;
628 | }
629 | .settingBox > button:active {
630 | width: 90%;
631 | padding: 10px;
632 | background-color: #44aadd;
633 | color: #dddddd;
634 | margin-left: 5%;
635 | font-size: 1.1em;
636 | }
637 | .settingBox > div > div {
638 | width: 20%;
639 | margin: 2px auto;
640 | position: relative;
641 | vertical-align: middle;
642 | display: inline-block;
643 | }
644 | .settingBox > div > .settingInputBox {
645 | width: 50%;
646 | margin: 2px auto;
647 | position: relative;
648 | vertical-align: middle;
649 | display: inline-block;
650 | }
651 | .settingBox > div > div > .toggle {
652 | position: absolute;
653 | top: 50%;
654 | transform: translateY(-50%);
655 | right: 0;
656 | text-align: center;
657 | }
658 | .settingBox > div > div > select {
659 | position: absolute;
660 | top: 50%;
661 | transform: translateY(-50%);
662 | right: 0;
663 | margin: 2px auto;
664 | width: 50px;
665 | height: 25px;
666 | text-align: center;
667 | padding: 0;
668 | border: 1px solid var(--border-color);
669 | border-radius: 4px;
670 | font-size: 1em;
671 | }
672 | .settingBox > div > div > button {
673 | position: absolute;
674 | padding: 0;
675 | display: inline-block;
676 | top: 50%;
677 | transform: translateY(-50%);
678 | right: 0;
679 | margin: 2px auto;
680 | width: 50px;
681 | height: 25px;
682 | text-align: center;
683 | padding: 0;
684 | border: 1px solid var(--border-color);
685 | border-radius: 4px;
686 | font-size: 1em;
687 | }
688 | .settingBox > div > div > input {
689 | width: 95%;
690 | transition: width 0.5s;
691 | }
692 | .settingBox > div > div > input:hover {
693 | width: 100%;
694 | }
695 | /* 授权码输入 */
696 | #authorizationKeyName {
697 | width: 50%;
698 | }
699 | #authorizationKeyBox {
700 | width: 50%;
701 | text-align: center;
702 | margin: 2px auto;
703 | right: 0;
704 | }
705 | #authorizationKeyBox > input[type="text"] {
706 | width: 95%;
707 | transition: width 0.5s;
708 | }
709 | #authorizationKeyBox > input[type="text"]:hover {
710 | width: 100%;
711 | }
712 | #inet4_range_name {
713 | width: 50%;
714 | }
715 | #inet4_range_box {
716 | width: 50%;
717 | text-align: center;
718 | margin: 2px auto;
719 | right: 0;
720 | }
721 | #inet4_range_box > input[type="text"] {
722 | width: 95%;
723 | transition: width 0.5s;
724 | }
725 | #inet4_range_box > input[type="text"]:hover {
726 | width: 100%;
727 | }
728 | #inet6_range_name {
729 | width: 50%;
730 | }
731 | #inet6_range_box {
732 | width: 50%;
733 | text-align: center;
734 | margin: 2px auto;
735 | right: 0;
736 | }
737 | #inet6_range_box > input[type="text"] {
738 | width: 95%;
739 | transition: width 0.5s;
740 | resize: none;
741 | overflow: hidden;
742 | white-space: nowrap;
743 | }
744 | #inet6_range_box > input[type="text"]:hover {
745 | width: 100%;
746 | }
747 | #boxConfigBox {
748 | background: blue;
749 | text-align: right;
750 | }
751 | /* 二级选项列表 */
752 | .settingListBox {
753 | width: 93%;
754 | min-height: 30px;
755 | max-height: 95%;
756 | padding: 1%;
757 | position: fixed;
758 | top: 2%;
759 | bottom: 2%;
760 | margin: auto;
761 | left: 2.5%;
762 | right: 2.5%;
763 | border-radius: 25px;
764 | border: var(--border-size) solid var(--border-color);
765 | background-color: var(--transparent-background-color);
766 | word-break: break-all;
767 | box-shadow: 0 0 35px var(--light-color);
768 | backdrop-filter: blur(10px);
769 | overflow: scroll;
770 | animation: FadeIn 0.2s ease-in forwards;
771 | }
772 | .settingListBox > p {
773 | width: 80%;
774 | font-size: 1.6em;
775 | margin: 10px auto;
776 | text-align: center;
777 | font-weight: bold;
778 | }
779 | .allsub {
780 | width: 96%;
781 | max-height: 90%;
782 | position: relative;
783 | left: 1.5%;
784 | overflow: scroll;
785 | margin: 5px auto;
786 | }
787 | .everysub {
788 | width: 38%;
789 | padding: 2%;
790 | margin: 2% 3%;
791 | display: inline-block;
792 | vertical-align: middle;
793 | position: relative;
794 | border-radius: 15px;
795 | border: var(--border-size) solid var(--border-color);
796 | background-color: var(--transparent-background-color);
797 | box-shadow: 0 0 10px var(--light-color);
798 | }
799 | .everysub > p:first-child {
800 | font-size: 1.2em;
801 | color: var(--main-color);
802 | font-weight: bold;
803 | white-space: nowrap;
804 | overflow: hidden;
805 | text-overflow: ellipsis;
806 | }
807 | .everysub > p {
808 | width: 100%;
809 | text-align: center;
810 | }
811 | #PartitionLine {
812 | background-color: white;
813 | height: 3px;
814 | border-radius: 2px;
815 | border: var(--border-size) solid var(--border-color);
816 | }
817 | /* dns 选项 */
818 | .dnsOption {
819 | width: 90%;
820 | margin: 2px auto;
821 | }
822 |
823 | /* 淡入动画 */
824 | @keyframes FadeIn {
825 | from {
826 | opacity: 0;
827 | }
828 | 20% {
829 | opacity: 0.1;
830 | }
831 | 40% {
832 | opacity: 0.2;
833 | }
834 | 60% {
835 | opacity: 0.3;
836 | }
837 | 80% {
838 | opacity: 0.7;
839 | }
840 | to {
841 | opacity: 1;
842 | }
843 | }
844 | @keyframes FadeOut {
845 | from {
846 | opacity: 1;
847 | }
848 | 20% {
849 | opacity: 0.7;
850 | }
851 | 40% {
852 | opacity: 0.3;
853 | }
854 | 60% {
855 | opacity: 0.2;
856 | }
857 | 80% {
858 | opacity: 0.1;
859 | }
860 | to {
861 | opacity: 0;
862 | }
863 | }
864 | @keyframes GrowUp {
865 | 10% {
866 | height: 10vh;
867 | }
868 | 20% {
869 | height: 20vh;
870 | }
871 | 50% {
872 | height: 30vh;
873 | }
874 | 70 {
875 | height: 40vh;
876 | }
877 | 100% {
878 | height: 50vh;
879 | }
880 | }
881 | /* 隐藏滚动条 */
882 | ::-webkit-scrollbar {
883 | display: none;
884 | }
885 | .no-script {
886 | /*display: flex;*/
887 | /*width: 100px;*/
888 | /*height: 30px;*/
889 | /*border: var(--border-size) solid var(--border-color);*/
890 | /*align-items: center;*/
891 | /*justify-content: center;*/
892 | display: none;
893 | }
894 | /* 响应式布局 */
895 | @media screen and (min-width: 800px){
896 | #log, #subs {
897 | display: inline-block;
898 | margin: 15px 1%;
899 | width: 38%;
900 | height: 50vh;
901 | vertical-align: middle;
902 | }
903 | #subs {
904 | left: 10%;
905 | }
906 | #log {
907 | left: 10%;
908 | }
909 | .app {
910 | margin: 15px auto;
911 | }
912 | .subs {
913 | overflow: scroll;
914 | }
915 | .container > p {
916 | font-size: 1em
917 | }
918 | .infoIcon {
919 | font-size: 1em;
920 | }
921 | .infoText {
922 | font-size: 1em;
923 | }
924 | .updateTime > .infoIcon, .totalFlowRate > .infoIcon, .httpUpdateTime > .infoIcon {
925 | font-size: 1.3em;
926 | }
927 | .speed > p > span {
928 | font-size: 1.0em;
929 | }
930 | }
931 | @media screen and (max-width: 800px){
932 | #subs, #log {
933 | max-width: 800px;
934 | }
935 | .infoIcon {
936 | font-size: 0.9em;
937 | }
938 | .infoText {
939 | font-size: 0.9em;
940 | }
941 | .updateTime > .infoIcon, .totalFlowRate > .infoIcon, .httpUpdateTime > .infoIcon {
942 | font-size: 1.2em;
943 | }
944 | .speed > p > span {
945 | font-size: 1.0em;
946 | }
947 | .container > p {
948 | font-size: 0.9em
949 | }
950 | }
951 | @media screen and (max-width: 380px){
952 | #subs, #log {
953 | max-height: 50vh;
954 | }
955 | .container > p {
956 | font-size: 0.9em
957 | }
958 | .infoIcon {
959 | font-size: 0.6em;
960 | }
961 | .infoText {
962 | font-size: 0.6em;
963 | }
964 | .updateTime > .infoIcon, .totalFlowRate > .infoIcon, .httpUpdateTime > .infoIcon {
965 | font-size: 0.9em;
966 | }
967 | .speed > p > span {
968 | font-size: 1.0em;
969 | }
970 | }
--------------------------------------------------------------------------------