import { LitElement, html, unsafeCSS } from "lit"; import { customElement, query, state } from "lit/decorators.js"; import globalStyles from "./index.css?inline"; import { type Config, type Rule, type RuleProvider } from "./interface.js"; import axios, { AxiosError } from "axios"; import { base64EncodeUnicode, base64decodeUnicode } from "./utils.js"; import "./components/rule-provider-input.js"; import "./components/rule-input.js"; import "./components/rename-input.js"; @customElement("sub2clash-app") export class Sub2clashApp extends LitElement { static styles = [unsafeCSS(globalStyles)]; private _config: Config = { clashType: 2, subscriptions: [], proxies: [], refresh: false, autoTest: false, lazy: false, ignoreCountryGroup: false, useUDP: false, template: "", sort: "nameasc", remove: "", nodeList: false, ruleProviders: [], replace: undefined, rules: [], }; @state() set config(value: Config) { console.log(JSON.stringify(value)); if ( (value.subscriptions == null || value.subscriptions.length == 0) && (value.proxies == null || value.proxies.length == 0) ) { this.configUrl = ""; return; } const oldValue = this._config; this.configUrl = `${ window.location.origin }${window.location.pathname.replace( /\/$/, "" )}/convert/${base64EncodeUnicode(JSON.stringify(value)) .replace(/\+/g, "-") .replace(/\//g, "_")}`; this._config = value; this.requestUpdate("config", oldValue); } get config(): Config { return this._config; } @state({ hasChanged(value: boolean) { localStorage.setItem("theme", value ? "dark" : "light"); document .querySelector("html") ?.setAttribute("data-theme", value ? "dark" : "light"); return true; }, }) darkTheme: boolean = this.initTheme(); initTheme(): boolean { const savedTheme = localStorage.getItem("theme"); if (savedTheme != null) { return savedTheme === "dark" ? true : false; } const prefersDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; return prefersDark; } @state() reverseUrl: string = ""; @state() dialogMessage: string = ""; @state() dialogTitle: string = ""; @query("dialog#my_modal") dialog!: HTMLDialogElement; showDialog(title: string, message: string): void { if (title.trim() === "") { title = "警告"; } this.dialogTitle = title; this.dialogMessage = message; this.dialog.showModal(); } @state() configUrl: string = ""; @state() shortLinkID: string = ""; @state() shortLinkPasswd: string = ""; async copyToClipboard(content: string, e: HTMLButtonElement) { try { await navigator.clipboard.writeText(content); let text = e.textContent; e.addEventListener("mouseout", function () { e.textContent = text; }); e.textContent = "复制成功"; } catch (err) { console.error("复制到剪贴板失败:", err); } } generateShortLink() { if (this.configUrl === "") { this.showDialog("", "还未填写配置"); return; } axios .post( "./short", { config: this.config, password: this.shortLinkPasswd, id: this.shortLinkID, }, { headers: { "Content-Type": "application/json", }, } ) .then((response) => { // 设置返回的短链ID和密码 this.shortLinkID = response.data.id; this.shortLinkPasswd = response.data.password; }) .catch((error) => { if (error.response && error.response.data) { this.showDialog("", "生成短链失败:" + error.response.data); } else { this.showDialog("", "生成短链失败"); } }); } updateShortLink() { if (this.shortLinkID.trim() === "") { this.showDialog("", "请输入ID"); return; } if (this.shortLinkPasswd.trim() === "") { this.showDialog("", "请输入密码"); return; } if (this.configUrl === "") { this.showDialog("", "还未填写配置"); return; } axios .put( "./short", { id: this.shortLinkID, config: this.config, password: this.shortLinkPasswd, }, { headers: { "Content-Type": "application/json", }, } ) .then(() => { this.showDialog("成功", "更新成功"); }) .catch((error) => { if (error.response && error.response.status === 401) { this.showDialog("", "密码错误"); } else if (error.response && error.response.data) { this.showDialog("", "更新短链失败:" + error.response.data); } else { this.showDialog("", "更新短链失败"); } }); } deleteShortLink() { if (this.shortLinkID.trim() === "") { this.showDialog("", "请输入ID"); return; } if (this.shortLinkPasswd.trim() === "") { this.showDialog("", "请输入密码"); return; } const params = new URLSearchParams(); params.append("password", this.shortLinkPasswd); axios .delete(`./short/${this.shortLinkID}?${params.toString()}`, { headers: { "Content-Type": "application/json", }, }) .then(() => { this.showDialog("成功", "删除成功"); }) .catch((error) => { if (error.response && error.response.status === 401) { this.showDialog("", "短链不存在或密码错误"); } else if (error.response && error.response.data) { this.showDialog("", "删除短链失败:" + error.response.data); } else { this.showDialog("", "删除短链失败"); } }); } getRawConfigFromShortLink() { const s = this.reverseUrl.split("/s/"); if (s.length != 2) { this.showDialog("", "解析失败"); return; } axios .get(`./short/${s[1]}`) .then((resp) => { this.config = resp.data; }) .catch((err: AxiosError) => { if (err.response && err.response.status == 401) { this.showDialog("", "短链不存在或密码错误"); } else if (err.response && err.response.data) { this.showDialog("", "获取配置失败:" + err.response.data); } else { this.showDialog("", "获取配置失败"); } }); } parseConfig() { if (this.reverseUrl.trim() === "") { this.showDialog("", "无法解析,链接为空"); } if (this.reverseUrl.indexOf("/s/") != -1) { this.getRawConfigFromShortLink(); return; } let url = new URL(this.reverseUrl); const pathSections = url.pathname.split("/"); if (pathSections.length < 2) { this.showDialog("", "无法解析,链接格式错误"); } if (pathSections[pathSections.length - 2] == "convert") { let base64Data = pathSections[pathSections.length - 1]; base64Data = base64Data.replace(/-/g, "+").replace(/_/g, "/"); try { const configData = base64decodeUnicode(base64Data); this.config = JSON.parse(configData) as Config; } catch (e: any) { this.showDialog("", "无法解析 Base64,配置格式错误"); return; } } else { this.showDialog("", "无法解析,链接格式错误"); } } render() { return html`
sub2clash
输出配置
`; } } declare global { interface HTMLElementTagNameMap { "sub2clash-app": Sub2clashApp; } }