const proxyUtil = require('scripts/proxyUitl') const updateUtil = require('scripts/updateUtil') const cu = require('scripts/colorUtil') const ruleUpdateUtil = require('scripts/ruleUpdateUtil') const colorUtil = require('scripts/colorUtil') const FILE = 'data.js' const PROXY_HEADER = 'ProxyHeader' const settingKeys = ['widgetSettings', 'generalSettings', 'proxyGroupSettings', 'customSettings', 'hostSettings', 'urlrewriteSettings', 'ssidSettings', 'headerrewriteSettings', 'hostnameSettings', 'mitmSettings'] if (!$file.exists(FILE)) { $file.write({ data: $data({ "string": JSON.stringify({ "urls": [] }) }), path: FILE }) } String.prototype.reverse = function () { return this.toString().split('').reverse().join('') } String.prototype.contains = function (sub) { return this.indexOf(sub) > -1 } setDefaultSettings() let screenHeight = $device.info.screen.height const screenWidth = $device.info.screen.width const iPhoneX = $device.isIphoneX if (iPhoneX) { screenHeight -= 48 } const statusBarHeight = iPhoneX ? 44 : 20 const navBarHeight = 45 const selectedColor = $color("#c1dcf0") const btnOnFg = colorUtil.getColor("usualBtnOnFg") const btnOnBg = colorUtil.getColor("usualBtnOnBg") const btnOffFg = colorUtil.getColor("usualBtnOffFg") const btnOffBg = colorUtil.getColor("usualBtnOffBg") function renderUI() { $ui.render({ props: { title: "lhie1规则", navBarHidden: true, statusBarHidden: colorUtil.getColor("statusBar", true) === 'clear' ? true : false, statusBarStyle: colorUtil.getColor("statusBar", true) === '#ffffff' ? 1 : 0, id: "bodyView", }, events: { appeared: function (sender) { // $("bodyView").runtimeValue().$viewController().$navigationController().$interactivePopGestureRecognizer().$delegate() // $("bodyView").runtimeValue().$viewController().$navigationController().$interactivePopGestureRecognizer().$setDelegate(null) if (typeof $ui.window.next !== 'undefined') { console.error('警告:正在调试模式运行,界面可能会被遮挡,请从JSBox主界面运行此脚本!') $addin.restart() } } }, views: [{ type: "view", props: { id: "navBar", bgcolor: colorUtil.getColor("navBar") }, layout: (make, view) => { make.height.equalTo(navBarHeight + statusBarHeight) make.width.equalTo(view.super) }, views: [{ type: "lottie", props: { id: "navLoadingIcon", loop: false, speed: 3, src: "assets/balloons.json" }, layout: (make, view) => { make.size.equalTo($size(screenWidth, screenHeight)) make.bottom.equalTo(navBarHeight + statusBarHeight + 80) // make.right.equalTo(view.prev.left).offset(-15) // make.bottom.equalTo(view.super).offset(100) } }, { type: "label", props: { text: "lhie1规则", textColor: colorUtil.getColor("navTitleText"), font: $font("bold", 25) }, layout: (make, view) => { make.height.equalTo(navBarHeight) make.top.equalTo(statusBarHeight) make.left.equalTo(15) } }, { type: "image", props: { icon: $icon("204", colorUtil.getColor("navIconRight"), $size(25, 25)), bgcolor: $color("clear") }, layout: (make, view) => { make.right.equalTo(view.super).offset(-15) make.height.width.equalTo(25) make.bottom.equalTo(view.super).offset(-10) }, events: { tapped: sender => { archivesHandler() } } }, { type: "image", props: { icon: $icon("074", colorUtil.getColor("navIconLeft"), $size(25, 25)), bgcolor: $color("clear") }, layout: (make, view) => { make.right.equalTo(view.prev.left).offset(-15) make.height.width.equalTo(25) make.bottom.equalTo(view.super).offset(-10) }, events: { tapped: sender => { // $clipboard.text = 'GxsAtS84U7' // $app.openURL("alipay://") // $app.openURL("https://qr.alipay.com/c1x047207ryk0wiaj6m6ye3") $ui.alert({ title: '感谢支持', message: '作者投入大量时间和精力对脚本进行开发和完善,你愿意给他赏杯咖啡支持一下吗?', actions: [{ title: "支付宝", handler: () => { $app.openURL($qrcode.decode($file.read("assets/thankyou2.jpg").image)) } }, { title: "微信", handler: () => { $quicklook.open({ image: $file.read("assets/thankyou.jpg").image }) } }, { title: "返回" }] }) } } },] }, { type: "view", props: { id: "mainView" }, layout: (make, view) => { make.height.equalTo(view.super).offset(navBarHeight + statusBarHeight); make.width.equalTo(view.super) make.top.equalTo(navBarHeight + statusBarHeight) }, views: [{ type: "input", props: { id: "fileName", text: '', placeholder: "配置名(lhie1)" }, layout: (make, view) => { make.width.equalTo(view.super).offset(-120) make.height.equalTo(40) make.left.top.equalTo(10) }, events: { changed: sender => { saveWorkspace() }, returned: sender => { sender.blur() } } }, { type: "button", props: { type: $btnType.contactAdd, id: "serverURL", titleColor: colorUtil.getColor("importBtnText"), title: " 导入节点", }, layout: (make, view) => { make.width.equalTo(100) make.height.equalTo(40) make.right.equalTo(-10) make.top.equalTo(10) }, events: { tapped: sender => { importMenu({ handler: nodeImportHandler }) } } }, { type: "matrix", props: { id: "serverControl", columns: 3, scrollEnabled: false, itemHeight: 40, bgcolor: $color("#f0f5f5"), data: genControlItems(), template: [{ type: "label", props: { id: "title", align: $align.center, font: $font(14), textColor: colorUtil.getColor("controlBtnText"), autoFontSize: true }, layout: $layout.fill }], info: {} }, layout: (make, view) => { make.width.equalTo(view.super).offset(-20) make.centerX.equalTo(view.super) make.height.equalTo(40) make.top.equalTo($("serverURL").bottom).offset(10) }, events: { didSelect: (sender, indexPath, data) => { let btnTitle = data.title.text if (btnTitle === '节点倒序') { reverseServerGroup() } else if (btnTitle === '删除分组') { deleteServerGroup() } else if (btnTitle === '特殊代理') { specialProxyGroup(); } else { groupShortcut() } } } }, { type: "list", props: { id: "serverEditor", data: [], separatorHidden: true, reorder: true, actions: [{ title: "delete", handler: (sender, indexPath) => { saveWorkspace() } }, { title: "编辑", color: $color("tint"), handler: (sender, indexPath) => { let od = sender.data let section = od[indexPath.section] let item = section.rows[indexPath.row] showAlterDialog(section.title, item.proxyLink, (newSec, newLink) => { item.proxyLink = newLink item.proxyName.text = newLink.split(/\s*=/)[0] section.rows[indexPath.row] = item section.title = newSec sender.data = formatListData(od) saveWorkspace() }) } }], borderWidth: 2, borderColor: $color("#f0f5f5"), template: { views: [{ type: 'label', props: { id: 'proxyName', align: $align.left, line: 1, textColor: colorUtil.getColor("editorItemText"), font: $font(16), }, layout: (make, view) => { make.width.equalTo(view.super).offset(-60) make.height.equalTo(view.super) make.left.equalTo(view.super).offset(15) } }, { type: "image", props: { id: "proxyAuto", icon: $icon("062", colorUtil.getColor("editorItemIcon"), $size(15, 15)), bgcolor: $color("clear"), hidden: true }, layout: (make, view) => { make.right.equalTo(view.super).offset(-15) make.size.equalTo($size(15, 15)) make.centerY.equalTo(view.super) } }] }, // radius: 5 }, layout: (make, view) => { make.width.equalTo(view.super).offset(-20) make.centerX.equalTo(view.super) make.height.equalTo(screenHeight - 330 - (typeof $ui.window.next !== 'undefined' ? 45 : 0)) make.top.equalTo($("serverControl").bottom) }, events: { didSelect: (sender, indexPath, data) => { let proxyName = data.proxyName.text let isSelected = !data.proxyAuto.hidden let controlInfo = $("serverControl").info let currentGroup = controlInfo.currentProxyGroup let customProxyGroup = controlInfo.customProxyGroup || {} if (isSelected) { data.proxyAuto.hidden = true customProxyGroup[currentGroup] = customProxyGroup[currentGroup].filter(i => i !== proxyName) } else { data.proxyAuto.hidden = false customProxyGroup[currentGroup].push(proxyName) } let uiData = sender.data uiData[indexPath.section].rows[indexPath.row] = data sender.data = uiData $("serverControl").info = controlInfo saveWorkspace() }, reorderFinished: data => { $thread.background({ delay: 0, handler: function () { $("serverEditor").data = formatListData(data) saveWorkspace() } }) }, forEachItem: (view, indexPath) => { if (indexPath.row % 2 === 1) { view.bgcolor = $color("#f1f8ff") } else { view.bgcolor = $color("white") } }, pulled: sender => { let listSections = $("serverEditor").data.filter(i => /^http/.test(i.url)) linkHandler(listSections.map(i => i.url).join('\n'), { handler: (res, name, url, type) => { sender.endRefreshing() nodeImportHandler(res, name, url, type) } }) } } }, { type: "input", props: { id: "serverSuffixEditor", placeholder: '节点后缀', text: '', font: $font(18), type: $kbType.ascii }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(10) make.width.equalTo(view.prev).offset(-125) make.height.equalTo(45) make.left.equalTo(view.prev.left) }, events: { changed: sender => { saveWorkspace() }, returned: sender => { sender.blur() } } }, { type: "view", props: { id: "outputFormatLabel", bgcolor: $color("#f1f1f1"), radius: 6 }, layout: (make, view) => { make.right.equalTo(view.super.right).offset(-10) make.top.equalTo(view.prev) make.height.equalTo(view.prev) make.width.equalTo(120) }, views: [{ type: "image", props: { data: $file.read('assets/menu_icon.png'), bgcolor: $color("clear"), hidden: true }, layout: (make, view) => { make.left.equalTo(view.super).offset(10) make.height.width.equalTo(view.super.height).offset(-25) make.centerY.equalTo(view.super) } }, { type: "image", props: { id: "outputFormatIcon", data: $file.read('assets/today_surge.png'), bgcolor: $color("clear") }, layout: (make, view) => { make.left.equalTo(view.super).offset(5) make.height.width.equalTo(view.super.height).offset(-15) make.centerY.equalTo(view.super).offset(1) } }, { type: 'label', props: { textColor: colorUtil.getColor("outputFormatText"), id: 'outputFormatType', text: 'Surge3 Pro', align: $align.center, font: $font("bold", 16), autoFontSize: true, }, layout: (make, view) => { make.height.equalTo(view.super) make.width.equalTo(view.super).offset(-45) make.left.equalTo(view.prev.right) make.top.equalTo(view.super) } }], events: { tapped: sender => { renderOutputFormatMenu(sender) } } }, { type: "matrix", props: { id: "usualSettings", columns: 4, itemHeight: 40, spacing: 5, scrollEnabled: false, data: [{ title: { text: 'ADS', bgcolor: btnOnFg, textColor: btnOffFg } }, { title: { text: 'MITM', bgcolor: btnOnFg, textColor: btnOffFg } }, { title: { text: 'Emoji', bgcolor: btnOnFg, textColor: btnOffFg } }, { title: { text: '导出', bgcolor: btnOnFg, textColor: btnOffFg } }], template: [{ type: "label", props: { id: "title", align: $align.center, font: $font(14), radius: 5, borderColor: btnOnBg, borderWidth: 0.3, }, layout: $layout.fill }] }, layout: (make, view) => { make.width.equalTo(view.super).offset(-10) make.centerX.equalTo(view.super) make.height.equalTo(50) make.top.equalTo($("serverSuffixEditor").bottom).offset(5) }, events: { didSelect: (sender, indexPath, data) => { if (indexPath.row === 2) { refreshListEmoji(isEmoji()) } data.title.bgcolor = cu.isEqual(data.title.bgcolor, btnOnBg) ? btnOffBg : btnOnBg data.title.textColor = cu.isEqual(data.title.bgcolor, btnOnFg) ? btnOffFg : btnOnFg let uiData = $("usualSettings").data uiData[indexPath.row] = data $("usualSettings").data = uiData saveWorkspace() } } }] }, { type: "button", props: { id: "advanceBtn", titleColor: colorUtil.getColor("advanceBtnFg"), title: "进阶设置", bgcolor: colorUtil.getColor("advanceBtnBg") }, layout: (make, view) => { make.width.equalTo((screenWidth / 2 - 15) * 0.686 - 10) make.left.equalTo(10) make.height.equalTo(40) make.top.equalTo($("usualSettings").bottom).offset(5) }, events: { tapped: sender => { renderAdvanceUI() } } }, { type: "button", props: { id: "aboutBtn", titleColor: colorUtil.getColor("aboutBtnFg"), title: "关于", bgcolor: colorUtil.getColor("aboutBtnBg") }, layout: (make, view) => { make.height.equalTo(40) make.width.equalTo((screenWidth / 2 - 15) * 0.314 + 5) make.left.equalTo($("advanceBtn").right).offset(5) make.top.equalTo($("usualSettings").bottom).offset(5) }, events: { tapped: sender => { renderAboutUI() } } }, { type: "button", props: { id: "genBtn", titleColor: colorUtil.getColor("genBtnFg"), bgcolor: colorUtil.getColor("genBtnBg"), title: "生成配置" }, layout: (make, view) => { make.width.equalTo((screenWidth - 10) * 0.5 - 5) make.height.equalTo(40) make.right.equalTo(view.super).offset(-10) make.top.equalTo($("usualSettings").bottom).offset(5) }, events: { tapped: sender => { makeConf({ onStart: () => { $("progressView").hidden = false $ui.animate({ duration: 0.2, animation: function () { $("progressView").alpha = 1 } }) }, onProgress: (p, hint) => { hint !== '' && ($("loadingHintLabel").text = hint) $("progressBar").value = p }, onDone: res => { $ui.animate({ duration: 0.3, animation: function () { $("progressView").alpha = 0 }, completion: function () { $("loadingHintLabel").text = '处理中,请稍后' $("progressBar").value = 0 $("progressView").hidden = true } }) exportConf(res.fileName, res.fileData, res.target, res.actionSheet, false, () => { $http.stopServer() }) $app.listen({ resume: function () { $http.stopServer() } }) }, onError: res => { $("progressView").value = 0 $("progressView").hidden = true } }) }, longPressed: sender => { $share.sheet(['data.js', $file.read('data.js')]) } } }, { type: "blur", props: { id: "progressView", style: 1, alpha: 0, hidden: true }, layout: $layout.fill, views: [{ type: "label", props: { id: "loadingHintLabel", text: "处理中,请稍后", textColor: $color("black"), }, layout: (make, view) => { make.centerX.equalTo(view.super) make.centerY.equalTo(view.super).offset(-30) } }, { type: "progress", props: { id: "progressBar", value: 0 }, layout: (make, view) => { make.width.equalTo(screenWidth * 0.8) make.center.equalTo(view.super) make.height.equalTo(3) } }] },] }) } let nodeImportHandler = (res, name, url, type = -1) => { // 如果是托管,url不为undefined // console.log([res, name, url]) let listData = $("serverEditor").data || [] let existsSec = listData.find(item => item.url === url) if (!res || res.length === 0) { $ui.alert({ title: `${existsSec ? '更新' : '获取'}失败`, message: `${existsSec ? existsSec.title : url}` }) return } let section = existsSec || { title: name, rows: [], url: url } section.rows = [] let controlInfo = $("serverControl").info for (let idx in res) { if (res[idx].split("=")[1].trim() == 'direct') { // 过滤直连 continue } let selected = controlInfo.customProxyGroup[controlInfo.currentProxyGroup].indexOf(res[idx].split('=')[0].trim()) > -1 section.rows.push({ proxyName: { text: res[idx].split('=')[0].trim() }, proxyLink: res[idx], proxyAuto: { hidden: !selected }, proxyType: type }) } if (!existsSec) { listData.unshift(section) } $("serverEditor").data = listData saveWorkspace() } function formatListData(data) { if (!data || data.length === 0) { return [] } let noGroup = [] data = data.map(i => { if (i.title === '' || i.rows.length === 0) { noGroup = noGroup.concat(i.rows) return null } return i }).filter(i => i !== null) if (noGroup.length > 0) { data.unshift({ title: "", rows: noGroup }) } return data } function loading(on) { let iconView = $("navLoadingIcon") if (on) { // iconView.play() } else { // iconView.pause() } } function refreshListEmoji(isEmoji) { function addEmoji(emojiSet, name) { let minIdx = 300; let resEmoji = ''; for (let idx in emojiSet) { let reg = `(${emojiSet[idx].slice(1).join('|')})` let matcher = name.match(new RegExp(reg)) if (matcher && matcher.index < minIdx) { minIdx = matcher.index resEmoji = emojiSet[idx][0] } } return minIdx !== 300 ? `${resEmoji} ${name}` : name } function removeEmoji(emojiSet, name) { let emoji = emojiSet.map(i => i[0]) let reg = `(${emoji.join('|')}) ` return name.replace(new RegExp(reg, 'g'), '') } let serverEditorData = $("serverEditor").data loading(true) $http.get({ url: "https://raw.githubusercontent.com/Fndroid/country_emoji/master/emoji.json" + `?t=${new Date().getTime()}` }).then(resp => { loading(false) let emojiSet = resp.data $("serverEditor").data = serverEditorData.map(sec => { sec.rows = sec.rows.map(i => { i.proxyLink = isEmoji ? removeEmoji(emojiSet, i.proxyLink) : addEmoji(emojiSet, i.proxyLink) i.proxyName.text = i.proxyLink.split(/\s*=/)[0] return i }) return sec }) saveWorkspace() }).catch(error => { loading(false) $ui.alert("Emoji配置获取失败") }) } function showAlterDialog(reg, rep, callback) { let fontSize = $text.sizeThatFits({ text: rep, width: screenWidth - 100, font: $font(16) }) let view = { type: "blur", layout: $layout.fill, props: { id: "alertBody", style: 1, alpha: 0 }, views: [{ type: "view", props: { id: "alterMainView", bgcolor: $color("#ccc"), smoothRadius: 10 }, layout: (make, view) => { make.height.equalTo(230 + fontSize.height); make.width.equalTo(view.super).offset(-60); make.center.equalTo(view.super) }, events: { tapped: sender => { } }, views: [{ type: "label", props: { text: "组别名称", font: $font("bold", 16) }, layout: (make, view) => { make.top.equalTo(view.super).offset(20); make.left.equalTo(view.super).offset(10); } }, { type: "input", props: { id: "alterInputSection", text: reg, autoFontSize: true }, events: { returned: sender => { sender.blur() } }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(10); make.width.equalTo(view.super).offset(-20); make.centerX.equalTo(view.super) make.left.equalTo(view.super).offset(10); make.height.equalTo(40) } }, { type: "label", props: { text: "节点信息", font: $font("bold", 16) }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(15); make.left.equalTo(view.super).offset(10); } }, { type: "text", props: { id: "alberInputLink", text: rep, autoFontSize: true, radius: 6, font: $font(16), bgcolor: $color("#eff0f2"), insets: $insets(10, 5, 10, 5) }, events: { returned: sender => { sender.blur() } }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(10); make.width.equalTo(view.super).offset(-20); make.centerX.equalTo(view.super) make.left.equalTo(view.super).offset(10); make.height.equalTo(fontSize.height + 20) } }, { type: 'button', props: { icon: $icon("064", $color("#fff"), $size(20, 20)), id: 'confirmBtn', radius: 25 }, layout: (make, view) => { make.height.width.equalTo(50) make.bottom.equalTo(view.super).offset(-10) make.right.equalTo(view.super).offset(-10) }, events: { tapped: sender => { callback && callback($("alterInputSection").text, $("alberInputLink").text); $("alertBody").remove(); } } }] }], events: { tapped: sender => { sender.remove() } } } $("bodyView").add(view) $ui.animate({ duration: 0.2, animation: () => { $("alertBody").alpha = 1 } }) } function renderOutputFormatMenu(superView) { $("bodyView").add({ type: "view", props: { id: "outputFormatSelectorView", alpha: 0 }, layout: (make, view) => { make.height.width.equalTo(view.super) make.center.equalTo(view.super) }, views: [{ type: "blur", props: { style: 2, alpha: 1, }, layout: $layout.fill, events: { tapped: sender => { hideView(sender); } } }, { type: "list", props: { id: "outputFormatSelectorItems", radius: 15, rowHeight: 50, alwaysBounceVertical: false, data: ['Surge 3 TF', 'Surge 3 Pro', 'Surge 2', 'Quantumult'], frame: resetFrame(superView.frame), header: { type: "label", props: { text: "选择导出格式", height: 50, font: $font("bold", 15), align: 1 } }, separatorHidden: true }, events: { didSelect: (sender, indexPath, data) => { let type = 'surge' if (data === 'Quantumult') { type = 'quan' } else if (data === 'Surge 2') { type = 'surge2' } $("outputFormatType").text = data $("outputFormatIcon").data = $file.read(`assets/today_${type}.png`) saveWorkspace() hideView(sender) } } }] }) $ui.animate({ duration: 0.3, damping: 0.8, velocity: 0.3, animation: () => { superView.scale(1.2) $("outputFormatSelectorView").alpha = 1 $("outputFormatSelectorItems").frame = $rect(80, screenHeight - 430 + navBarHeight + statusBarHeight, screenWidth - 90, 250) } }) function hideView(sender) { $ui.animate({ duration: 0.2, velocity: 0.5, animation: () => { superView.scale(1) $("outputFormatSelectorView").alpha = 0; $("outputFormatSelectorItems").frame = resetFrame(superView.frame); }, completion: () => { sender.super.remove(); } }); } } function resetFrame(frame) { return $rect(frame.x, frame.y + navBarHeight + statusBarHeight, frame.width, frame.height) } function archivesHandler() { const ARCHIVES = $addin.current.name + '/archivesFiles' if (!$drive.exists(ARCHIVES)) { $drive.mkdir(ARCHIVES) } console.log($drive.list(ARCHIVES)) let latestVersion = ($app.info.build * 1) > 335 let getFiles = function () { return $drive.list(ARCHIVES).map(i => { let path = i.runtimeValue().invoke('pathComponents').rawValue() let name = latestVersion ? i : path[path.length - 1] return { archiveName: { text: name, textColor: name === $cache.get('currentArchive') ? $color("red") : $color("black") } } }) } $("bodyView").add({ type: "view", props: { id: "archivesView", alpha: 0 }, layout: (make, view) => { make.height.width.equalTo(view.super) make.center.equalTo(view.super) }, views: [{ type: "blur", props: { style: 2, alpha: 1, }, layout: $layout.fill, events: { tapped: sender => { $ui.animate({ duration: 0.2, animation: () => { $("archivesView").alpha = 0 $("archivesList").frame = $rect(0, 0, screenWidth, screenHeight) }, completion: () => { sender.super.remove() } }) } } }, { type: "list", props: { id: "archivesList", radius: 15, data: getFiles(), header: { type: "label", props: { text: "配置备份", height: 50, font: $font("bold", 20), align: $align.center } }, template: { props: { bgcolor: $color("clear") }, views: [ { type: "label", props: { id: "archiveName" }, layout: (make, view) => { make.width.equalTo(view.super).offset(-20) make.left.equalTo(view.super).offset(10) make.height.equalTo(view.super) } } ] }, actions: [{ title: "删除", color: $color('red'), handler: (sender, indexPath) => { let fileName = sender.object(indexPath).archiveName.text let success = $drive.delete(ARCHIVES + '/' + fileName) if (success) { sender.data = getFiles() } } }, { title: "导出", handler: (sender, indexPath) => { let fileName = sender.object(indexPath).archiveName.text $share.sheet(['data.js', $drive.read(ARCHIVES + "/" + fileName)]) } }, { title: "覆盖", color: $color("tint"), handler: (sender, indexPath) => { let filename = sender.object(indexPath).archiveName.text let success = $drive.write({ data: $file.read('data.js'), path: ARCHIVES + '/' + filename }) $ui.toast("配置文件覆盖" + (success ? "成功" : "失败")) } }] }, layout: (make, view) => { make.height.equalTo(view.super).dividedBy(12 / 7) make.width.equalTo(view.super).dividedBy(12 / 9) make.center.equalTo(view.super) }, events: { didSelect: (sender, indexPath, item) => { let data = item.archiveName.text if (/\..*?\.icloud/.test(data)) { $ui.alert(`备份文件不在本地,请先进入iCloud下载,路径为:文件 -> JSBox -> ${$addin.current.name} -> archivesFiles`) return } let success = $file.write({ data: $drive.read(ARCHIVES + '/' + data), path: "data.js" }) if (success) { $cache.set('currentArchive', data) $app.notify({ name: 'loadData' }) $ui.animate({ duration: 0.2, animation: () => { $("archivesView").alpha = 0 $("archivesList").frame = $rect(0, 0, screenWidth, screenHeight) }, completion: () => { sender.super.remove() } }) } } } }, { type: "button", props: { title: "+", circular: true, }, layout: (make, view) => { make.bottom.equalTo(view.prev) make.right.equalTo(view.prev).offset(-5) make.height.width.equalTo(50) }, events: { tapped: sender => { $input.text({ type: $kbType.default, placeholder: "请输入备份文件名", handler: function (text) { if (text === '') { $ui.error("无法创建备份") return } let success = $drive.write({ data: $file.read('data.js'), path: ARCHIVES + '/' + text }) if (success) { $cache.set('currentArchive', text) sender.prev.data = getFiles() } } }) } } }] }) $ui.animate({ duration: .3, damping: 0.8, velocity: 0.3, animation: () => { $("archivesView").alpha = 1 $("archivesList").scale(1.1) } }) } // function specialProxyGroup() { // if (getRulesReplacement()) { // $ui.alert('检测到有规则替换,无法使用特殊代理设置') // return // } // let groups = getProxyGroups(); // const menuItems = groups.concat(['🚀 Direct', '查看设置', '清除设置']); // $ui.menu({ // items: menuItems, // handler: function (mTitle, idx) { // if (idx === menuItems.length - 1) { // $("serverEditor").info = {}; // saveWorkspace(); // } // else if (idx === menuItems.length - 2) { // let videoProxy = $("serverEditor").info; // let output = []; // for (let k in videoProxy) { // output.push(`${k} - ${videoProxy[k]}`); // } // $ui.alert(output.length > 0 ? output.join('\n') : "无设置特殊代理"); // } // else { // let videoReg = require('scripts/videoReg') // $ui.menu({ // items: Object.keys(videoReg), // handler: function (title, idx) { // let proxyName = mTitle; // let videoProxy = $("serverEditor").info; // videoProxy[title] = proxyName; // $("serverEditor").info = videoProxy; // saveWorkspace(); // } // }); // } // } // }); // } function genControlItems() { let currentProxyGroup = PROXY_HEADER try { currentProxyGroup = $("serverControl").info.currentProxyGroup } catch (e) { } return [{ title: { text: '节点倒序' } }, { title: { text: currentProxyGroup } }, { title: { text: '删除分组' } }] } function getProxyGroups() { let fileData = JSON.parse($file.read(FILE).string) let proxyGroupSettings = fileData.proxyGroupSettings let groups = proxyGroupSettings.split(/[\n\r]/).filter(i => /^(?!\/\/)[\s\S]+=[\s\S]+/.test(i)).map(i => i.split('=')[0].trim()) return groups } function groupShortcut() { let controlInfo = $("serverControl").info let currentProxyGroup = controlInfo.currentProxyGroup || PROXY_HEADER let customProxyGroup = controlInfo.customProxyGroup || {} let menuItems = Object.keys(customProxyGroup).sort() $("bodyView").add({ type: "view", props: { id: "placeholderView", alpha: 0 }, layout: (make, view) => { make.height.width.equalTo(view.super) make.center.equalTo(view.super) }, views: [{ type: "blur", props: { style: 2, alpha: 1, }, layout: $layout.fill, events: { tapped: sender => { removeAnimation() } } }, { type: "list", props: { id: "placeholderList", radius: 15, data: menuItems, header: { type: "label", props: { text: "占位符", height: 50, font: $font("bold", 20), align: $align.center } }, actions: [{ title: "删除", color: $color('red'), handler: (sender, indexPath) => { let title = sender.object(indexPath) if ([PROXY_HEADER, 'Proxy Header'].indexOf(title) > -1) { $ui.error("此占位符无法删除") return } delete customProxyGroup[title] $("serverControl").info = controlInfo saveWorkspace() $("placeholderList").data = Object.keys(customProxyGroup).sort() } }, { title: "重命名", handler: (sender, indexPath) => { let title = sender.object(indexPath) if ([PROXY_HEADER, 'Proxy Header'].indexOf(title) > -1) { $ui.error("此占位符无法重命名") return } $input.text({ type: $kbType.default, placeholder: title, handler: function (text) { if (sender.data.indexOf(text) > -1) { $ui.error("此名称已被占用") } else { customProxyGroup[text] = customProxyGroup[title] delete customProxyGroup[title] if ($("serverControl").info.currentProxyGroup === title) { switchToGroup(text) } $("serverControl").info = controlInfo saveWorkspace() sender.data = Object.keys(customProxyGroup).sort() } } }) } }, { title: "删除节点", color: $color('tint'), handler: (sender, indexPath) => { let title = sender.object(indexPath) let headers = customProxyGroup[title] let editorData = $("serverEditor").data editorData.map(section => { section.rows = section.rows.filter(item => headers.indexOf(item.proxyName.text) === -1) return section }) $("serverEditor").data = editorData saveWorkspace() $ui.toast("已删除占位符对应节点") removeAnimation() } }] }, layout: (make, view) => { make.height.equalTo(view.super).dividedBy(12 / 7) make.width.equalTo(view.super).dividedBy(12 / 9) make.center.equalTo(view.super) }, events: { didSelect: (sender, indexPath, data) => { $ui.toast(`当前占位符为:${data}`) switchToGroup(data) removeAnimation() } } }, { type: "button", props: { title: "+", circular: true, }, layout: (make, view) => { make.bottom.equalTo(view.prev) make.right.equalTo(view.prev).offset(-5) make.height.width.equalTo(50) }, events: { tapped: sender => { $input.text({ type: $kbType.default, placeholder: "建议:xxxHeader", handler: function (text) { if ([PROXY_HEADER, 'Proxy Header', ''].indexOf(text) > -1) { $ui.error("占位符名称冲突") return } customProxyGroup[text] = [] $("serverControl").info = controlInfo saveWorkspace() $("placeholderList").data = Object.keys(customProxyGroup).sort() } }) } } }] }) function removeAnimation() { $ui.animate({ duration: 0.2, animation: () => { $("placeholderView").alpha = 0 $("placeholderList").frame = resetFrame($("serverEditor").frame) }, completion: () => { $("placeholderView").remove() } }) } $ui.animate({ duration: .3, damping: 0.8, animation: () => { $("placeholderView").alpha = 1 $("placeholderList").scale(1.1) } }) function switchToGroup(title) { let group = customProxyGroup[title]; // 保存当前编辑策略组 controlInfo.currentProxyGroup = title; $("serverControl").info = controlInfo; // 恢复选中的策略组UI let listData = $("serverEditor").data || []; listData = listData.map(section => { section.rows = section.rows.map(item => { item.proxyAuto.hidden = !(group.indexOf(item.proxyName.text) > -1) return item; }); return section; }); $("serverEditor").data = listData; $("serverControl").data = genControlItems() } } function listReplace(sender, indexPath, obj) { let oldData = sender.data if (indexPath.section != null) { oldData[indexPath.section].rows[indexPath.row] = obj } else { oldData[indexPath.row] = obj } sender.data = oldData } function getAutoRules(url, done, hint = '') { return new Promise((resolve, reject) => { $http.get({ url: url, handler: function (resp) { if (done) done(hint) resolve(resp.data) } }) }) } function importMenu(params) { let staticItems = ['剪贴板', '二维码', '空节点'] $ui.menu({ items: staticItems, handler: function (title, idx) { if (title === staticItems[0]) { let clipText = $clipboard.text linkHandler(clipText, params) } else if (title === staticItems[1]) { $qrcode.scan({ handler(string) { linkHandler(string, params) } }) } else if (title === staticItems[2]) { params.handler(['Empty Node = '], 'Default', `emptynode${new Date().getTime()}`) } } }) } function isEmoji() { try { let advanceSettings = JSON.parse($file.read(FILE).string) let workspace = advanceSettings.workspace let usualData = workspace.usualData let usualValue = function (key) { return usualData.find(i => i.title.text == key) ? usualData.find(i => i.title.text == key).title.bgcolor : false } return usualValue('Emoji') } catch (e) { return false } } function linkHandler(url, params) { let emoji = isEmoji() let servers = { shadowsocks: [], surge: [], online: [], vmess: [], ignore: [], shadowsocksr: [] } if (!url) { $ui.alert('没有识别到有效链接') return } let urls = url.split(/[\r\n]+/g).map(i => i.trim()).filter(i => i !== '') urls.forEach(item => { if (/^ss:\/\//.test(item)) { servers.shadowsocks.push(item) } else if (/^https?:\/\//.test(item)) { servers.online.push(item) } else if (/[\S\s]+=[\s]*(custom|ss|http|https|socks5|socks5-tls|external)/.test(item)) { servers.surge.push(item) } else if (/^vmess:\/\//.test(item)) { servers.vmess.push(item) } else if (/^ssr:\/\//.test(item)) { servers.shadowsocksr.push(item) } else { servers.ignore.push(item) } }) let updateHint = '' updateHint += servers.shadowsocks.length > 0 ? `\nShadowsocks链接${servers.shadowsocks.length}个\n` : '' updateHint += servers.shadowsocksr.length > 0 ? `\nShadowsocksR链接${servers.shadowsocksr.length}个\n` : '' updateHint += servers.surge.length > 0 ? `\nSurge链接${servers.surge.length}个\n` : '' updateHint += servers.vmess.length > 0 ? `\nV2Ray链接${servers.vmess.length}个\n` : '' updateHint += servers.online.length > 0 ? `\n托管或订阅${servers.online.length}个\n` : '' // $ui.alert({ // title: '更新概况', // message: updateHint // }) function addEmoji(emojiSet, link) { let name = link.split(/=/)[0] let minIdx = 300; let resEmoji = ''; for (let idx in emojiSet) { let reg = `(${emojiSet[idx].slice(1).join('|')})` let matcher = name.match(new RegExp(reg)) if (matcher && matcher.index < minIdx) { minIdx = matcher.index resEmoji = emojiSet[idx][0] } } return minIdx !== 300 ? `${resEmoji} ${link}` : link } function detailHandler(emojiSet = null) { for (let k in servers) { if (servers[k].length === 0) { continue } if (k === 'shadowsocks') { let res = proxyUtil.proxyFromURL(servers[k]) params.handler(emojiSet ? res.servers.map(i => addEmoji(emojiSet, i)) : res.servers, res.sstag, servers[k].join('\n')) } else if (k === 'surge') { let urls = servers[k].map(i => i.replace(/,[\s]*udp-relay=true/, '')) let result = [] for (let idx in urls) { result[idx] = urls[idx] } $delay(0.3, function () { params.handler(emojiSet ? result.map(i => addEmoji(emojiSet, i)) : result, urls.length > 1 ? `批量Surge链接(${urls.length})` : result[0].split('=')[0].trim(), urls.join('\n')) }) } else if (k === 'online') { loading(true) proxyUtil.proxyFromConf({ urls: servers[k], handler: res => { console.log('res', res); loading(false) params.handler(emojiSet ? res.servers.map(i => addEmoji(emojiSet, i)) : res.servers, res.filename, res.url, res.type) } }) } else if (k === 'vmess') { let res = proxyUtil.proxyFromVmess(servers[k]) params.handler(emojiSet ? res.servers.map(i => addEmoji(emojiSet, i)) : res.servers, res.sstag, servers[k].join('\n')) } else if (k === 'shadowsocksr') { let res = proxyUtil.proxyFromSSR(servers[k]) params.handler(emojiSet ? res.servers.map(i => addEmoji(emojiSet, i)) : res.servers, res.sstag, servers[k].join('\n')) } else { $ui.alert('剪贴板存在无法识别的行:\n\n' + servers.ignore.join('\n') + '\n\n以上行将被丢弃!') } } } if (emoji) { loading(true) $http.get({ url: "https://raw.githubusercontent.com/Fndroid/country_emoji/master/emoji.json" + `?t=${new Date().getTime()}` }).then(resp => { loading(false) let emojiSet = resp.data detailHandler(emojiSet) }).catch(error => { loading(false) $ui.alert("Emoji配置获取失败") }) } else { detailHandler(null) } } function write2file(key, value) { let content = JSON.parse($file.read(FILE).string) content[key] = value $file.write({ data: $data({ "string": JSON.stringify(content) }), path: FILE }) } function renderAdvanceUI() { let previewData = JSON.parse($file.read(FILE).string) let placeHolders = Object.keys(previewData.workspace.customProxyGroup) let inputViewData = [] for (let idx in settingKeys) { let content = previewData[settingKeys[idx]] let view = { type: "text", props: { text: content, bgcolor: $color("#f0f5f5"), font: $font(14) }, events: { didChange: sender => { let content = sender.text if (sender.text == '') { content = $file.read('defaultConf/' + settingKeys[idx]).string sender.text = content } write2file(settingKeys[idx], content) } } } let phWidth = placeHolders.reduce((pre, cur) => (cur.length * 10 + pre), 0) + (placeHolders.length + 1) * 5 if (idx == 2) { view.props.accessoryView = { type: "view", props: { height: 44, bgcolor: $color("#ced4d4") }, views: [{ type: "label", props: { text: "完成", align: $align.center }, layout: (make, view) => { make.right.equalTo(view.super) make.top.equalTo(view.super) make.height.equalTo(view.super) make.width.equalTo(60) }, events: { tapped: sender => { $("inputViews").views[2].blur() } } }, { type: "scroll", props: { alwaysBounceHorizontal: true, alwaysBounceVertical: false, showsHorizontalIndicator: false, contentSize: $size(phWidth, 34) }, views: placeHolders.map((i, idx) => { return { type: 'label', props: { text: i, lines: 1, font: $font("bold", 15), align: $align.center, bgcolor: $color("#fff"), radius: 5, textColor: $color('#000') }, layout: (make, view) => { make.size.equalTo($size(i.length * 10, 34)) make.centerY.equalTo(view.super) // make.top.equalTo(view.suepr).offset(5) if (idx > 0) { make.left.equalTo(view.prev.right).offset(5) } else { make.left.equalTo(5) } }, events: { tapped: sender => { let inputView = $("inputViews").views[2] inputView.runtimeValue().$insertText(sender.text) // let sr = inputView.selectedRange // let old = inputView.text.split('') // old.splice(sr.location, sr.length, sender.text) // inputView.text = old.join('') } } } }), layout: (make, view) => { make.left.equalTo(view.super) make.top.equalTo(view.super) make.height.equalTo(view.super).offset(0) make.width.equalTo(view.super).offset(-60) } }] } } inputViewData.push(view) } let genControlBnts = function (idx) { let titleTexts = ['小组件流量', '常规', '代理分组', '代理规则', '本地DNS映射', 'URL重定向', 'SSID', 'Header修改', '主机名', '配置根证书'] const sbgc = colorUtil.getColor("advanceGridOnBg") const stc = colorUtil.getColor("advanceGridOnFg") const dbgc = colorUtil.getColor("advanceGridOffBg") const dtc = colorUtil.getColor("advanceGridOffFg") return titleTexts.map((item, i) => { return { title: { text: item, bgcolor: i === idx ? sbgc : dbgc, radius: 5, color: i == idx ? stc : dtc } } }) } $ui.push({ type: "scroll", props: { title: "进阶设置", navBarHidden: true, statusBarHidden: colorUtil.getColor("statusBar", true) === 'clear' ? true : false, statusBarStyle: colorUtil.getColor("statusBar", true) === '#ffffff' ? 1 : 0, }, views: [{ type: "view", props: { id: "navBar", bgcolor: colorUtil.getColor("navBar") }, layout: (make, view) => { make.height.equalTo(navBarHeight + statusBarHeight) make.width.equalTo(view.super) }, views: [{ type: "label", props: { text: "进阶设置", textColor: colorUtil.getColor("navTitleText"), font: $font("bold", 25) }, layout: (make, view) => { make.height.equalTo(navBarHeight) make.top.equalTo(statusBarHeight) make.left.equalTo(15) } }, { type: "image", props: { icon: $icon("225", colorUtil.getColor("navIconRight"), $size(25, 25)), bgcolor: $color("clear") }, layout: (make, view) => { make.right.equalTo(view.super).offset(-15) make.height.width.equalTo(25) make.bottom.equalTo(view.super).offset(-10) }, events: { tapped: sender => { $ui.pop() } } }] }, { type: "gallery", props: { id: "inputViews", items: inputViewData, interval: 0, smoothRadius: 10, }, layout: (make, view) => { make.height.equalTo(screenHeight - 325 - statusBarHeight - navBarHeight) make.width.equalTo(view.super).offset(-20) make.centerX.equalTo(view.super) make.top.equalTo(navBarHeight + statusBarHeight + 10) }, events: { changed: sender => { let idx = sender.page $("settingsControl").data = genControlBnts(idx) } } }, { type: "matrix", props: { columns: 2, id: "settingsControl", itemHeight: 40, bgcolor: $color("#ffffff"), spacing: 3, data: genControlBnts(0), template: [{ type: "label", props: { id: "title", align: $align.center, font: $font("bold", 14) }, layout: $layout.fill }] }, layout: (make, view) => { make.height.equalTo(220) make.centerX.equalTo(view.super) make.width.equalTo(view.super).offset(-13) make.top.equalTo(view.prev.bottom).offset(5) }, events: { didSelect: (sender, indexPath, data) => { let idx = indexPath.row $("inputViews").page = idx } } }, { type: "label", props: { text: "上述设置点击完成生效,清空保存一次恢复默认", font: $font(12), textColor: $color("#595959"), align: $align.center }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(0) make.width.equalTo(view.super) make.height.equalTo(30) make.centerX.equalTo(view.super) } }, { type: "button", props: { title: '还原全部进阶设置', bgcolor: $color("#ff6840") }, layout: (make, view) => { make.width.equalTo(view.super).offset(-40) make.centerX.equalTo(view.super) make.top.equalTo(view.prev.bottom).offset(10) make.height.equalTo(40) }, events: { tapped: sender => { $ui.alert({ title: "提示", message: "是否还原配置,还原后无法恢复", actions: [{ title: 'Cancel', handler: () => { } }, { title: 'OK', handler: () => { let previewData = JSON.parse($file.read(FILE).string) for (let idx in settingKeys) { let defaultValue = $file.read(`defaultConf/${settingKeys[idx]}`).string previewData[settingKeys[idx]] = defaultValue } $file.write({ data: $data({ "string": JSON.stringify(previewData) }), path: FILE }) $ui.pop() } }] }) } } }] }) } function renderAboutUI() { let previewMD = function (title, filePath) { $ui.push({ props: { title: title }, views: [{ type: "markdown", props: { id: "", content: $file.read(filePath).string }, layout: $layout.fill }] }) } $ui.push({ props: { title: "关于", id: "aboutMainView", navBarHidden: true, statusBarHidden: colorUtil.getColor("statusBar", true) === 'clear' ? true : false, statusBarStyle: colorUtil.getColor("statusBar", true) === '#ffffff' ? 1 : 0, }, views: [{ type: "view", props: { id: "navBar", bgcolor: colorUtil.getColor("navBar") }, layout: (make, view) => { make.height.equalTo(navBarHeight + statusBarHeight) make.width.equalTo(view.super) }, views: [{ type: "label", props: { text: "脚本相关", textColor: colorUtil.getColor("navTitleText"), font: $font("bold", 25) }, layout: (make, view) => { make.height.equalTo(navBarHeight) make.top.equalTo(statusBarHeight) make.left.equalTo(15) } }, { type: "image", props: { icon: $icon("225", colorUtil.getColor("navIconRight"), $size(25, 25)), bgcolor: $color("clear") }, layout: (make, view) => { make.right.equalTo(view.super).offset(-15) make.height.width.equalTo(25) make.bottom.equalTo(view.super).offset(-10) }, events: { tapped: sender => { $ui.pop() } } }] }, { type: "scroll", props: { id: "mainAboutView", contentSize: $size(0, 1000) }, layout: (make, view) => { make.top.equalTo(navBarHeight + statusBarHeight); make.width.equalTo(view.super) make.height.equalTo(view.super).offset(navBarHeight + statusBarHeight) }, views: [{ type: "label", props: { text: "文档说明", font: $font(13), textColor: $color("#505050") }, layout: (make, view) => { make.top.equalTo(view.super).offset(10) make.height.equalTo(30) make.left.equalTo(15) } }, { type: "list", props: { data: ["🗂 脚本简介", "🛠 使用手册", "📃 更新日志", "🖥 论坛导航"], scrollEnabled: false }, layout: (make, view) => { make.width.equalTo(view.super) make.top.equalTo(view.prev.bottom).offset(0) make.height.equalTo(180) }, events: { didSelect: (sender, indexPath, data) => { if (indexPath.row === 0) { previewMD(data, 'docs.md') } else if (indexPath.row === 1) { $safari.open({ url: "https://github.com/Fndroid/jsbox_script/wiki/Rules-lhie1" }) } else if (indexPath.row === 2) { previewMD(data, 'updateLog.md') } else { $safari.open({ url: "https://jsboxbbs.com/d/290-lhie1" }) } } } }, { type: "label", props: { text: "外部拓展", font: $font(13), textColor: $color("#505050") }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(20) make.height.equalTo(30) make.left.equalTo(15) } }, { type: "list", props: { data: ["🤖️ Rules-lhie1托管", "🎩 Quantumult去FA更新"], scrollEnabled: false }, layout: (make, view) => { make.width.equalTo(view.super) make.top.equalTo(view.prev.bottom).offset(0) make.height.equalTo(90) }, events: { didSelect: (sender, indexPath, data) => { if (indexPath.row === 0) { $app.openURL("https://t.me/rules_lhie1_bot") } else { $safari.open({ url: "https://jsboxbbs.com/d/474-quantumult-filter-action", }) } } } }, { type: "label", props: { text: "致谢捐献", font: $font(13), textColor: $color("#505050") }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(20) make.height.equalTo(30) make.left.equalTo(15) } }, { type: "list", props: { data: ["🙏 捐献打赏名单", "👍 赏杯咖啡支持作者"], scrollEnabled: false }, layout: (make, view) => { make.width.equalTo(view.super) make.top.equalTo(view.prev.bottom).offset(0) make.height.equalTo(90) }, events: { didSelect: (sender, indexPath, data) => { if (indexPath.row === 0) { $http.get('https://raw.githubusercontent.com/Fndroid/sponsor_list/master/sponsors.md').then(res => { let success = $file.write({ data: $data({ string: res.data }), path: 'donate.md' }) success && previewMD(data, 'donate.md') }) } else if (indexPath.row === 1) { $ui.alert({ title: '感谢支持', message: '作者投入大量时间和精力对脚本进行开发和完善,你愿意给他赏杯咖啡支持一下吗?', actions: [{ title: "支付宝", handler: () => { $app.openURL($qrcode.decode($file.read("assets/thankyou2.jpg").image)) } }, { title: "微信", handler: () => { $quicklook.open({ image: $file.read("assets/thankyou.jpg").image }) } }, { title: "返回" }] }) } else { // $clipboard.text = 'GxsAtS84U7' // $app.openURL("alipay://") $app.openURL("https://qr.alipay.com/c1x047207ryk0wiaj6m6ye3") } } } }, { type: "label", props: { text: "反馈联系", font: $font(13), textColor: $color("#505050") }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(20) make.height.equalTo(30) make.left.equalTo(15) } }, { type: "list", props: { data: ["📠 Telegram", "💡 GitHub", "📅 Channel"], scrollEnabled: false }, layout: (make, view) => { make.width.equalTo(view.super) make.top.equalTo(view.prev.bottom).offset(0) make.height.equalTo(140) }, events: { didSelect: (sender, indexPath, data) => { if (indexPath.row === 0) { $safari.open({ url: "https://t.me/Rules_lhie1", }) } else if (indexPath.row === 1) { $safari.open({ url: "https://github.com/Fndroid/jsbox_script/tree/master/Rules-lhie1/README.md", }) } else { $safari.open({ url: "https://t.me/Fndroids", }) } } } }, { type: "label", props: { text: "版本号:" + updateUtil.getCurVersion(), font: $font(13), textColor: $color("#505050") }, layout: (make, view) => { make.top.equalTo(view.prev.bottom).offset(20) make.height.equalTo(30) make.centerX.equalTo(view.super) } }] }] }) } function deleteServerGroup() { let serverData = $("serverEditor").data let sections = serverData.map(i => { if (i.title === '') { return '无分组节点' } return i.title }) $ui.menu({ items: sections.concat(['全部删除', '关键字删除']), handler: function (title, idx) { if (title === '全部删除') { $("serverEditor").data = [] } else if (title === '关键字删除') { $input.text({ type: $kbType.default, placeholder: "关键字,空格隔开", text: $("serverControl").info.deleteKeywords || '', handler: function (text) { let keywords = text.split(/\s+/g).filter(i => i !== '') let editorData = $("serverEditor").data editorData.map(section => { section.rows = section.rows.filter(item => keywords.every(k => !(new RegExp(k, 'g')).test(item.proxyName.text))) return section }) $("serverEditor").data = editorData let controlInfo = $("serverControl").info controlInfo.deleteKeywords = text $("serverControl").info = controlInfo saveWorkspace() } }) } else { serverData.splice(idx, 1) $("serverEditor").data = serverData } saveWorkspace() } }) } function reverseServerGroup() { let serverData = $("serverEditor").data let sections = serverData.map(i => i.title) if (sections.length === 1) { serverData[0].rows.reverse() $("serverEditor").data = serverData saveWorkspace() return } $ui.menu({ items: sections.concat(['组别倒序']), handler: function (title, idx) { if (idx === sections.length) { $("serverEditor").data = serverData.reverse() } else { serverData[idx].rows.reverse() $("serverEditor").data = serverData } saveWorkspace() } }) } let filePartReg = function (name) { let reg = `\\[${name}\\]([\\S\\s]*?)(?:\\[General\\]|\\[Replica\\]|\\[Proxy\\]|\\[Proxy Group\\]|\\[Rule\\]|\\[Host\\]|\\[URL Rewrite\\]|\\[Header Rewrite\\]|\\[SSID Setting\\]|\\[MITM\\]|\\[URL-REJECTION\\]|\\[HOST\\]|\\[POLICY\\]|\\[REWRITE\\]|\\[Script\\]|$)` return new RegExp(reg) } function setUpWorkspace() { $app.listen({ ready: function () { $("navLoadingIcon").play({ fromProgress: 0, toProgress: 0.6, }) $app.notify({ name: 'loadData' }) }, resume: () => { }, loadData: () => { let file = JSON.parse($file.read(FILE).string) if (file && file.workspace) { let workspace = file.workspace $("fileName").text = workspace.fileName || '' $("serverSuffixEditor").text = workspace.serverSuffix || '' $("serverURL").info = workspace.withEmoji || false let customProxyGroup = workspace.customProxyGroup || {} let defaultGroupName = PROXY_HEADER if (!(defaultGroupName in customProxyGroup)) { customProxyGroup[defaultGroupName] = [] } let defaultGroup = customProxyGroup[defaultGroupName] $("serverEditor").data = workspace.serverData.map(section => { section.rows.map(item => { item.proxyName = { text: item.proxyName.text } item.proxyAuto = { hidden: !(defaultGroup.indexOf(item.proxyName.text) > -1) } return item }) return section }) let usualSettingsData = workspace.usualData let nd = $("usualSettings").data.map(item => { let sd = usualSettingsData.find(i => i.title.text === item.title.text) if (sd) { item.title.bgcolor = sd.title.bgcolor ? btnOnBg : btnOffBg item.title.textColor = sd.title.textColor ? btnOnFg : btnOffFg } else { item.title.bgcolor = btnOffFg item.title.textColor = btnOffFg } return item }) $("usualSettings").data = nd $("serverControl").info = { deleteKeywords: workspace.deleteKeywords || '', customProxyGroup: customProxyGroup, currentProxyGroup: defaultGroupName } let outputFormat = workspace.outputFormat || 'Surge3' let type = 'surge' if (outputFormat === 'Quantumult') { type = 'quan' } else if (outputFormat === 'Surge 2') { type = 'surge2' } $("outputFormatType").text = outputFormat $("outputFormatIcon").data = $file.read(`assets/today_${type}.png`) } else if (file && !file.workspace) { let customProxyGroup = {} let defaultGroupName = PROXY_HEADER customProxyGroup[defaultGroupName] = [] let defaultGroup = customProxyGroup[defaultGroupName] $("serverControl").info = { deleteKeywords: '', customProxyGroup: customProxyGroup, currentProxyGroup: defaultGroupName } } } }) } function saveWorkspace() { let workspace = { fileName: $("fileName").text, serverData: $("serverEditor").data, withEmoji: $("serverURL").info || false, usualData: $("usualSettings").data.map(i => { i.title.bgcolor = cu.isEqual(btnOnBg, i.title.bgcolor) i.title.textColor = cu.isEqual(btnOnFg, i.title.textColor) return i }), outputFormat: $("outputFormatType").text, serverSuffix: $("serverSuffixEditor").text, deleteKeywords: $("serverControl").info.deleteKeywords || '', customProxyGroup: $("serverControl").info.customProxyGroup || {} } let file = JSON.parse($file.read(FILE).string) file.workspace = workspace $file.write({ data: $data({ string: JSON.stringify(file) }), path: FILE }) } function setDefaultSettings() { let previewData = JSON.parse($file.read(FILE).string) for (let idx in settingKeys) { if (typeof previewData[settingKeys[idx]] === 'undefined' || previewData[settingKeys[idx]] == "") { let defaultValue = ' ' if ($file.exists(`defaultConf/${settingKeys[idx]}`)) { defaultValue = $file.read(`defaultConf/${settingKeys[idx]}`).string } previewData[settingKeys[idx]] = defaultValue } } $file.write({ data: $data({ "string": JSON.stringify(previewData) }), path: FILE }) } function autoGen() { $ui.render({ props: { title: "" }, layout: $layout.fill, views: [{ type: "blur", props: { id: "progressView", style: 1 }, layout: $layout.fill, views: [{ type: "label", props: { id: "loadingHintLabel", text: "处理中,请稍后", textColor: $color("black"), }, layout: (make, view) => { make.centerX.equalTo(view.super) make.centerY.equalTo(view.super).offset(-30) } }, { type: "progress", props: { id: "progressBar", value: 0 }, layout: (make, view) => { make.width.equalTo(screenWidth * 0.8) make.center.equalTo(view.super) make.height.equalTo(3) } }, { type: "button", props: { title: "CLOSE" }, layout: (make, view) => { make.width.equalTo(80) make.top.equalTo(view.prev.bottom).offset(20) make.centerX.equalTo(view.super) }, events: { tapped: sender => { $http.stopServer() $app.close() } } }] }] }) $app.listen({ ready: function () { makeConf({ onProgress: (p, hint) => { hint !== '' && ($("loadingHintLabel").text = hint) $("progressBar").value = p }, onDone: res => { exportConf(res.fileName, res.fileData, res.target, res.actionSheet, true, () => { $http.stopServer() $app.close() }) }, onError: res => { $ui.alert("无法生成配置文件,可能是规则仓库发生变化或网络出现问题") } }) } }) } function makeConf(params) { 'onStart' in params && params.onStart() try { let pu = { prototype: "https://raw.githubusercontent.com/lhie1/Rules/master/Surge/Prototype.conf", apple: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/Apple.conf', direct: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/DIRECT.conf', proxy: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/PROXY.conf', reject: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/REJECT.conf', testflight: 'https://raw.githubusercontent.com/lhie1/Rules/master/Surge/TestFlight.conf', host: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/HOST.conf', urlrewrite: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/URL%20Rewrite.conf', urlreject: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/URL%20REJECT.conf', headerrewrite: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/Header%20Rewrite.conf', hostname: 'https://raw.githubusercontent.com/lhie1/Rules/master/Auto/Hostname.conf', mitm: 'https://raw.githubusercontent.com/lhie1/Rules/master/Surge/MITM.conf', quanretcp: 'https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult.conf', quanextra: 'https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult_Extra_JS.conf', quanrejection: 'https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult_URL.conf', localhost: 'http://127.0.0.1/fndroid' } let advanceSettings = JSON.parse($file.read(FILE).string) let workspace = advanceSettings.workspace let usualData = workspace.usualData let customProxyGroup = workspace.customProxyGroup let usualValue = function (key) { return usualData.find(i => i.title.text == key) ? usualData.find(i => i.title.text == key).title.bgcolor : false } let ads = usualValue('ADS') let isMitm = usualValue('MITM') let isActionSheet = usualValue('导出') let outputFormat = workspace.outputFormat let surge2 = outputFormat === 'Surge 2' let isQuan = outputFormat === 'Quantumult' let testflight = outputFormat === 'Surge 3 TF' let serverEditorData = workspace.serverData if (isQuan) { serverEditorData = serverEditorData.map(i => { let rows = i.rows.map(s => { let containsOP = /obfs_param/.test(s.proxyLink) s.proxyLink = s.proxyLink.replace(/,\s*group\s*=[^,]*/, '') if (containsOP) { s.proxyLink = s.proxyLink.replace(/obfs_param/, `group=${i.title}, obfs_param`) } else { s.proxyLink += `, group=${i.title}` } return s }) i.rows = rows return i }) } let flatServerData = serverEditorData.reduce((all, cur) => { return { rows: all.rows.concat(cur.rows) } }, { rows: [] }).rows let proxyNameLegal = function (name) { return flatServerData.map(i => i.proxyName.text).concat(getProxyGroups()).concat(['🚀 Direct']).find(i => i === name) !== undefined } let proxySuffix = workspace.serverSuffix.split(/\s*,\s*/g).map(i => i.replace(/\s/g, '')).filter(i => i !== '') let proxies = flatServerData.map(i => { let notExistSuffix = proxySuffix.filter((ps, idx) => { if (idx === 0 && ps === '') return true return i.proxyLink.indexOf(ps) < 0 }) let containsOP = /obfs_param/.test(i.proxyLink) if (containsOP) { i.proxyLink = i.proxyLink.replace(/obfs_param/, `${notExistSuffix.join(',')},obfs_param`) } else if (notExistSuffix.length > 0) { i.proxyLink += `,${notExistSuffix.join(',')}` } return i.proxyLink.replace('http://omgib13x8.bkt.clouddn.com/SSEncrypt.module', 'https://github.com/lhie1/Rules/blob/master/SSEncrypt.module?raw=true') }).filter((item, idx, self) => { let proxyName = item.split(/=/)[0].trim() return self.findIndex(i => { let pn = i.split('=')[0].trim() return proxyName === pn }) === idx }) proxies = proxies.join('\n') let proxyHeaders = flatServerData.map(i => i.proxyName.text).join(', ') let rules = '' let prototype = '' let host = '' let urlRewrite = '' let urlReject = '' let headerRewrite = '' let hostName = '' let rename = null let rulesReplacement = getRulesReplacement() let pgs = 0 let onPgs = function (hint) { pgs += 0.1 'onProgress' in params && params.onProgress(pgs, hint) } let emptyPromise = function (done, hint = '') { if (done) done(hint) return Promise.resolve('') } let promiseArray = [ getAutoRules(pu.prototype, onPgs, '成功取回配置模板'), // 0 rulesReplacement ? getAutoRules(rulesReplacement, onPgs, '成功取回替换配置') : getAutoRules(isQuan || testflight ? pu.direct : pu.apple, onPgs, '成功取回APPLE规则'), // 1 !ads || rulesReplacement ? emptyPromise(onPgs) : getAutoRules(isQuan || testflight ? pu.localhost : pu.reject, onPgs, '成功取回Reject规则'), // 2 rulesReplacement ? emptyPromise(onPgs) : getAutoRules(isQuan || testflight ? pu.quanretcp : pu.proxy, onPgs, '成功取回Proxy规则'), // 3 rulesReplacement ? emptyPromise(onPgs) : getAutoRules(isQuan || testflight ? pu.quanextra : pu.direct, onPgs, '成功取回Direct规则'), // 4 rulesReplacement ? emptyPromise(onPgs) : getAutoRules(pu.host, onPgs, '成功取回Host'), // 5 rulesReplacement ? emptyPromise(onPgs) : getAutoRules(pu.urlrewrite, onPgs, '成功取回URL Rewrite'), // 6 !ads || rulesReplacement ? emptyPromise(onPgs) : getAutoRules(isQuan ? pu.quanrejection : pu.urlreject, onPgs, '成功取回URL Reject'), // 7 rulesReplacement ? emptyPromise(onPgs) : getAutoRules(pu.headerrewrite, onPgs, '成功取回Header Rewrite'), // 8 !ads || rulesReplacement ? emptyPromise(onPgs) : getAutoRules(pu.hostname, onPgs, '成功取回MITM Hostname'), // 9 ] // 获取RULE-SET let ruleSets = [] if (!testflight) { ruleSets = advanceSettings.customSettings.split(/[\r\n]/g).map(i => { if (/^RULE-SET\s*,\s*(.*?)\s*,\s*(.*)/.test(i)) { return { url: RegExp.$1, policy: RegExp.$2 } } return null }).filter(i => i) } console.log('ruleSets', ruleSets); promiseArray = promiseArray.concat(ruleSets.map(i => getAutoRules(i.url))) Promise.all(promiseArray).then(v => { prototype = v[0] if (rulesReplacement) { let repRules = v[1].match(filePartReg('Rule')) let repHost = v[1].match(filePartReg('Host')) let repUrlRewrite = v[1].match(filePartReg('URL Rewrite')) let repHeaderRewrite = v[1].match(filePartReg('Header Rewrite')) let repHostName = v[1].match(/hostname\s*=\s*(.*?)[\n\r]/) repRules && repRules[1] && (v[1] = repRules[1]) repHost && repHost[1] && (v[5] = repHost[1]) repUrlRewrite && repUrlRewrite[1] && (v[6] = '[URL Rewrite]\n' + repUrlRewrite[1]) repHeaderRewrite && repHeaderRewrite[1] && (v[8] = '[Header Rewrite]\n' + repHeaderRewrite[1]) repHostName && repHostName[1] && (v[9] = repHostName[1]) } if (isQuan && !rulesReplacement && /\[TCP\]([\s\S]*)\/\/ Detect local network/.test(v[3])) { let tcpRules = `${v[4]}\n${RegExp.$1}`.split(/[\n\r]+/g) if (!ads) { tcpRules = tcpRules.filter(i => !/^.*?,\s*REJECT\s*$/.test(i)) } let surgeLan = v[1].split(/[\r\n]/g).filter(i => /.*?,\s*DIRECT/.test(i)) tcpRules = tcpRules.map(r => { if (surgeLan.indexOf(r) > -1) return r r = r.replace(/(^.*?,.*?,\s*)选择YouTube Music的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择TVB\/Viu的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择Vidol的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择Hulu的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择Spotify的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择Google的策略,不懂就不选(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择微软服务的策略,不懂就选择DIRECT(.*$)/, '$1🍂 Domestic$2') r = r.replace(/(^.*?,.*?,\s*)选择PayPal的策略,不懂就选择DIRECT(.*$)/, '$1🍂 Domestic$2') r = r.replace(/(^.*?,.*?,\s*)选择Apple的策略,不懂就选择DIRECT(.*$)/, '$1🍎 Only$2') r = r.replace(/(^.*?,.*?,\s*)选择Netflix的策略,不懂就不选(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)选择国外流媒体的策略(.*$)/, '$1🍃 Proxy$2') r = r.replace(/(^.*?,.*?,\s*)DIRECT(.*$)/i, '$1🍂 Domestic$2') r = r.replace(/(^.*?,.*?,\s*)PROXY(.*$)/i, '$1🍃 Proxy$2') r = r.replace(/^DOMAIN(.*?)🍃 Proxy\s*$/, 'DOMAIN$1🍃 Proxy,force-remote-dns') return r }) v[1] = tcpRules.join('\n') v[2] = '' v[3] = '' v[4] = '' v[7] = v[7].replace(/hostname = /, '# hostname = ') } if (testflight && !rulesReplacement) { let autoNewPrefix = 'https://raw.githubusercontent.com/lhie1/Rules/master/Surge/Surge%203/Provider' v[1] = `RULE-SET,SYSTEM,DIRECT\nRULE-SET,${autoNewPrefix}/Apple.list,🍎 Only` v[2] = ads ? `RULE-SET,${autoNewPrefix}/Reject.list,REJECT` : '' v[3] = `RULE-SET,${autoNewPrefix}/AsianTV.list,🍂 Domestic\nRULE-SET,${autoNewPrefix}/GlobalTV.list,🍃 Proxy\nRULE-SET,${autoNewPrefix}/Proxy.list,🍃 Proxy` v[4] = `RULE-SET,${autoNewPrefix}/Domestic.list,🍂 Domestic\nRULE-SET,LAN,DIRECT` } rules += `\n${v[1]}\n${v[2].replace(/REJECT/g, surge2 || isQuan ? "REJECT" : "REJECT-TINYGIF")}\n${v[3]}\n${v[4]}\n` host = v[5] urlRewrite += v[6] urlReject += v[7] headerRewrite = v[8] hostName = v[9].split('\n') let seperateLines = function (content, rules = false) { let addRules = content.split('\n').filter(i => !/^-/.test(i)).map(i => i.trim()) if (rules && !testflight && promiseArray.length > 10) { addRules = addRules.filter(i => !/^\s*RULE-SET/.test(i)) for (let i = 10; i < promiseArray.length; i++) { let policy = ruleSets[i - 10].policy addRules = addRules.concat(v[i].split(/[\r\n]/g).map(i => { console.log('i', i); if (/^(.+?),(.+?),(.+)$/.test(i)) { return `${RegExp.$1 + ',' + RegExp.$2},${policy},${RegExp.$3}${!testflight ? ',force-remote-dns' : ''}` }else if (/^(.+?),(.+?)(?=$|\/\/|\#)/.test(i)) { return `${RegExp.$1},${RegExp.$2},${policy}${!testflight ? ',force-remote-dns' : ''}` } return i })) } } console.log('adru', addRules) let res = { add: addRules, delete: content.split("\n").filter(i => /^-/.test(i)).map(i => i.replace(/^-/, '').trim()) } return res } let prettyInsert = function (lines) { return '\n\n' + lines.join('\n') + '\n\n' } // 配置代理分组 if (advanceSettings.proxyGroupSettings) { let pgs = advanceSettings.proxyGroupSettings rename = pgs.match(/\/\/\s*rename\s*:\s*(.*?)(?:\n|\r|$)/) pgs = pgs.replace(/Proxy Header/g, proxyHeaders) for (let name in customProxyGroup) { let nameReg = new RegExp(`,\\s*${name}`, 'g') let serverNames = customProxyGroup[name] serverNames = serverNames.filter(i => proxyNameLegal(i)) pgs = pgs.replace(nameReg, ',' + (serverNames.join(',') || flatServerData.map(i => i.proxyName.text).join(','))) } prototype = prototype.replace(/\[Proxy Group\][\s\S]+\[Rule\]/, pgs + '\n\n[Rule]') } else { prototype = prototype.replace(/Proxy Header/g, proxyHeaders) prototype = prototype.replace(/ProxyHeader/g, customProxyGroup[PROXY_HEADER].filter(i => proxyNameLegal(i)).join(',') || flatServerData.map(i => i.proxyName.text).join(',')) } // 配置常规设置 if (advanceSettings.generalSettings) { prototype = prototype.replace(/\[General\][\s\S]+\[Proxy\]/, advanceSettings.generalSettings + '\n\n[Proxy]') } // 配置自定义规则 let customRules = seperateLines(advanceSettings.customSettings, true) let rulesList = rules.split(/[\r\n]/g) let deleteList = customRules.add.map(i => { if (/^(.*?),(.*?),/.test(i)) { let type = RegExp.$1 let content = RegExp.$2 return `${type},${content}` } }) rules = rulesList.filter(i => deleteList.findIndex(d => i.startsWith(d)) === -1).join('\n') customRules.delete.forEach(i => rules = rules.replace(i, '')) // 配置本地DNS映射 let userHost = seperateLines(advanceSettings.hostSettings) userHost.delete.forEach(i => host = host.replace(i, '')) // 配置URL重定向 let userUrl = seperateLines(advanceSettings.urlrewriteSettings) userUrl.delete.forEach(i => { urlRewrite = urlRewrite.replace(i, '') urlReject = urlReject.replace(i, '') }) // 配置Header修改 let userHeader = seperateLines(advanceSettings.headerrewriteSettings) userHeader.delete.forEach(i => headerRewrite = headerRewrite.replace(i, '')) // 配置SSID let userSSID = advanceSettings.ssidSettings // 配置MITM的Hostname let userHostname = seperateLines(advanceSettings.hostnameSettings) userHostname.delete.forEach(i => { if (hostName.indexOf(i) >= 0) { hostName.splice(hostName.indexOf(i), 1) } }) function ssr2ss(proxies) { let proxyList = proxies.split(/\n/); let res = proxyList.map(proxy => { if (/=\s*shadowsocksr/.test(proxy)) { return proxy.replace(/=\s*shadowsocksr/g, '= custom').replace(/"/g, '').replace(/,\s*(protocol|protocol_param|obfs|obfs_param)[^,$]+/g, '') + ', https://github.com/lhie1/Rules/blob/master/SSEncrypt.module?raw=true' } else { return proxy } }) return res.join('\n') } // if (isQuan) { // prototype = prototype.replace(/\/\/ Detect local network/, `${prettyInsert(customRules.add)}\n`) // } else { prototype = prototype.replace('# Custom', prettyInsert(customRules.add)) // } prototype = prototype.replace('Proxys', isQuan ? proxies : ssr2ss(proxies)) if (rulesReplacement) { prototype = prototype.replace(/\[Rule\][\s\S]*FINAL\s*,[^\r\n]+/, `[Rule]\n${prettyInsert(customRules.add)}\n${rules}\n`) } else { prototype = prototype.replace('# All Rules', rules) } prototype = prototype.replace('# Host', "[Host]\n" + host + prettyInsert(userHost.add)) prototype = prototype.replace('# URL Rewrite', urlRewrite.replace(/307/g, surge2 ? '302' : '307') + prettyInsert(userUrl.add)) prototype = prototype.replace('# URL REJECT', urlReject) prototype = prototype.replace('# SSID', '[SSID Setting]\n' + userSSID) prototype = prototype.replace('# Header Rewrite', headerRewrite + prettyInsert(userHeader.add)) //fix by shenqinci 2019年03月20日:当只有userHostName时,配置文件出现第一行为空的问题 let finalHostNames = hostName.concat(userHostname.add).filter(i => i).join(', ') if (finalHostNames !== '') { prototype = prototype.replace('// Hostname', 'hostname = ' + finalHostNames) } if (isMitm) { prototype = prototype.replace('# MITM', advanceSettings.mitmSettings) } else { prototype = prototype.replace(/\[MITM\][\s\S]*$/, '') } function genQuanPolices(content) { let items = content.split(/[\n\r]+/).filter(i => i !== '' && !/^\/\//.test(i)).map(sta => { let matcher = sta.match(/^(.*?)=(.*?),(.*?)$/); if (/^(.*?)=(.*?),(.*?)$/.test(sta)) { let pName = RegExp.$1 let pType = RegExp.$2 let pNodes = RegExp.$3 let data = pNodes.split(/,/g) if (/url-test/.test(pType) || /fallback/.test(pType)) { let v = data.filter(i => !/(?:url|interval|tolerance|timeout)\s*=\s*/.test(i)) return { name: pName, sta: ' auto', data: v } } else if (/select/.test(pType)) { return { name: pName, sta: pType.replace(/select/, 'static'), data: data } } else if (/round-robin/.test(pType)) { return { name: pName, sta: 'balance, round-robin', data: data } } else { return { name: pName, sta: 'ssid', data: data } } } else { return null } }).filter(i => i !== null) items.push({ name: '🚀 Direct', sta: 'static', data: ["DIRECT"] }) let policies = items.map(i => { if (i.sta.contains('auto') || i.sta.contains('balance, round-robin')) { return `${i.name} : ${i.sta}\n${i.data.join('\n')}` } else if (i.sta.contains('static')) { return `${i.name} : ${i.sta}, ${i.data[0]}\n${i.data.join('\n')}` } else if (i.sta === 'ssid') { let wifi = i.data.find(i => /default\s*=/.test(i)) let cellular = i.data.find(i => /cellular\s*=/.test(i)) || 'cellular = DIRECT' let left = i.data.filter(i => i !== wifi && i !== cellular).map(i => { let p = i.split('=') return p[0].replace(/"/g, '') + '=' + p.slice(1).join('=') }) return `${i.name} : ${wifi.replace(/default\s*=/, 'wifi =')}, ${cellular}\n${left.join('\n')}` } }) return policies.map(i => { if (rename && rename[1]) { i = globalRename(rename, i) // 圈特殊性 } return $text.base64Encode(i) }) } function seperateRejection(reject) { let lines = reject.split(/[\r\n]+/) let res = { reject: [], rewrite: []} lines.forEach(l => { if (/(.*?\s+\-\s+reject)/.test(l)) { res.reject.push(RegExp.$1) } else if (/(.*?\s+url\s(302|307|modify|simple\-response)\s.*$)/.test(l)) { res.rewrite.push(RegExp.$1) } }) return res } function genQuanRewrite(content) { let items = content.split(/[\n\r]+/).filter(i => i !== '' && /^(?!\/\/|#)/.test(i)).map(i => { if (/^(.*?)\s+(.*?)\s+(.*?)\s*$/.test(i)) { let type = RegExp.$3 return `${RegExp.$1} url ${type === 'header' ? 'modify' : type} ${RegExp.$2}` } return '' }).join('\n') return items } function genQuanRewriteTinyPng(reject, rewrite) { let rejects = reject.split(/[\n\r]/g).filter(i => /.*?\s*-\s*reject/.test(i)).map(i => i.replace(/(.*?)\s*-\s*reject\s*$/, '$1')) let items = rejects.map(i => `${i} url simple-response SFRUUC8xLjEgMjAwIE9LDQpTZXJ2ZXI6IG5naW54DQpDb250ZW50LVR5cGU6IGltYWdlL3BuZw0KQ29udGVudC1MZW5ndGg6IDU2DQpDb25uZWN0aW9uOiBjbG9zZQ0KDQqJUE5HDQoaCgAAAA1JSERSAAAAAQAAAAEIBgAAAB8VxIkAAAALSURBVHicY2AAAgAABQABel6rPw==`) items = items.concat(rewrite.split(/[\n\r]+/).filter(i => i !== '' && /^(?!\/\/|#)/.test(i)).map(i => { if (/^(.*?)\s+(.*?)\s+(.*?)\s*$/.test(i)) { let type = RegExp.$3 return `${RegExp.$1} url ${type === 'header' ? 'modify' : type} ${RegExp.$2}` } return '' })) return items.join('\n') } function genQuanPart(name, content) { return `\n[${name}]\n${content}\n` } if (isQuan) { prototype = prototype.replace(/☁️ Others, dns-failed/, '☁️ Others') let proxyGroup = prototype.match(filePartReg('Proxy Group')) if (proxyGroup && proxyGroup[1]) { let policies = genQuanPolices(proxyGroup[1]) prototype += genQuanPart('POLICY', policies.join('\n')) } userUrl.add.forEach(i => { // if (/reject\s*$/.test(i)) { // urlReject += `\n${i}\n` // } else { // urlRewrite += `\n${i}\n` // } let rule = i if (/(.*?)\s+(.*?)\s+(307|302|header|reject)\s*$/.test(i)) { if (RegExp.$3 === 'reject') { rule = i } else if (RegExp.$3 === 'header') { rule = `${RegExp.$1} url modify ${RegExp.$2}` } else { rule = `${RegExp.$1} url ${RegExp.$3} ${RegExp.$2}` } } urlReject = `\n${rule}\n${urlReject}` }) let quanRe = seperateRejection(urlReject) prototype += genQuanPart('URL-REJECTION', quanRe.reject.join('\n')) prototype += genQuanPart('REWRITE', quanRe.rewrite.join('\n')) // prototype += genQuanPart('REWRITE', genQuanRewriteTinyPng(urlReject, urlRewrite)) prototype += genQuanPart('HOST', host + prettyInsert(userHost.add)) let sourceType = 'false, true, false'; let sourceTypeParam = proxySuffix.find(x => /\s*source-type\s*=\s*[0-7]\s*(?:,|$)/.test(x)) if (sourceTypeParam) { let type = sourceTypeParam.match(/\s*source-type\s*=\s*([0-7])/)[1] * 1; sourceType = `${type & 4 ? 'true' : 'false'}, ${type & 2 ? 'true' : 'false'}, ${type & 1 ? 'true' : 'false'}` } console.log(serverEditorData) prototype += genQuanPart('SOURCE', serverEditorData.filter(i => { // let isSSR = i.rows.find(l => /^.*?=\s*(?=shadowsocksr|vmess)/.test(l.proxyLink)) // return isSSR !== undefined return i.rows.find(i => i.proxyType > 0) }).map(i => { return `${i.title}, server, ${i.url}, ${sourceType}, ${i.title}` }).join('\n') + (rulesReplacement ? "" : "\nlhie1, filter, https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult.conf, true\nlhie1_extra, filter, https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult_Extra.conf, true\nlhie1, blacklist, https://raw.githubusercontent.com/lhie1/Rules/master/Quantumult/Quantumult_URL.conf, true\n")) let customDNS = prototype.match(/dns-server\s*=\s*(.*?)(?:\n|\r|$)/) if (customDNS && customDNS[1]) { prototype += genQuanPart('DNS', customDNS[1]) } let widgetProxies = customProxyGroup['WidgetHeader'] || null if (widgetProxies) { widgetProxies = widgetProxies.filter(i => proxyNameLegal(i)) prototype += genQuanPart('BACKUP-SERVER', widgetProxies.join('\n')) } prototype = prototype.replace(/\[SSID Setting\]/, "[SUSPEND-SSID]").replace(/\ssuspend=true/g, '') } if (rename && rename[1]) { prototype = globalRename(rename, prototype); } let fn = (workspace.fileName || 'lhie1') + '.conf' let exportTarget = 0 if (surge2) { exportTarget = 1 } if (isQuan) { exportTarget = 2 } if ('onDone' in params) { ruleUpdateUtil.getGitHubFilesSha({ handler: sha => { if (sha) { ruleUpdateUtil.setFilesSha(sha) } params.onDone({ target: exportTarget, actionSheet: isActionSheet, fileName: fn, fileData: prototype }) } }) } }).catch(e => { console.error(e.stack) }) } catch (e) { console.error(e.stack) 'onError' in params && params.onError(e) } function globalRename(rename, prototype) { let renamePat = rename[1].split(/\s*,\s*/g).filter(i => i.indexOf('=') > -1).map(i => { let sp = i.reverse().split(/\s*=(?!\\)\s*/g); return sp.map(i => i.reverse().trim().replace(">", ',')).reverse(); }); console.log(renamePat) renamePat.forEach(i => { let oldName = i[0]; let newName = i[1].replace(/\\=/g, '='); let oldNameReg = new RegExp(oldName, 'g'); prototype = prototype.replace(oldNameReg, newName); }); return prototype; } } function getRulesReplacement(content = '') { let advanceSettings = content ? content : JSON.parse($file.read(FILE).string) if (advanceSettings.customSettings) { let cs = advanceSettings.customSettings; let pat = cs.match(/\/\/\s*replacement\s*:\s*(.*?)(?:\n|\r|$)/); if (pat && pat[1]) { return pat[1]; } } return null; } function exportConf(fileName, fileData, exportTarget, actionSheet, isAuto, actionSheetCancel) { let surge3 = exportTarget === 0 let surge2 = exportTarget === 1 let isQuan = exportTarget === 2 if (surge2 || surge3) { let fnReg = /^[\x21-\x2A\x2C-\x2E\x30-\x3B\x3D\x3F-\x5B\x5D\x5F\x61-\x7B\x7D-\x7E]+$/ if (actionSheet || !fnReg.test(fileName)) { $share.sheet({ items: [fileName, $data({ "string": fileData })], handler: success => { if (!success && actionSheetCancel) { actionSheetCancel() } } }) } else { if (!$file.exists("confs")) { $file.mkdir("confs") } else { $file.list('confs').forEach(i => $file.delete('confs/' + i)) } $file.write({ data: $data({ "string": fileData }), path: `confs/${fileName}` }) $http.startServer({ path: "confs/", handler: res => { let serverUrl = `http://127.0.0.1:${res.port}/` $http.get({ url: serverUrl + "list?path=", handler: function (resp) { if (resp.response.statusCode == 200) { let surgeScheme = `surge${surge2 ? "" : "3"}:///install-config?url=${encodeURIComponent(serverUrl + "download?path=" + fileName)}` $app.openURL(surgeScheme) $delay(10, () => { $http.stopServer() if (isAuto) { $app.close() } }) } else { $ui.alert("内置服务器启动失败,请重试") } } }) } }) } } else if (isQuan) { if (actionSheet) { $share.sheet({ items: [fileName, $data({ "string": fileData })], handler: success => { if (!success && actionSheetCancel) { actionSheetCancel() } } }) } else { $clipboard.text = fileData $app.openURL("quantumult://settings?configuration=clipboard&autoclear=1") } } function genServerFiles(name, data) { $file.write({ data: $data({ "string": data }), path: `confs/${name}` }); } } function urlsaveBase64Encode(url) { return $text.base64Encode(url).replace(/\+/g, '-').replace(/\\/g, '_').replace(/=/g, '') } module.exports = { renderUI: renderUI, setUpWorkspace: setUpWorkspace, autoGen: autoGen, getRulesReplacement: getRulesReplacement }