You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3013 lines
100 KiB
JavaScript

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
}