├── docs └── screenshot.png ├── Makefile ├── root └── usr │ ├── share │ ├── rpcd │ │ └── acl.d │ │ │ └── luci-app-simple-clash.json │ └── luci │ │ └── menu.d │ │ └── luci-app-simple-clash.json │ └── libexec │ └── rpcd │ └── luci.clash ├── htdocs └── luci-static │ └── resources │ └── view │ └── clash │ ├── logview.js │ └── overview.js ├── README.md └── .github └── workflows └── build.yml /docs/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandelures/luci-app-simple-clash/HEAD/docs/screenshot.png -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include $(TOPDIR)/rules.mk 2 | 3 | LUCI_TITLE:=LuCI Support for Clash 4 | LUCI_DEPENDS:=+clash 5 | LUCI_PKGARCH:=all 6 | 7 | PKG_MAINTAINER:=Chandelure Wang 8 | PKG_LICENSE:=MIT 9 | PKG_VERSION:=0.1.4 10 | 11 | include $(TOPDIR)/feeds/luci/luci.mk 12 | 13 | # call BuildPackage - OpenWrt buildroot signature 14 | -------------------------------------------------------------------------------- /root/usr/share/rpcd/acl.d/luci-app-simple-clash.json: -------------------------------------------------------------------------------- 1 | { 2 | "luci-app-simple-clash": { 3 | "description": "Grant UCI access for clash", 4 | "read": { 5 | "ubus": { 6 | "luci.clash": ["get_service_status", "update_profile"], 7 | "luci": ["setInitAction"] 8 | }, 9 | "uci": ["clash"], 10 | "file": { 11 | "/etc/clash/profiles/*": ["read"], 12 | "/sbin/logread": ["exec"] 13 | } 14 | }, 15 | "write": { 16 | "uci": ["clash"], 17 | "file": { 18 | "/etc/clash/profiles/*": ["write"] 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /root/usr/share/luci/menu.d/luci-app-simple-clash.json: -------------------------------------------------------------------------------- 1 | { 2 | "admin/services/clash": { 3 | "title": "Simple Clash", 4 | "action": { 5 | "type": "alias", 6 | "path": "admin/services/clash/overview" 7 | }, 8 | "depends": { 9 | "acl": ["luci-app-simple-clash"], 10 | "uci": { 11 | "clash": true 12 | } 13 | } 14 | }, 15 | "admin/services/clash/overview": { 16 | "title": "Overview", 17 | "order": 10, 18 | "action": { 19 | "type": "view", 20 | "path": "clash/overview" 21 | } 22 | }, 23 | "admin/services/clash/logview": { 24 | "title": "Log View", 25 | "order": 20, 26 | "action": { 27 | "type": "view", 28 | "path": "clash/logview" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/clash/logview.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "require view"; 3 | "require poll"; 4 | "require fs"; 5 | 6 | return view.extend({ 7 | load: function () { 8 | return Promise.all([L.resolveDefault(fs.stat("/sbin/logread"), null)]); 9 | }, 10 | render: function (stat) { 11 | var logger = stat[0] ? stat[0].path : null; 12 | poll.add(function () { 13 | return L.resolveDefault(fs.exec_direct(logger, ["-e", "clash"])).then( 14 | function (res) { 15 | var log = document.getElementById("logfile"); 16 | if (res) { 17 | log.value = res.trim(); 18 | } else { 19 | log.value = _(""); 20 | } 21 | log.scrollTop = log.scrollHeight; 22 | } 23 | ); 24 | }); 25 | return E( 26 | "div", 27 | { class: "cbi-map" }, 28 | E("div", { class: "cbi-section" }, [ 29 | E("textarea", { 30 | id: "logfile", 31 | style: "width: 100% !important; padding: 5px; font-family: monospace", 32 | readonly: "readonly", 33 | wrap: "off", 34 | rows: 25, 35 | }), 36 | ]) 37 | ); 38 | }, 39 | handleSaveApply: null, 40 | handleSave: null, 41 | handleReset: null, 42 | }); 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Luci App Simple Clash 2 | 3 | [![build](https://github.com/chandelures/luci-app-simple-clash/workflows/build/badge.svg)](https://github.com/chandelures/luci-app-simple-clash/actions) 4 | 5 | ## Description 6 | 7 | LuCI support for Clash. Configuration of clash based on https://github.com/chandelures/openwrt-clash to build your tunnel on Openwrt. 8 | 9 | ## Installation 10 | 11 | ### Manual Install 12 | 13 | 1. Update list of available packages 14 | 15 | ```shell 16 | $ opkg update 17 | ``` 18 | 19 | 2. Use Opkg package manager to install .ipk from release page 20 | 21 | ```shell 22 | $ opkg install luci-app-simple-clash_*_all.ipk 23 | ``` 24 | 25 | ### Build From Source 26 | 27 | 1. Download Openwrt Source Code or SDK as the basic enviroment to build the package. 28 | 29 | ```shell 30 | $ git clone https://github.com/openwrt/openwrt 31 | $ cd openwrt 32 | 33 | # or 34 | 35 | $ wget https://downloads.openwrt.org/path/to/openwrt-sdk_*.tar.xz 36 | $ tar -Jxvf openwrt-sdk_*.tar.xz 37 | $ cd openwrt-sdk_* 38 | ``` 39 | 40 | 2. Prepare build environment 41 | 42 | ```shell 43 | $ ./scripts/feeds update -a 44 | $ ./scripts/feeds install -a 45 | 46 | $ git clone https://github.com/chandelures/openwrt-clash package/openwrt-clash 47 | $ git clone https://github.com/chandelures/luci-app-simple-clash package/luci-app-simple-clash 48 | ``` 49 | 50 | 3. Choose luci-app-simple-clash as a module or built-in module 51 | 52 | ```shell 53 | $ make menuconfig 54 | 55 | ... 56 | 57 | LuCI ---> 58 | Applications ---> 59 | luci-app-simple-clash 60 | 61 | ... 62 | 63 | ``` 64 | 65 | 4. Build packages 66 | 67 | ```shell 68 | $ make package/luci-app-simple-clash/{clean,compile} V=s 69 | ``` 70 | 71 | ## Screenshot 72 | 73 | ![screenshot](https://github.com/chandelures/luci-app-simple-clash/raw/master/docs/screenshot.png) 74 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | 8 | jobs: 9 | release: 10 | name: Build and Release 11 | runs-on: ubuntu-latest 12 | env: 13 | PACKAGE_NAME: luci-app-simple-clash 14 | SDK_URL_PATH: https://downloads.openwrt.org/snapshots/targets/x86/64 15 | SDK_NAME: -sdk-x86-64_ 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Install Dependencies 19 | run: | 20 | sudo apt-get update 21 | sudo apt-get install -yq gettext libncurses5-dev rsync xsltproc 22 | - name: Create Directories 23 | run: | 24 | echo "SDK_DL_DIR=$(mktemp -d)" >> $GITHUB_ENV 25 | echo "SDK_HOME=$(mktemp -d)" >> $GITHUB_ENV 26 | - name: Prepare Build Environment 27 | run: | 28 | cd "$SDK_DL_DIR" 29 | if ! ( wget -q -O - "$SDK_URL_PATH/sha256sums" | grep -- "$SDK_NAME" > sha256sums.small 2>/dev/null ) ; then 30 | echo "Can not find ${SDK_NAME} file in sha256sums." 31 | exit 1 32 | fi 33 | SDK_FILE="$(cat sha256sums.small | cut -d' ' -f2 | sed 's/*//g')" 34 | wget -q -O "$SDK_FILE" "$SDK_URL_PATH/$SDK_FILE" 35 | if ! sha256sum -c ./sha256sums.small >/dev/null 2>&1 ; then 36 | echo "SDK can not be verified!" 37 | exit 1 38 | fi 39 | tar -Jxf "$SDK_DL_DIR/$SDK_FILE" -C "$SDK_HOME" --strip=1 40 | - name: Build Packages 41 | run: | 42 | cd "$SDK_HOME" 43 | ./scripts/feeds update luci > /dev/null 2>&1 44 | ln -s "${{ github.workspace }}" "package/$PACKAGE_NAME" 45 | make defconfig > /dev/null 2>&1 46 | make package/${PACKAGE_NAME}/compile V=s > /dev/null 47 | find "$SDK_HOME/bin" -type f -name "${PACKAGE_NAME}_*.ipk" \ 48 | -exec cp -f {} "${{ github.workspace }}" \; 49 | 50 | - name: Release and Upload Assets 51 | uses: softprops/action-gh-release@v1 52 | with: 53 | files: "*.ipk" 54 | env: 55 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /root/usr/libexec/rpcd/luci.clash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env lua 2 | 3 | local json = require "luci.jsonc" 4 | local sys = require "luci.sys" 5 | 6 | local update_profile_script = "/usr/lib/clash/update_profile.sh" 7 | 8 | local methods = { 9 | get_service_status = { 10 | call = function() 11 | local res = {} 12 | res["enabled"] = sys.call("pidof clash >/dev/null") == 0 13 | res["version"] = sys.exec("clash -v") 14 | res["dashboard"] = sys.call('opkg list-installed | grep "clash-dashboard" >/dev/null') == 0 15 | return res 16 | end 17 | }, 18 | update_profile = { 19 | args = {profile_name = "profile_name"}, 20 | call = function(args) 21 | local res = {} 22 | if not args or not args.profile_name then 23 | res["success"] = false 24 | res["message"] = "Require profile name" 25 | return res 26 | end 27 | local profile_name = args.profile_name 28 | if sys.call(update_profile_script .. " " .. profile_name) == 0 then 29 | res["success"] = true 30 | res["message"] = "Update profile " .. profile_name .. " finished" 31 | return res 32 | end 33 | res["success"] = false 34 | res["message"] = "Update profile " .. profile_name .. " failed" 35 | return res 36 | end 37 | } 38 | } 39 | 40 | local function parseInput() 41 | local parse = json.new() 42 | local done, err 43 | 44 | while true do 45 | local chunk = io.read(4096) 46 | if not chunk then 47 | break 48 | elseif not done and not err then 49 | done, err = parse:parse(chunk) 50 | end 51 | end 52 | 53 | if not done then 54 | print(json.stringify({error = err or "Incomplete input"})) 55 | os.exit(1) 56 | end 57 | 58 | return parse:get() 59 | end 60 | 61 | local function validateArgs(func, uargs) 62 | local method = methods[func] 63 | if not method then 64 | print(json.stringify({error = "Method not found"})) 65 | os.exit(1) 66 | end 67 | 68 | if type(uargs) ~= "table" then 69 | print(json.stringify({error = "Invalid arguments"})) 70 | os.exit(1) 71 | end 72 | 73 | uargs.ubus_rpc_session = nil 74 | 75 | local k, v 76 | local margs = method.args or {} 77 | for k, v in pairs(uargs) do 78 | if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then 79 | print(json.stringify({error = "Invalid arguments"})) 80 | os.exit(1) 81 | end 82 | end 83 | 84 | return method 85 | end 86 | 87 | if arg[1] == "list" then 88 | local _, method, rv = nil, nil, {} 89 | for _, method in pairs(methods) do 90 | rv[_] = method.args or {} 91 | end 92 | print((json.stringify(rv):gsub(":%[%]", ":{}"))) 93 | elseif arg[1] == "call" then 94 | local args = parseInput() 95 | local method = validateArgs(arg[2], args) 96 | local result, code = method.call(args) 97 | print((json.stringify(result):gsub("^%[%]$", "{}"))) 98 | os.exit(code or 0) 99 | end 100 | -------------------------------------------------------------------------------- /htdocs/luci-static/resources/view/clash/overview.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | "require ui"; 3 | "require form"; 4 | "require view"; 5 | "require dom"; 6 | "require uci"; 7 | "require rpc"; 8 | "require fs"; 9 | 10 | return view.extend({ 11 | callGetServiceStatus: rpc.declare({ 12 | object: "luci.clash", 13 | method: "get_service_status", 14 | expect: {}, 15 | }), 16 | callInitAction: rpc.declare({ 17 | object: "luci", 18 | method: "setInitAction", 19 | params: ["name", "action"], 20 | expect: { result: false }, 21 | }), 22 | callUpdateProfile: rpc.declare({ 23 | object: "luci.clash", 24 | method: "update_profile", 25 | params: ["profile_name"], 26 | expect: {}, 27 | }), 28 | handleOpenDashboard: function () { 29 | var path = "clash-dashboard"; 30 | var host = window.location.host; 31 | var protocol = window.location.protocol; 32 | window.open("%s//%s/%s?hostname=%s".format(protocol, host, path, host)); 33 | }, 34 | handleUpdateProfile: function (m, profile_name, ev) { 35 | return this.callUpdateProfile(profile_name) 36 | .then(function (data) { 37 | if (!data["success"]) 38 | ui.addNotification(null, E("p", _(data["message"]))); 39 | }) 40 | .then(L.bind(m.load, m)) 41 | .then(L.bind(m.render, m)); 42 | }, 43 | load: function () { 44 | return Promise.all([this.callGetServiceStatus(), uci.load("clash")]); 45 | }, 46 | render: function (data) { 47 | var _this = this; 48 | var status = data[0]; 49 | var m, s, o; 50 | 51 | m = new form.Map( 52 | "clash", 53 | _("Simple Clash"), 54 | _( 55 | 'LuCI support for clash to translate traffic. More infomation is available at the official ducument' 56 | ) 57 | ); 58 | 59 | s = m.section(form.NamedSection, "global", null, _("Infomations")); 60 | 61 | o = s.option(form.DummyValue, "_running", _("Status")); 62 | o.cfgvalue = function () { 63 | return status["enabled"] ? _("Running") : _("Not Running"); 64 | }; 65 | 66 | o = s.option(form.DummyValue, "_version", _("Version")); 67 | o.cfgvalue = function () { 68 | return _(status["version"]); 69 | }; 70 | 71 | if (status["dashboard"]) { 72 | o = s.option(form.Button, "_dashboard", _("Web Interface")); 73 | o.inputtitle = _("Dashboard"); 74 | o.inputstyle = "apply"; 75 | o.onclick = _this.handleOpenDashboard; 76 | } else { 77 | o = s.option(form.DummyValue, "_dashboard", _("Web Interface")); 78 | o.cfgvalue = function () { 79 | return _("Please install 'clash-dashboard' package first."); 80 | }; 81 | } 82 | 83 | o = s.option(form.Button, "_restart", _("Service")); 84 | o.inputtitle = _("Restart"); 85 | o.inputstyle = "apply"; 86 | o.onclick = function () { 87 | return _this 88 | .callInitAction("clash", "restart") 89 | .then(L.bind(m.load, m)) 90 | .then(L.bind(m.render, m)); 91 | }; 92 | 93 | s = m.section(form.NamedSection, "global", "clash", _("Settings")); 94 | s.tab("general", _("Basic Options")); 95 | s.tab("addition", _("Addtional Options")); 96 | s.tab("dns", _("DNS Settings")); 97 | 98 | o = s.taboption( 99 | "general", 100 | form.Flag, 101 | "enabled", 102 | _("Enabled"), 103 | _("Enable the clash service.") 104 | ); 105 | o.rmempty = false; 106 | 107 | o = s.taboption( 108 | "general", 109 | form.Flag, 110 | "tproxy_enabled", 111 | _("TProxy Enabled"), 112 | _("Enable the transparent proxy.") 113 | ); 114 | o.rmempty = false; 115 | 116 | o = s.taboption( 117 | "general", 118 | form.Value, 119 | "tproxy_port", 120 | _("TProxy Port"), 121 | _("The port of transparent proxy server.") 122 | ); 123 | o.depends("tproxy_enabled", "1"); 124 | o.datatype = "port"; 125 | o.rmempty = false; 126 | o.default = 7893; 127 | 128 | o = s.taboption( 129 | "general", 130 | form.ListValue, 131 | "current_profile", 132 | _("Profile"), 133 | _("List of avaliable configurations for clash.") 134 | ); 135 | for (var v of L.uci.sections("clash", "profile")) { 136 | o.value(v[".name"], v[".name"]); 137 | } 138 | o.optional = true; 139 | 140 | o = s.taboption( 141 | "general", 142 | form.ListValue, 143 | "mode", 144 | _("Mode"), 145 | _("Clash router mode.") 146 | ); 147 | o.value("direct", "Direct"); 148 | o.value("rule", "Rule"); 149 | o.value("global", "Global"); 150 | o.rmempty = false; 151 | o.default = "Rule"; 152 | 153 | o = s.taboption( 154 | "general", 155 | form.Value, 156 | "mixed_port", 157 | _("Socks & HTTP Port"), 158 | _( 159 | "The port of http and socks proxy server. Set 0 to close socks and http proxy." 160 | ) 161 | ); 162 | o.datatype = "port"; 163 | o.rmempty = false; 164 | o.default = 0; 165 | 166 | o = s.taboption( 167 | "addition", 168 | form.Value, 169 | "prog", 170 | _("Custom Path"), 171 | _("The path of clash to execute.") 172 | ); 173 | o.datatype = "string"; 174 | o.rmempty = false; 175 | o.placeholder = "/usr/bin/clash"; 176 | 177 | o = s.taboption( 178 | "addition", 179 | form.Flag, 180 | "allow_lan", 181 | _("Allow Lan"), 182 | _("Allow connections to the local server.") 183 | ); 184 | o.rmempty = false; 185 | 186 | o = s.taboption( 187 | "addition", 188 | form.Value, 189 | "bind_addr", 190 | _("Bind Address"), 191 | _( 192 | "IP addresses be allowed to create connections. By default, this value is 0.0.0.0." 193 | ) 194 | ); 195 | o.depends("allow_lan", "1"); 196 | o.datatype = "ipaddr"; 197 | o.rmempty = false; 198 | o.default = "0.0.0.0"; 199 | o.placeholder = "0.0.0.0"; 200 | 201 | o = s.taboption( 202 | "addition", 203 | form.Flag, 204 | "ipv6", 205 | _("IPv6 Enabled"), 206 | _("Enable ipv6 support.") 207 | ); 208 | 209 | o = s.taboption( 210 | "addition", 211 | form.ListValue, 212 | "log_level", 213 | _("Log Level"), 214 | _("Clash by default prints log.") 215 | ); 216 | o.value("silent", "Silent"); 217 | o.value("debug", "Debug"); 218 | o.value("error", "Error"); 219 | o.value("warning", "Warning"); 220 | o.value("info", "Info"); 221 | o.rmempty = false; 222 | 223 | o = s.taboption( 224 | "addition", 225 | form.Value, 226 | "api_host", 227 | _("Api Address"), 228 | _("The host of Clash RESTful API.") 229 | ); 230 | o.datatype = "ipaddr"; 231 | o.rmempty = false; 232 | o.default = "0.0.0.0"; 233 | o.placeholder = "0.0.0.0"; 234 | 235 | o = s.taboption( 236 | "addition", 237 | form.Value, 238 | "api_port", 239 | _("Api Port"), 240 | _("The port of Clash RESTful API.") 241 | ); 242 | o.datatype = "port"; 243 | o.rmempty = false; 244 | o.default = 9090; 245 | o.placeholder = 9090; 246 | 247 | o = s.taboption( 248 | "dns", 249 | form.Value, 250 | "dns_host", 251 | _("DNS Host"), 252 | _("The host of Clash built-in DNS server.") 253 | ); 254 | o.datatype = "ipaddr"; 255 | o.rmempty = false; 256 | o.default = "127.0.0.1"; 257 | o.placeholder = "127.0.0.1"; 258 | 259 | o = s.taboption( 260 | "dns", 261 | form.Value, 262 | "dns_port", 263 | _("DNS Port"), 264 | _("The port of Clash build-in DNS server.") 265 | ); 266 | o.datatype = "port"; 267 | o.rmempty = false; 268 | o.default = 5353; 269 | o.placeholder = 5353; 270 | 271 | o = s.taboption( 272 | "dns", 273 | form.DynamicList, 274 | "default_nameserver", 275 | _("Default Nameservers"), 276 | _( 277 | "Nameservers are used to resolve the DNS nameserver hostnames. Only support UDP." 278 | ) 279 | ); 280 | o.datatype = "ipaddr"; 281 | o.rmempty = false; 282 | o.placeholder = "114.114.114.114"; 283 | 284 | o = s.taboption( 285 | "dns", 286 | form.DynamicList, 287 | "nameserver", 288 | _("Nameservers"), 289 | _( 290 | "Nameservers are used to resolve all DNS query. Support UDP, TCP, DOT, DOH." 291 | ) 292 | ); 293 | o.datatype = "string"; 294 | o.rmempty = false; 295 | o.placeholder = "114.114.114.114"; 296 | 297 | s = m.section(form.GridSection, "profile", _("Profiles")); 298 | s.sortable = true; 299 | s.addremove = true; 300 | s.anonymous = true; 301 | s.nodescriptions = true; 302 | s.addbtntitle = _("Add new profiles..."); 303 | s.modaltitle = function (section_id) { 304 | return _("Clash Profiles - %s".format(section_id)); 305 | }; 306 | 307 | o = s.option(form.DummyValue, "_cfg_name", _("Name")); 308 | o.modalonly = false; 309 | o.textvalue = function (section_id) { 310 | return section_id; 311 | }; 312 | 313 | o = s.option(form.Value, "type", _("Type")); 314 | o.value("Static", "Static"); 315 | o.value("URL", "URL"); 316 | o.rmempty = false; 317 | 318 | o = s.option( 319 | form.Value, 320 | "url", 321 | _("URL"), 322 | _("This needs cURL with SSL support, install 'curl' package first.") 323 | ); 324 | o.rmempty = false; 325 | o.depends("type", "URL"); 326 | o.textvalue = function (section_id) { 327 | var maxLen = 40; 328 | var cval = this.cfgvalue(section_id); 329 | if (cval == null) return this.default; 330 | if (cval.length <= maxLen) return cval; 331 | return "%h...".format(cval.slice(0, maxLen)); 332 | }; 333 | 334 | o = s.option(form.DummyValue, "_modify_time", _("Last Update")); 335 | o.modalonly = false; 336 | o.load = function (section_id) { 337 | return fs 338 | .stat("/etc/clash/profiles/%s.yaml".format(section_id)) 339 | .then(function (fileStat) { 340 | var mtime = new Date(fileStat.mtime * 1000); 341 | return mtime.toLocaleString(); 342 | }) 343 | .catch(function (e) { 344 | return "Never"; 345 | }); 346 | }; 347 | 348 | s.handleCreateProfile = function (m, name, type, url, ev) { 349 | var section_id = name.isValid("_new_") ? name.formvalue("_new_") : null; 350 | var type_value = type.isValid("_new_") ? type.formvalue("_new_") : ""; 351 | var url_value = url.isValid("_new_") ? url.formvalue("_new_") : ""; 352 | 353 | if (section_id == null || type_value == "") return; 354 | if (type_value == "URL" && url_value == "") return; 355 | 356 | if (uci.get("clash", section_id) != null) { 357 | s.handleModalSave(); 358 | ui.hideModal(); 359 | return; 360 | } 361 | 362 | return m 363 | .save(function () { 364 | section_id = uci.add("clash", "profile", section_id); 365 | uci.set("clash", section_id, "type", type_value); 366 | if (type_value == "URL") 367 | uci.set("clash", section_id, "url", url_value); 368 | }) 369 | .then(function () { 370 | return ui.hideModal(); 371 | }); 372 | }; 373 | 374 | s.handleAdd = function (ev) { 375 | var m2, s2, name, type; 376 | 377 | m2 = new form.Map("clash"); 378 | s2 = m2.section(form.NamedSection, "_new_"); 379 | s2.render = function () { 380 | return Promise.all([{}, this.renderUCISection("_new_")]).then( 381 | this.renderContents.bind(this) 382 | ); 383 | }; 384 | 385 | name = s2.option(form.Value, "name", _("Name")); 386 | name.rmempty = false; 387 | name.datatype = "uciname"; 388 | name.validate = function (section_id, value) { 389 | if (uci.get("clash", value) != null) 390 | return _("The profile name is already used"); 391 | return true; 392 | }; 393 | 394 | type = s2.option(form.Value, "type", _("Type")); 395 | type.rmempty = false; 396 | type.value("Static", "Static"); 397 | type.value("URL", "URL"); 398 | type.default = "Static"; 399 | 400 | url = s2.option( 401 | form.Value, 402 | "url", 403 | _("URL"), 404 | _("This needs cURL with SSL support, install 'curl' package first.") 405 | ); 406 | url.depends("type", "URL"); 407 | url.rmempty = false; 408 | 409 | m2.render().then( 410 | L.bind(function (nodes) { 411 | ui.showModal( 412 | _("Add new profile..."), 413 | [ 414 | nodes, 415 | E("div", { class: "right" }, [ 416 | E("button", { class: "btn", click: ui.hideModal }, _("Cancel")), 417 | " ", 418 | E( 419 | "button", 420 | { 421 | class: "cbi-button cbi-button-positive important", 422 | click: ui.createHandlerFn( 423 | this, 424 | "handleCreateProfile", 425 | m, 426 | name, 427 | type, 428 | url 429 | ), 430 | }, 431 | _("Create profile") 432 | ), 433 | ]), 434 | ], 435 | "cbi-modal" 436 | ); 437 | nodes 438 | .querySelector( 439 | '[id="%s"] input[type="text"]'.format(name.cbid("_new_")) 440 | ) 441 | .focus(); 442 | }, this) 443 | ); 444 | }; 445 | 446 | s.renderRowActions = function (section_id) { 447 | var element = this.super("renderRowActions", [section_id, _("Edit")]); 448 | var update_opt = { 449 | class: "cbi-button cbi-button-neutral", 450 | click: ui.createHandlerFn(_this, "handleUpdateProfile", m, section_id), 451 | title: _("Update this profile"), 452 | }; 453 | if (uci.get("clash", section_id, "type") != "URL") 454 | update_opt["disabled"] = "disabled"; 455 | dom.content(element.lastChild, [ 456 | E("button", update_opt, _("Update")), 457 | element.lastChild.childNodes[0], 458 | element.lastChild.childNodes[1], 459 | element.lastChild.childNodes[2], 460 | ]); 461 | return element; 462 | }; 463 | 464 | s.addModalOptions = function (s, section_id) { 465 | o = s.option(form.TextValue, null, _("Content")); 466 | o.rmempty = false; 467 | o.modalonly = true; 468 | o.monospace = true; 469 | o.rows = 20; 470 | o.load = function (section_id) { 471 | return fs 472 | .read("/etc/clash/profiles/%s.yaml".format(section_id), "") 473 | .then(function (value) { 474 | return value; 475 | }) 476 | .catch(function (e) { 477 | var type = uci.get("clash", section_id, "type"); 478 | if (type == "Static") return ""; 479 | if (type == "URL") { 480 | o.readonly = true; 481 | return "Please update profile first."; 482 | } 483 | }); 484 | }; 485 | o.write = function (section_id, formvalue) { 486 | return fs 487 | .write("/etc/clash/profiles/%s.yaml".format(section_id), formvalue) 488 | .then(function () { 489 | s.textvalue = formvalue; 490 | ui.addNotification(null, E("p", _("Changes have been saved."))); 491 | }) 492 | .catch(function (e) { 493 | ui.addNotification( 494 | null, 495 | E("p", _("Unable to save changes: %s").format(e.message)) 496 | ); 497 | }); 498 | }; 499 | }; 500 | 501 | return m.render(); 502 | }, 503 | }); 504 | --------------------------------------------------------------------------------