├── README.md ├── app.webmanifest ├── assets ├── Initialization.js ├── api.js ├── document.js ├── index.css ├── index.js ├── logging.js ├── pages │ ├── 404.js │ ├── auth.js │ └── home.js ├── route.js ├── setting │ ├── dns.js │ ├── general.js │ ├── log.js │ ├── mystery.js │ ├── ntp.js │ ├── outbound-provider.js │ ├── outbounds.js │ ├── package.js │ └── setting.js ├── sw.js └── task.js ├── config.json ├── fonts ├── CascadiaMono.ttf ├── CascadiaMonoItalic.ttf ├── CascadiaMonoPL.ttf ├── CascadiaMonoPLItalic.ttf ├── GoogleSansText-Bold.ttf ├── GoogleSansText-BoldItalic.ttf ├── GoogleSansText-Italic.ttf ├── GoogleSansText-Medium.ttf ├── GoogleSansText-MediumItalic.ttf └── GoogleSansText-Regular.ttf ├── images ├── bg.png ├── maho.gif ├── maho.ico ├── maho_144x144.ico ├── maho_72x72.ico └── maho_96x96.ico ├── index.html └── sw.js /README.md: -------------------------------------------------------------------------------- 1 | # TPanel 2 | 一个不知道是什么的透明面板。 3 | 4 | ## 免责申明 5 | 所有用户行为均与本人无关,使用面板即视为同意。 6 | -------------------------------------------------------------------------------- /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/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 | })() -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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/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/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/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/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/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/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/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/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/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/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/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/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 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"log":true,"speed":false,"shortly":false,"YiYan":true} -------------------------------------------------------------------------------- /fonts/CascadiaMono.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/CascadiaMono.ttf -------------------------------------------------------------------------------- /fonts/CascadiaMonoItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/CascadiaMonoItalic.ttf -------------------------------------------------------------------------------- /fonts/CascadiaMonoPL.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/CascadiaMonoPL.ttf -------------------------------------------------------------------------------- /fonts/CascadiaMonoPLItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/CascadiaMonoPLItalic.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-Bold.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-Italic.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-Medium.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-MediumItalic.ttf -------------------------------------------------------------------------------- /fonts/GoogleSansText-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/fonts/GoogleSansText-Regular.ttf -------------------------------------------------------------------------------- /images/bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/bg.png -------------------------------------------------------------------------------- /images/maho.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/maho.gif -------------------------------------------------------------------------------- /images/maho.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/maho.ico -------------------------------------------------------------------------------- /images/maho_144x144.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/maho_144x144.ico -------------------------------------------------------------------------------- /images/maho_72x72.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/maho_72x72.ico -------------------------------------------------------------------------------- /images/maho_96x96.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xireiki/tpanel/039ffceff0016639c729faf985c49767bc35868d/images/maho_96x96.ico -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------