From 1e0dafbda95f2ec87740943853d66276425a61d7 Mon Sep 17 00:00:00 2001
From: Xi Xu
Date: Sun, 13 Jul 2025 22:07:38 +0800
Subject: [PATCH] Initial commit: add META.js Customizer web app
Add CNAME for custom domain, README with project overview and instructions, main HTML, CSS, and JavaScript files for the META.js Customizer web app. Includes meta_template.js for configuration generation and script.js for interactive UI logic.
---
CNAME | 1 +
README.md | 117 +++++
index.html | 150 +++++++
meta_template.js | 662 ++++++++++++++++++++++++++++
script.js | 371 ++++++++++++++++
style.css | 1087 ++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 2388 insertions(+)
create mode 100644 CNAME
create mode 100644 README.md
create mode 100644 index.html
create mode 100644 meta_template.js
create mode 100644 script.js
create mode 100644 style.css
diff --git a/CNAME b/CNAME
new file mode 100644
index 0000000..35a5c67
--- /dev/null
+++ b/CNAME
@@ -0,0 +1 @@
+metajs.xi-xu.me
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..94ab96a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,117 @@
+# META.js Customizer
+
+## 🎯 Overview
+
+META.js Customizer is an intuitive web app that allows users to easily create custom META.js of [xixu-me/META](https://github.com/xixu-me/META). It provides a visual interface to select and configure service rulesets from [xixu-me/RFM](https://github.com/xixu-me/RFM) repository.
+
+### ✨ Key Features
+
+- 🔍 **Smart Service Search**: Instantly search and filter through all available services
+- 🎨 **Modern Glass UI**: Beautiful, responsive design with dark/light theme support
+- 🌙 **Intelligent Theming**: Auto-detects system theme with manual override options
+- 📱 **Mobile Responsive**: Works seamlessly on all device sizes
+- 🚀 **Real-time Generation**: Instantly generates META.js configuration as you select services
+- 📋 **One-click Copy**: Copy generated configuration to clipboard
+- 💾 **Download Support**: Download your configuration as META.js file
+
+## 🚀 Quick Start
+
+### Online Usage (Recommended)
+
+Visit [metajs.xi-xu.me](https://metajs.xi-xu.me) to use the customizer directly in your browser.
+
+### Local Development
+
+1. **Clone the repository**
+
+ ```bash
+ git clone https://github.com/xixu-me/META.js-Customizer.git
+ cd META.js-Customizer
+ ```
+
+2. **Serve the files**
+
+ ```bash
+ # Using Python
+ python -m http.server 8000
+
+ # Using Node.js http-server
+ npx http-server
+
+ # Or simply open index.html in your browser
+ ```
+
+3. **Open in browser**
+ Navigate to `http://localhost:8000` (or your chosen port)
+
+## 📖 Usage
+
+### Basic Workflow
+
+1. **Search for Services**: Use the search bar to find services you want to include
+2. **Select Services**: Click on search results to add them to your configuration
+3. **Review Selection**: Check your selected services in the main panel
+4. **Generate Configuration**: Your META.js configuration is automatically generated
+5. **Copy or Download**: Use the action buttons to copy or download your configuration
+
+### Service Configuration
+
+Service icons are sourced from [xixu-me/favicons](https://github.com/xixu-me/favicons). Auto-generated configurations may require manual adjustments based on availability.
+
+- **Icon Unavailable**: Uses specific `domain` for services requiring custom domains
+ - Example: `rednote` → `{ name: "rednote", domain: "xiaohongshu.com" }`
+
+- **Icon Available**: Uses only `tld` when service name matches Second-Level Domain
+ - Example: `github` → `{ name: "GitHub", tld: "com" }`
+
+- **Name does not match SLD**: Adds `sld` for services with non-standard naming
+ - Example: `xai` → `{ name: "xAI", sld: "x", tld: "ai" }`
+
+### Theme Options
+
+Click the theme toggle button to cycle through:
+
+- 🌙 **Auto**: Follows system preference (default)
+- ☀️ **Light**: Force light mode
+- 🌙 **Dark**: Force dark mode
+
+## 🏗️ Architecture
+
+### Key Components
+
+- **Service Fetcher**: Retrieves available services from RFM repository via GitHub API
+- **Search Engine**: Real-time filtering and matching of services
+- **Configuration Generator**: Creates META.js configuration with proper formatting
+- **Theme Manager**: Handles theme detection, switching, and persistence
+- **UI Controller**: Manages user interactions and visual feedback
+
+### Technology Stack
+
+- **Frontend**: Vanilla HTML5, CSS3, JavaScript (ES6+)
+- **Styling**: CSS Custom Properties, CSS Grid/Flexbox, Backdrop Filter
+- **Icons**: Font Awesome 6
+- **API**: GitHub REST API for fetching service data
+
+## 🎨 Design Features
+
+### Glass Morphism UI
+
+- Translucent cards with backdrop blur effects
+- Smooth animations and transitions
+- Modern button interactions with liquid shine effects
+
+### Responsive Design
+
+- Mobile-first approach
+- Adaptive layouts for all screen sizes
+- Touch-friendly interface elements
+
+### Accessibility
+
+- Semantic HTML structure
+- Keyboard navigation support
+- Screen reader friendly labels
+
+## 📄 License
+
+This project is licensed under the GNU General Public License v3.0 - see the [LICENSE](LICENSE) file for details.
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..fa25299
--- /dev/null
+++ b/index.html
@@ -0,0 +1,150 @@
+
+
+
+
+
+ META.js Customizer
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 selected
+
+
+
+
+
+
+
+
No services selected
+
Search for services above to get started
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Service icons are sourced from
+
+ xixu-me/favicons. Auto-generated configurations may require manual adjustments
+ based on availability.
+
+
+
Configuration Rules
+
+ -
+ Icon unavailable: Uses specific
+
domain
+ (e.g., "xiaohongshu.com" for rednote)
+
+ -
+ Name matches SLD: Uses only
tld
+ (e.g., "github" → tld: "com")
+
+ -
+ Name does not match SLD: Adds
sld
+ (e.g., "xai" → sld: "x", tld: "ai")
+
+
+
+
+
+
Pro Tip: Browse the
+
+ assets branch
+ to verify icon availability before finalizing your configuration.
+
+
+
+
+
+
+
+
+
+
+
diff --git a/meta_template.js b/meta_template.js
new file mode 100644
index 0000000..ec04c2f
--- /dev/null
+++ b/meta_template.js
@@ -0,0 +1,662 @@
+const metaTemplate = `// from https://github.com/xixu-me/META
+
+// ################################################### this section can be flexibly customized ###################################################
+
+const LOCATION_ICON_SET_URL = "https://img.icons8.com/color/144/";
+
+// Routing rules are matched in order from top to bottom, with the rule at the top of the list taking precedence over the rules below it.
+const services = [
+ /* SERVICES_PLACEHOLDER */
+];
+
+const locations = [
+ {
+ name: "Argentina 🇦🇷",
+ icon: \`\${LOCATION_ICON_SET_URL}argentina\`,
+ filter: "(?i)\\u963f\\u6839\\u5ef7|Argentina|ARG|AR|argentina|arg|ar|🇦🇷",
+ },
+ {
+ name: "Canada 🇨🇦",
+ icon: \`\${LOCATION_ICON_SET_URL}canada\`,
+ filter: "(?i)\\u52a0\\u62ff\\u5927|Canada|CAN|CA|canada|ca|🇨🇦",
+ },
+ {
+ name: "Finland 🇫🇮",
+ icon: \`\${LOCATION_ICON_SET_URL}finland\`,
+ filter: "(?i)\\u82ac\\u5170|Finland|FIN|FI|finland|fin|fi|🇫🇮",
+ },
+ {
+ name: "France 🇫🇷",
+ icon: \`\${LOCATION_ICON_SET_URL}france\`,
+ filter: "(?i)\\u6cd5\\u56fd|France|FR|france|fr|🇫🇷",
+ },
+ {
+ name: "Germany 🇩🇪",
+ icon: \`\${LOCATION_ICON_SET_URL}germany\`,
+ filter: "(?i)\\u5fb7\\u56fd|Germany|GER|DE|germany|ger|de|🇩🇪",
+ },
+ {
+ name: "Hong Kong, China 🇭🇰",
+ icon: \`\${LOCATION_ICON_SET_URL}hongkong-circular\`,
+ filter: "(?i)\\u9999\\u6e2f|Hong Kong|HK|hong kong|hk|🇭🇰",
+ },
+ {
+ name: "Iraq 🇮🇶",
+ icon: \`\${LOCATION_ICON_SET_URL}iraq\`,
+ filter: "(?i)\\u4f0a\\u62c9\\u514b|Iraq|IRQ|IQ|iraq|iq|🇮🇶",
+ },
+ {
+ name: "Japan 🇯🇵",
+ icon: \`\${LOCATION_ICON_SET_URL}japan\`,
+ filter: "(?i)\\u65e5\\u672c|Japan|japan|ja|🇯🇵",
+ },
+ {
+ name: "Korea 🇰🇷",
+ icon: \`\${LOCATION_ICON_SET_URL}south-korea\`,
+ filter: "(?i)\\u97e9\\u56fd|Korea|KR|korea|kr|🇰🇷",
+ },
+ {
+ name: "Russia 🇷🇺",
+ icon: \`\${LOCATION_ICON_SET_URL}russian-federation\`,
+ filter:
+ "(?i)\\u4fc4\\u7f57\\u65af|Russia Federation|Russia|RU|russia federation|russia|ru|🇷🇺",
+ },
+ {
+ name: "Singapore 🇸🇬",
+ icon: \`\${LOCATION_ICON_SET_URL}singapore\`,
+ filter: "(?i)\\u65b0\\u52a0\\u5761|Singapore|SG|singapore|sg|🇸🇬",
+ },
+ {
+ name: "Taiwan, China 🇨🇳",
+ icon: \`\${LOCATION_ICON_SET_URL}china-circular\`,
+ filter: "(?i)\\u53f0\\u6e7e|Taiwan|TW|taiwan|tw|\\uD83C\\uDDF9\\uD83C\\uDDFC",
+ },
+ {
+ name: "Thailand 🇹🇭",
+ icon: \`\${LOCATION_ICON_SET_URL}thailand\`,
+ filter: "(?i)\\u6cf0\\u56fd|Thailand|TH|thailand|th|🇹🇭",
+ },
+ {
+ name: "Türkiye 🇹🇷",
+ icon: \`\${LOCATION_ICON_SET_URL}turkey\`,
+ filter:
+ "(?i)\\u571f\\u8033\\u5176|Türkiye|Turkey|TUR|TR|türkiye|turkey|tur|tr|🇹🇷",
+ },
+ {
+ name: "Ukraine 🇺🇦",
+ icon: \`\${LOCATION_ICON_SET_URL}ukraine\`,
+ filter: "(?i)\\u4e4c\\u514b\\u5170|Ukraine|UKR|UA|ukraine|ukr|ua|🇺🇦",
+ },
+ {
+ name: "United Kingdom 🇬🇧",
+ icon: \`\${LOCATION_ICON_SET_URL}great-britain\`,
+ filter:
+ "(?i)\\u82f1\\u56fd|United Kingdom|Great Britain|UK|GB|united kingdom|great britain|uk|gb|🇬🇧",
+ },
+ {
+ name: "United States 🇺🇸",
+ icon: \`\${LOCATION_ICON_SET_URL}usa\`,
+ filter:
+ "(?i)\\u7f8e\\u56fd|United States of America|United States|USA|US|united states of america|united states|usa|us|🇺🇸",
+ },
+ {
+ name: "Global 🌐",
+ icon: "https://img.icons8.com/?size=144&id=3685&format=png&color=7bbbe9",
+ filter: "(?i)Global|GL|global|gl|Cloudflare|CF|cloudflare|cf|🌐",
+ },
+];
+
+// ####################################### DO NOT MODIFY THE CODE BELOW UNLESS YOU KNOW WHAT YOU ARE DOING #######################################
+
+function extractFavicon(domain) {
+ return \`https://t1.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=https://\${domain}&size=256\`;
+}
+
+// General Configuration
+
+const generalConfig = {
+ "allow-lan": false,
+ mode: "rule",
+ "log-level": "info",
+ ipv6: true,
+ "find-process-mode": "strict",
+ "external-controller": "",
+ profile: {
+ "store-selected": true,
+ "store-fake-ip": true,
+ },
+ "unified-delay": true,
+ "tcp-concurrent": true,
+ "global-client-fingerprint": "chrome",
+ "global-ua": "\\u0063\\u006c\\u0061\\u0073\\u0068.\\u006D\\u0065\\u0074\\u0061",
+ "etag-support": true,
+};
+
+// DNS
+
+const chineseNameservers = [
+ "https://doh.pub/dns-query",
+ "https://dns.alidns.com/dns-query",
+];
+
+const internationalNameservers = [
+ "quic://unfiltered.adguard-dns.com:784",
+ "https://dns.google/dns-query",
+ "https://doh.dns.sb/dns-query",
+ "https://dns.quad9.net/dns-query",
+ "https://doh.opendns.com/dns-query",
+ "https://dns.mullvad.net/dns-query",
+ "https://doh.umbrella.com/dns-query",
+ "https://wikimedia-dns.org/dns-query",
+ "https://doh.dns.apple.com/dns-query",
+ "https://cloudflare-dns.com/dns-query",
+ "https://common.dot.dns.yandex.net/dns-query",
+ "https://unfiltered.adguard-dns.com/dns-query",
+];
+
+const adguardDefaultNameservers = [
+ "quic://dns.adguard-dns.com:784",
+ "https://dns.adguard-dns.com/dns-query",
+];
+
+const adguardFamilyNameservers = [
+ "quic://family.adguard-dns.com:784",
+ "https://family.adguard-dns.com/dns-query",
+];
+
+const dns = {
+ enable: true,
+ "prefer-h3": false,
+ "use-hosts": true,
+ "use-system-hosts": false,
+ "respect-rules": true,
+ listen: "0.0.0.0:1053",
+ ipv6: true,
+ "enhanced-mode": "fake-ip",
+ "fake-ip-range": "198.18.0.1/16",
+ "fake-ip-filter-mode": "blacklist",
+ "fake-ip-filter": [
+ "rule-set:fake-ip-filter",
+ "rule-set:direct",
+ "rule-set:private",
+ "rule-set:connectivity-check",
+ ],
+ "default-nameserver": [
+ "119.29.29.29",
+ "223.5.5.5",
+ "223.6.6.6",
+ "8.8.8.8",
+ "8.8.4.4",
+ "1.1.1.1",
+ "1.0.0.1",
+ "9.9.9.9",
+ "149.112.112.112",
+ "208.67.222.222",
+ "208.67.220.220",
+ ],
+ "proxy-server-nameserver": chineseNameservers,
+ "nameserver-policy": {
+ "rule-set:private,direct,geolocation-cn": chineseNameservers,
+ "rule-set:proxy": internationalNameservers,
+ },
+ nameserver: [
+ ...chineseNameservers,
+ ...internationalNameservers,
+ // ...adguardDefaultNameservers,
+ // ...adguardFamilyNameservers,
+ ],
+};
+
+// Hosts
+
+const hosts = {};
+
+// Domain Sniffing
+
+const sniffer = {
+ enable: false,
+};
+
+// TUN
+
+const tun = {
+ enable: true,
+ stack: "mixed",
+ "auto-route": true,
+ "auto-redirect": true,
+ "auto-detect-interface": true,
+ "strict-route": true,
+ "dns-hijack": ["any:53", "tcp://any:53"],
+};
+
+// Proxy Groups
+
+const proxyGroupDefaults = {
+ url: "https://www.gstatic.com/generate_204",
+ interval: 300,
+ lazy: true,
+ timeout: 5000,
+ "max-failed-times": 5,
+};
+
+const serviceProxyGroupProxies = [
+ "PROXY",
+ "AUTO",
+ "STATIC",
+ "DIRECT",
+ "Mainland China 🇨🇳",
+ ...locations.map(({ name }) => name),
+];
+
+const locationPolicyProxyGroupDefaults = {
+ ...proxyGroupDefaults,
+ proxies: ["REJECT"],
+ "include-all": true,
+};
+
+const SERVICE_ICON_SET_URL =
+ "https://raw.githubusercontent.com/xixu-me/favicons/refs/heads/assets/";
+
+function getServiceIcon(service) {
+ if (service.icon) return service.icon;
+ if (service.domain) return extractFavicon(service.domain);
+ if (service.tld) {
+ const secondLevelDomain = service.sld || service.name.toLowerCase();
+ return \`\${SERVICE_ICON_SET_URL}\${secondLevelDomain}.\${service.tld}.png\`;
+ }
+ console.warn(\`No icon configuration for service: \${service.name}\`);
+ return null;
+}
+
+services.forEach((service) => {
+ service.icon = getServiceIcon(service);
+});
+
+function generateServiceProxyGroups(items, defaultConfig) {
+ return items.map(({ name, icon }) => ({
+ ...defaultConfig,
+ name,
+ icon,
+ type: "select",
+ proxies: serviceProxyGroupProxies,
+ }));
+}
+
+function generateLocationPolicyProxyGroups(
+ items,
+ defaultConfig,
+ type,
+ extraProps = {}
+) {
+ const getStrategyName = (type, strategy) => {
+ switch (type) {
+ case "url-test":
+ return "AUTO";
+ case "fallback":
+ return "FALLBACK";
+ case "load-balance":
+ switch (strategy) {
+ case "consistent-hashing":
+ return "LOAD BALANCING (consistent hashing)";
+ case "round-robin":
+ return "LOAD BALANCING (round-robin)";
+ case "sticky-sessions":
+ return "LOAD BALANCING (sticky sessions)";
+ default:
+ return "LOAD BALANCING";
+ }
+ default:
+ return type.toUpperCase();
+ }
+ };
+ return items.map(({ name, icon, filter }) => {
+ const strategyName = getStrategyName(type, extraProps.strategy);
+ const emoji = name.split(" ").pop();
+ const newName = \`\${strategyName} \${emoji}\`;
+ return {
+ ...defaultConfig,
+ name: newName,
+ type,
+ icon,
+ filter,
+ hidden: true,
+ ...extraProps,
+ };
+ });
+}
+
+function generateLocationSelectProxyGroups() {
+ return locations.map(({ name, icon }) => ({
+ ...proxyGroupDefaults,
+ name,
+ type: "select",
+ icon,
+ proxies: [
+ \`AUTO \${name.split(" ").pop()}\`,
+ \`FALLBACK \${name.split(" ").pop()}\`,
+ \`LOAD BALANCING (consistent hashing) \${name.split(" ").pop()}\`,
+ \`LOAD BALANCING (round-robin) \${name.split(" ").pop()}\`,
+ \`LOAD BALANCING (sticky sessions) \${name.split(" ").pop()}\`,
+ ],
+ }));
+}
+
+const BASE_ICON_SET_URL =
+ "https://cdn.jsdelivr.net/gh/Koolson/Qure@master/IconSet/Color/";
+
+const proxyGroups = [
+ {
+ ...proxyGroupDefaults,
+ name: "PROXY",
+ type: "select",
+ proxies: [
+ "AUTO",
+ "STATIC",
+ "Mainland China 🇨🇳",
+ ...locations.map(({ name }) => name),
+ ],
+ icon: \`\${BASE_ICON_SET_URL}Proxy.png\`,
+ },
+ {
+ ...proxyGroupDefaults,
+ name: "AUTO",
+ type: "url-test",
+ tolerance: 50,
+ "include-all": true,
+ icon: \`\${BASE_ICON_SET_URL}Auto.png\`,
+ hidden: true,
+ },
+ {
+ ...proxyGroupDefaults,
+ name: "STATIC",
+ type: "select",
+ "include-all": true,
+ icon: \`\${BASE_ICON_SET_URL}Static.png\`,
+ },
+ ...generateServiceProxyGroups(services, proxyGroupDefaults),
+ {
+ ...proxyGroupDefaults,
+ name: "Others",
+ type: "select",
+ proxies: [...serviceProxyGroupProxies],
+ icon: \`\${BASE_ICON_SET_URL}Final.png\`,
+ },
+ {
+ ...proxyGroupDefaults,
+ name: "Advertising",
+ type: "select",
+ proxies: ["REJECT", "DIRECT", "STATIC"],
+ icon: \`\${BASE_ICON_SET_URL}Advertising.png\`,
+ },
+ {
+ ...locationPolicyProxyGroupDefaults,
+ name: "Mainland China 🇨🇳",
+ type: "select",
+ icon: \`\${LOCATION_ICON_SET_URL}china\`,
+ proxies: ["DIRECT"],
+ "include-all": true,
+ filter:
+ "(?i)\\u5927\\u9646|\\u4e2d\\u56fd|\\u7535\\u4fe1|\\u79fb\\u52a8|\\u8054\\u901a|Mainland|China|CN|mainland|china|cn|🇨🇳",
+ },
+ ...generateLocationPolicyProxyGroups(
+ locations,
+ locationPolicyProxyGroupDefaults,
+ "url-test",
+ {
+ tolerance: 50,
+ }
+ ),
+ ...generateLocationPolicyProxyGroups(
+ locations,
+ locationPolicyProxyGroupDefaults,
+ "fallback"
+ ),
+ ...generateLocationPolicyProxyGroups(
+ locations,
+ locationPolicyProxyGroupDefaults,
+ "load-balance",
+ {
+ strategy: "round-robin",
+ }
+ ),
+ ...generateLocationPolicyProxyGroups(
+ locations,
+ locationPolicyProxyGroupDefaults,
+ "load-balance",
+ {
+ strategy: "consistent-hashing",
+ }
+ ),
+ ...generateLocationPolicyProxyGroups(
+ locations,
+ locationPolicyProxyGroupDefaults,
+ "load-balance",
+ {
+ strategy: "sticky-sessions",
+ }
+ ),
+ ...generateLocationSelectProxyGroups(),
+];
+
+// Routing Rules
+
+function generateServiceRules(services) {
+ return services.map(({ name }) => \`RULE-SET,\${name.toLowerCase()},\${name}\`);
+}
+
+const rules = [
+ "RULE-SET,applications,DIRECT",
+ "RULE-SET,lancidr,DIRECT,no-resolve",
+ "RULE-SET,private,DIRECT",
+ "RULE-SET,reject,Advertising",
+ "RULE-SET,win-spy,Advertising",
+ ...generateServiceRules(services),
+ "RULE-SET,direct,Mainland China 🇨🇳",
+ "RULE-SET,proxy,PROXY",
+ "RULE-SET,telegramcidr,Telegram",
+ "RULE-SET,xcidr,X",
+ "RULE-SET,googlecidr,Google",
+ "RULE-SET,cloudflarecidr,Cloudflare",
+ "RULE-SET,cncidr,Mainland China 🇨🇳",
+ "MATCH,Others",
+];
+
+// Rule Providers
+
+const ruleProviderDefaults = {
+ type: "http",
+ interval: 14400,
+};
+
+function generateServiceRuleProviders(services, defaultConfig) {
+ return services.reduce((acc, { name, alias }) => {
+ const ruleName = alias || name;
+ acc[name.toLowerCase()] = {
+ ...defaultConfig,
+ format: "mrs",
+ behavior: "domain",
+ url: \`https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/\${ruleName.toLowerCase()}.mrs\`,
+ path: \`./rulesets/\${name.toLowerCase()}.mrs\`,
+ };
+ return acc;
+ }, {});
+}
+
+const ruleProviders = {
+ "fake-ip-filter": {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/fake-ip-filter.mrs",
+ path: "./rulesets/fake-ip-filter.mrs",
+ },
+ "connectivity-check": {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/connectivity-check.mrs",
+ path: "./rulesets/connectivity-check.mrs",
+ },
+ applications: {
+ ...ruleProviderDefaults,
+ format: "yaml",
+ behavior: "classical",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/applications.yaml",
+ path: "./rulesets/applications.yaml",
+ },
+ lancidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/lancidr.mrs",
+ path: "./rulesets/lancidr.mrs",
+ },
+ cncidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/cncidr.mrs",
+ path: "./rulesets/cncidr.mrs",
+ },
+ cloudflarecidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/cloudflarecidr.mrs",
+ path: "./rulesets/cloudflarecidr.mrs",
+ },
+ googlecidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/googlecidr.mrs",
+ path: "./rulesets/googlecidr.mrs",
+ },
+ telegramcidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/telegramcidr.mrs",
+ path: "./rulesets/telegramcidr.mrs",
+ },
+ xcidr: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "ipcidr",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/xcidr.mrs",
+ path: "./rulesets/xcidr.mrs",
+ },
+ private: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/private.mrs",
+ path: "./rulesets/private.mrs",
+ },
+ direct: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/direct.mrs",
+ path: "./rulesets/direct.mrs",
+ },
+ proxy: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/proxy.mrs",
+ path: "./rulesets/proxy.mrs",
+ },
+ "geolocation-cn": {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/geolocation-cn.mrs",
+ path: "./rulesets/geolocation-cn.mrs",
+ },
+ reject: {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@basic/reject.mrs",
+ path: "./rulesets/reject.mrs",
+ },
+ "win-spy": {
+ ...ruleProviderDefaults,
+ format: "mrs",
+ behavior: "domain",
+ url: "https://cdn.jsdelivr.net/gh/xixu-me/RFM@universal/win-spy.mrs",
+ path: "./rulesets/win-spy.mrs",
+ },
+ ...generateServiceRuleProviders(services, ruleProviderDefaults),
+};
+
+// generate configuration using the above settings
+
+function validateOriginalConfig(config) {
+ if (!config)
+ throw new Error("Configuration object cannot be null or undefined");
+ const proxyCount = Array.isArray(config.proxies) ? config.proxies.length : 0;
+ const proxyProviderCount = Object.keys(
+ config["proxy-providers"] || {}
+ ).length;
+ if (proxyCount === 0 && proxyProviderCount === 0)
+ throw new Error(
+ "The original configuration must contain a non-empty proxies array (see https://wiki.\\u006D\\u0065\\u0074\\u0061\\u0063\\u0075\\u0062\\u0065\\u0078.one/en/config/proxies/) or a proxy-providers object with at least one property (see https://wiki.\\u006D\\u0065\\u0074\\u0061\\u0063\\u0075\\u0062\\u0065\\u0078.one/en/config/proxy-providers/)"
+ );
+ if (proxyCount > 0) {
+ config.proxies.forEach((proxy, index) => {
+ if (!proxy.name || !proxy.type || !proxy.server || !proxy.port) {
+ throw new Error(
+ \`Invalid proxy number \${
+ index + 1
+ } configuration (see https://wiki.\\u006D\\u0065\\u0074\\u0061\\u0063\\u0075\\u0062\\u0065\\u0078.one/en/config/proxies/)\`
+ );
+ }
+ });
+ console.log(
+ \`The original configuration contains \${proxyCount} proxies, which will be preserved\`
+ );
+ }
+ if (proxyProviderCount > 0) {
+ Object.entries(config["proxy-providers"]).forEach(
+ ([name, provider], index) => {
+ if (!provider.type || (provider.type === "http" && !provider.url)) {
+ throw new Error(
+ \`Invalid proxy provider number \${
+ index + 1
+ } configuration (see https://wiki.\\u006D\\u0065\\u0074\\u0061\\u0063\\u0075\\u0062\\u0065\\u0078.one/en/config/proxy-providers/)\`
+ );
+ }
+ }
+ );
+ console.log(
+ \`The original configuration contains \${proxyProviderCount} proxy providers, which will be preserved\`
+ );
+ }
+ return true;
+}
+
+function main(config) {
+ try {
+ validateOriginalConfig(config);
+ Object.assign(config, generalConfig, {
+ dns,
+ hosts,
+ sniffer,
+ tun,
+ "proxy-groups": proxyGroups,
+ rules,
+ "rule-providers": ruleProviders,
+ });
+ console.log("The generated configuration is as follows");
+ console.log(config);
+ return config;
+ } catch (error) {
+ console.error(
+ \`An error occurred during configuration generation: \${error.message}\`
+ );
+ return { error: error.message, originalConfig: config };
+ }
+}
+`;
diff --git a/script.js b/script.js
new file mode 100644
index 0000000..d338f66
--- /dev/null
+++ b/script.js
@@ -0,0 +1,371 @@
+document.addEventListener("DOMContentLoaded", () => {
+ const searchInput = document.getElementById("search-input");
+ const searchResults = document.getElementById("search-results");
+ const selectedServices = document.getElementById("selected-services");
+ const selectedCountEl = document.getElementById("selected-count");
+ const copyButton = document.getElementById("copyButton");
+ const downloadButton = document.getElementById("downloadButton");
+ const themeToggle = document.getElementById("theme-toggle");
+
+ const owner = "xixu-me";
+ const repo = "RFM";
+ const branch = "universal";
+ const apiUrl = `https://api.github.com/repos/${owner}/${repo}/git/trees/${branch}?recursive=1`;
+
+ let allServices = [];
+ let selectedServicesList = [];
+
+ // Theme management
+ function getSystemTheme() {
+ return window.matchMedia("(prefers-color-scheme: dark)").matches
+ ? "dark"
+ : "light";
+ }
+
+ function initTheme() {
+ const savedTheme = localStorage.getItem("theme");
+ const systemTheme = getSystemTheme();
+ const initialTheme = savedTheme || systemTheme;
+
+ document.documentElement.setAttribute("data-theme", initialTheme);
+ updateThemeIcon(initialTheme);
+
+ // Listen for system theme changes
+ window
+ .matchMedia("(prefers-color-scheme: dark)")
+ .addEventListener("change", (e) => {
+ // Only auto-switch if user hasn't manually set a preference
+ if (!localStorage.getItem("theme")) {
+ const newSystemTheme = e.matches ? "dark" : "light";
+ document.documentElement.setAttribute("data-theme", newSystemTheme);
+ updateThemeIcon(newSystemTheme);
+ }
+ });
+ }
+
+ function updateThemeIcon(theme) {
+ const icon = themeToggle.querySelector("i");
+ const isSystemDefault = !localStorage.getItem("theme");
+
+ if (isSystemDefault) {
+ // Show auto icon when following system
+ icon.className = "fas fa-circle-half-stroke";
+ themeToggle.title = "Theme: Auto (following system)";
+ } else if (theme === "dark") {
+ icon.className = "fas fa-sun";
+ themeToggle.title = "Switch to light mode";
+ } else {
+ icon.className = "fas fa-moon";
+ themeToggle.title = "Switch to dark mode";
+ }
+ }
+
+ function toggleTheme() {
+ const currentTheme = document.documentElement.getAttribute("data-theme");
+ const savedTheme = localStorage.getItem("theme");
+
+ if (!savedTheme) {
+ // First click: set to opposite of current system theme
+ const newTheme = currentTheme === "dark" ? "light" : "dark";
+ document.documentElement.setAttribute("data-theme", newTheme);
+ localStorage.setItem("theme", newTheme);
+ updateThemeIcon(newTheme);
+ } else {
+ // Subsequent clicks: cycle through light -> dark -> auto
+ if (savedTheme === "light") {
+ document.documentElement.setAttribute("data-theme", "dark");
+ localStorage.setItem("theme", "dark");
+ updateThemeIcon("dark");
+ } else if (savedTheme === "dark") {
+ // Reset to auto (system preference)
+ localStorage.removeItem("theme");
+ const systemTheme = getSystemTheme();
+ document.documentElement.setAttribute("data-theme", systemTheme);
+ updateThemeIcon(systemTheme);
+ }
+ }
+ }
+
+ async function fetchServices() {
+ try {
+ const response = await fetch(apiUrl);
+ if (!response.ok) {
+ throw new Error(`GitHub API error: ${response.status}`);
+ }
+ const data = await response.json();
+ const mrsFiles = data.tree.filter(
+ (file) => file.path.endsWith(".mrs") && file.type === "blob"
+ );
+
+ allServices = mrsFiles.map((file) => {
+ const name = file.path.replace(".mrs", "");
+ // A simple heuristic to format the name
+ const formattedName = name
+ .replace(/-/g, " ")
+ .replace("Google", "Google ")
+ .replace(" ", "");
+
+ const serviceDefinition = {
+ name: formattedName,
+ };
+
+ // Improved heuristics for tld, sld, alias, domain based on favicon availability and service patterns
+ if (name.includes("google-")) {
+ // Google services: use google TLD for favicon consistency
+ serviceDefinition.tld = "google";
+ serviceDefinition.alias = formattedName;
+ } else if (name === "xai") {
+ // xAI: specific case with SLD needed for proper favicon
+ serviceDefinition.name = "xAI";
+ serviceDefinition.tld = "ai";
+ serviceDefinition.sld = "x";
+ } else if (name === "rednote") {
+ // RedNote: use domain for services with non-standard favicon requirements
+ serviceDefinition.name = "rednote";
+ serviceDefinition.domain = "xiaohongshu.com";
+ serviceDefinition.alias = "Xiaohongshu";
+ } else if (name === "claude-ai") {
+ // Claude: use domain for Anthropic's service
+ serviceDefinition.domain = "claude.ai";
+ } else if (name === "openai") {
+ // OpenAI: simple case where service name matches SLD
+ serviceDefinition.tld = "com";
+ } else if (name === "github") {
+ // GitHub: simple case where service name matches SLD
+ serviceDefinition.tld = "com";
+ } else if (name.includes("-")) {
+ // Services with hyphens: check if last part is a known TLD
+ const parts = name.split("-");
+ const lastPart = parts[parts.length - 1];
+
+ if (["com", "org", "net", "ai", "io", "co"].includes(lastPart)) {
+ // If ends with known TLD, use it and add SLD if needed
+ serviceDefinition.tld = lastPart;
+ if (parts.length > 1) {
+ const remainingParts = parts.slice(0, -1);
+ if (remainingParts.length === 1) {
+ // Single part before TLD becomes SLD
+ serviceDefinition.sld = remainingParts[0];
+ } else {
+ // Multiple parts: join as SLD or use domain for complex cases
+ const potentialSld = remainingParts.join("-");
+ if (potentialSld.length <= 15) {
+ serviceDefinition.sld = potentialSld;
+ } else {
+ // For very long or complex names, prefer domain
+ serviceDefinition.domain = `${name.replace("-", ".")}.com`;
+ }
+ }
+ }
+ } else {
+ // No clear TLD pattern: check if it's a compound service name
+ if (
+ parts.length === 2 &&
+ parts[0].length <= 10 &&
+ parts[1].length <= 10
+ ) {
+ // Two reasonable parts: treat as SLD-TLD or use SLD with common TLD
+ serviceDefinition.tld = "com";
+ serviceDefinition.sld = parts[0];
+ } else {
+ // Complex name: use domain to ensure favicon availability
+ serviceDefinition.domain = `${name.replace(/-/g, "")}.com`;
+ }
+ }
+ } else {
+ // Single word services: default to com TLD
+ serviceDefinition.tld = "com";
+ }
+
+ return serviceDefinition;
+ });
+
+ updateSelectedCount();
+ } catch (error) {
+ searchResults.innerHTML = `Error loading services: ${error.message}
`;
+ console.error(error);
+ }
+ }
+
+ function showSearchResults(searchTerm) {
+ if (!searchTerm.trim()) {
+ searchResults.classList.remove("show");
+ return;
+ }
+
+ const filteredServices = allServices.filter(
+ (service) =>
+ service.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
+ !selectedServicesList.find((selected) => selected.name === service.name)
+ );
+
+ if (filteredServices.length === 0) {
+ searchResults.innerHTML =
+ 'No matching services found
';
+ } else {
+ searchResults.innerHTML = filteredServices
+ .map(
+ (service) => `
+
+ ${service.name}
+
+ `
+ )
+ .join("");
+ }
+
+ searchResults.classList.add("show");
+ }
+
+ function addService(service) {
+ if (
+ !selectedServicesList.find((selected) => selected.name === service.name)
+ ) {
+ selectedServicesList.push(service);
+ renderSelectedServices();
+ updateSelectedCount();
+ searchInput.value = "";
+ searchResults.classList.remove("show");
+ }
+ }
+
+ function removeService(serviceName) {
+ selectedServicesList = selectedServicesList.filter(
+ (service) => service.name !== serviceName
+ );
+ renderSelectedServices();
+ updateSelectedCount();
+ }
+
+ function renderSelectedServices() {
+ if (selectedServicesList.length === 0) {
+ selectedServices.innerHTML = `
+
+
+
No services selected
+
Search for services above to get started
+
+ `;
+ return;
+ }
+
+ selectedServices.innerHTML = selectedServicesList
+ .map(
+ (service) => `
+
+ ${service.name}
+
+
+ `
+ )
+ .join("");
+ }
+
+ function updateSelectedCount() {
+ const selectedCount = selectedServicesList.length;
+ const totalCount = allServices.length;
+ selectedCountEl.textContent = `${selectedCount} of ${totalCount} selected`;
+ }
+
+ function generateServicesString(services) {
+ return services
+ .map((s) => {
+ let definition = `{ name: "${s.name}"`;
+ if (s.tld) definition += `, tld: "${s.tld}"`;
+ if (s.sld) definition += `, sld: "${s.sld}"`;
+ if (s.domain) definition += `, domain: "${s.domain}"`;
+ if (s.alias) definition += `, alias: "${s.alias}"`;
+ definition += ` }`;
+ return definition;
+ })
+ .join(",\n ");
+ }
+
+ function generateOutput() {
+ const servicesString = generateServicesString(selectedServicesList);
+ return metaTemplate.replace("/* SERVICES_PLACEHOLDER */", servicesString);
+ }
+
+ // Event listeners
+ searchInput.addEventListener("input", (e) => {
+ showSearchResults(e.target.value);
+ });
+
+ searchInput.addEventListener("blur", () => {
+ // Delay hiding results to allow clicks on results
+ setTimeout(() => {
+ searchResults.classList.remove("show");
+ }, 200);
+ });
+
+ searchResults.addEventListener("click", (e) => {
+ if (e.target.classList.contains("search-result-item")) {
+ const serviceData = e.target.getAttribute("data-service");
+ if (serviceData) {
+ const service = JSON.parse(serviceData);
+ addService(service);
+ }
+ }
+ });
+
+ selectedServices.addEventListener("click", (e) => {
+ if (e.target.classList.contains("delete-button")) {
+ const serviceName = e.target.getAttribute("data-service");
+ removeService(serviceName);
+ }
+ });
+
+ copyButton.addEventListener("click", () => {
+ const generatedCode = generateOutput();
+ const originalContent = copyButton.innerHTML;
+
+ navigator.clipboard
+ .writeText(generatedCode)
+ .then(() => {
+ copyButton.innerHTML = ' Copied!';
+ copyButton.classList.add("btn-success");
+ setTimeout(() => {
+ copyButton.innerHTML = originalContent;
+ copyButton.classList.remove("btn-success");
+ }, 2000);
+ })
+ .catch((err) => {
+ alert("Failed to copy text.");
+ console.error("Copy error", err);
+ });
+ });
+
+ downloadButton.addEventListener("click", () => {
+ const generatedCode = generateOutput();
+ const originalContent = downloadButton.innerHTML;
+
+ downloadButton.innerHTML =
+ ' Downloading...';
+
+ const blob = new Blob([generatedCode], {
+ type: "text/javascript",
+ });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement("a");
+ a.href = url;
+ a.download = "META.js";
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+
+ setTimeout(() => {
+ downloadButton.innerHTML = originalContent;
+ }, 1000);
+ });
+
+ themeToggle.addEventListener("click", toggleTheme);
+
+ // Initialize
+ initTheme();
+ fetchServices();
+});
diff --git a/style.css b/style.css
new file mode 100644
index 0000000..88cfde9
--- /dev/null
+++ b/style.css
@@ -0,0 +1,1087 @@
+/* CSS Variables for theming */
+:root {
+ --primary-color: #2563eb;
+ --primary-hover: #1d4ed8;
+ --secondary-color: #64748b;
+ --secondary-hover: #475569;
+ --success-color: #059669;
+ --danger-color: #dc2626;
+ --danger-hover: #b91c1c;
+
+ /* Light theme */
+ --bg-primary: #ffffff;
+ --bg-secondary: #f8fafc;
+ --bg-tertiary: #f1f5f9;
+ --text-primary: #0f172a;
+ --text-secondary: #64748b;
+ --text-muted: #94a3b8;
+ --border-color: #e2e8f0;
+ --border-hover: #cbd5e1;
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1),
+ 0 4px 6px -4px rgb(0 0 0 / 0.1);
+}
+
+/* Dark theme */
+[data-theme="dark"] {
+ --bg-primary: #0f172a;
+ --bg-secondary: #1e293b;
+ --bg-tertiary: #334155;
+ --text-primary: #f8fafc;
+ --text-secondary: #cbd5e1;
+ --text-muted: #94a3b8;
+ --border-color: #334155;
+ --border-hover: #475569;
+ --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
+ --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
+ --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3),
+ 0 4px 6px -4px rgb(0 0 0 / 0.3);
+}
+
+[data-theme="dark"] body {
+ background: radial-gradient(
+ ellipse at top left,
+ rgba(59, 130, 246, 0.15) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at top right,
+ rgba(168, 85, 247, 0.15) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at bottom left,
+ rgba(34, 197, 94, 0.15) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at bottom right,
+ rgba(239, 68, 68, 0.15) 0%,
+ transparent 50%
+ ),
+ linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
+}
+
+[data-theme="dark"] body::before {
+ background: radial-gradient(
+ circle at 20% 80%,
+ rgba(59, 130, 246, 0.2) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ circle at 80% 20%,
+ rgba(168, 85, 247, 0.2) 0%,
+ transparent 50%
+ );
+}
+
+[data-theme="dark"] .card {
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+}
+
+[data-theme="dark"] .card:hover {
+ background: rgba(255, 255, 255, 0.08);
+}
+
+[data-theme="dark"] #theme-toggle {
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+}
+
+[data-theme="dark"] #theme-toggle:hover {
+ background: rgba(0, 0, 0, 0.4);
+}
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
+ "Helvetica Neue", Arial, sans-serif;
+ background: radial-gradient(
+ ellipse at top left,
+ rgba(120, 119, 198, 0.3) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at top right,
+ rgba(255, 105, 180, 0.3) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at bottom left,
+ rgba(255, 165, 0, 0.3) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ ellipse at bottom right,
+ rgba(0, 255, 255, 0.3) 0%,
+ transparent 50%
+ ),
+ linear-gradient(135deg, var(--bg-secondary) 0%, var(--bg-tertiary) 100%);
+ color: var(--text-primary);
+ min-height: 100vh;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow-x: hidden;
+}
+
+/* Animated background orbs for enhanced glass effect */
+body::before {
+ content: "";
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(
+ circle at 20% 80%,
+ rgba(120, 119, 198, 0.4) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ circle at 80% 20%,
+ rgba(255, 105, 180, 0.4) 0%,
+ transparent 50%
+ );
+ animation: float 20s ease-in-out infinite;
+ z-index: -1;
+}
+
+@keyframes float {
+ 0%,
+ 100% {
+ transform: translateY(0px) rotate(0deg);
+ }
+ 33% {
+ transform: translateY(-30px) rotate(120deg);
+ }
+ 66% {
+ transform: translateY(30px) rotate(240deg);
+ }
+}
+
+.theme-toggle {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 1000;
+}
+
+#theme-toggle {
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.2) 0%,
+ rgba(255, 255, 255, 0.1) 50%,
+ rgba(255, 255, 255, 0.2) 100%
+ );
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ border-radius: 50%;
+ width: 50px;
+ height: 50px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.15),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.1);
+ color: var(--text-primary);
+ position: relative;
+ overflow: hidden;
+}
+
+#theme-toggle:hover {
+ transform: scale(1.08) rotate(15deg);
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.3) 0%,
+ rgba(255, 255, 255, 0.2) 50%,
+ rgba(255, 255, 255, 0.3) 100%
+ );
+ border-color: rgba(255, 255, 255, 0.4);
+ box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.2),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.4),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.15);
+}
+
+#theme-toggle:active {
+ transform: scale(1.05) rotate(0deg);
+ transition: all 0.1s ease;
+}
+
+#theme-toggle:hover::after {
+ left: 150%;
+}
+
+#theme-toggle::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -150%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0.5) 20%,
+ rgba(255, 255, 255, 0.7) 50%,
+ rgba(255, 255, 255, 0.5) 80%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ transform: skewX(-25deg);
+ transition: left 0.75s ease-out;
+ border-radius: 50%;
+}
+
+#theme-toggle[title*="Auto"]:before {
+ content: "";
+ position: absolute;
+ top: -2px;
+ right: -2px;
+ width: 8px;
+ height: 8px;
+ background: var(--primary-color);
+ border-radius: 50%;
+ border: 2px solid var(--bg-primary);
+}
+
+.container {
+ max-width: 1000px;
+ margin: 0 auto;
+ padding: 2rem;
+ padding-top: 4rem;
+}
+
+.header {
+ text-align: center;
+ margin-bottom: 3rem;
+}
+
+.header h1 {
+ font-size: 2.5rem;
+ font-weight: 700;
+ color: var(--text-primary);
+ margin-bottom: 0.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+}
+
+.header h1 i {
+ color: var(--primary-color);
+}
+
+.subtitle {
+ font-size: 1.1rem;
+ color: var(--text-secondary);
+ line-height: 1.6;
+}
+
+.subtitle a {
+ color: var(--primary-color);
+ text-decoration: none;
+ font-weight: 500;
+ transition: color 0.2s ease;
+}
+
+.subtitle a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+.card {
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ -webkit-backdrop-filter: blur(15px);
+ border-radius: 20px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
+ margin-bottom: 2rem;
+ overflow: hidden;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.card::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.4),
+ transparent
+ );
+}
+
+.card:hover {
+ transform: translateY(-5px);
+ background: rgba(255, 255, 255, 0.15);
+ box-shadow: 0 15px 45px 0 rgba(0, 0, 0, 0.15);
+}
+
+.card-header {
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ padding: 1.5rem;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ position: relative;
+}
+
+.card-header::after {
+ content: "";
+ position: absolute;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ height: 1px;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.3),
+ transparent
+ );
+}
+
+.card-header h2 {
+ font-size: 1.5rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.card-header h2 i {
+ color: var(--primary-color);
+}
+
+.card-content {
+ padding: 1.5rem;
+}
+
+.output-actions {
+ display: flex;
+ gap: 1rem;
+ flex-wrap: wrap;
+}
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 12px 24px;
+ border: none;
+ border-radius: 12px;
+ font-size: 14px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ text-decoration: none;
+ min-width: 120px;
+ justify-content: center;
+ position: relative;
+ overflow: hidden;
+ backdrop-filter: blur(20px);
+ -webkit-backdrop-filter: blur(20px);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.12),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.1);
+}
+
+/* Enhanced liquid shine effect */
+.btn::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0.4) 20%,
+ rgba(255, 255, 255, 0.6) 50%,
+ rgba(255, 255, 255, 0.4) 80%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ transform: skewX(-25deg);
+ transition: left 0.6s ease-out;
+ z-index: 1;
+}
+
+.btn::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 12px;
+ background: inherit;
+ filter: blur(0.5px);
+ z-index: -1;
+}
+
+.btn:hover::before {
+ left: 100%;
+}
+
+.btn:active {
+ transform: translateY(1px) scale(0.98);
+ transition: all 0.1s ease;
+}
+
+.btn-primary {
+ background: linear-gradient(
+ 135deg,
+ rgba(37, 99, 235, 0.9) 0%,
+ rgba(29, 78, 216, 0.8) 50%,
+ rgba(37, 99, 235, 0.9) 100%
+ );
+ border: 1px solid rgba(59, 130, 246, 0.4);
+ color: white;
+ box-shadow: 0 8px 32px 0 rgba(37, 99, 235, 0.4),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.2);
+}
+
+.btn-primary:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(29, 78, 216, 0.95) 0%,
+ rgba(30, 64, 175, 0.9) 50%,
+ rgba(29, 78, 216, 0.95) 100%
+ );
+ transform: translateY(-3px) scale(1.02);
+ box-shadow: 0 15px 45px 0 rgba(37, 99, 235, 0.5),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.4),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.3);
+}
+
+.btn-secondary {
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.15) 0%,
+ rgba(255, 255, 255, 0.1) 50%,
+ rgba(255, 255, 255, 0.15) 100%
+ );
+ border: 1px solid rgba(255, 255, 255, 0.25);
+ color: var(--text-primary);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.08),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.25),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.1);
+}
+
+.btn-secondary:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(255, 255, 255, 0.2) 0%,
+ rgba(255, 255, 255, 0.15) 50%,
+ rgba(255, 255, 255, 0.2) 100%
+ );
+ transform: translateY(-3px) scale(1.02);
+ border-color: rgba(255, 255, 255, 0.35);
+ box-shadow: 0 15px 45px 0 rgba(0, 0, 0, 0.12),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.35),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.15);
+}
+
+.btn-success {
+ background: linear-gradient(
+ 135deg,
+ var(--success-color) 0%,
+ #047857 100%
+ ) !important;
+ color: white !important;
+}
+
+/* Footer */
+.footer {
+ margin-top: 2rem;
+ padding: 1.5rem;
+ text-align: center;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+ position: relative;
+ overflow: hidden;
+}
+
+.footer::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ transparent,
+ rgba(255, 255, 255, 0.1),
+ transparent
+ );
+ animation: shine 3s infinite;
+}
+
+.footer p {
+ margin: 0;
+ font-size: 0.9rem;
+ color: var(--text-secondary);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.5rem;
+ flex-wrap: wrap;
+}
+
+.footer a {
+ color: var(--primary-color);
+ text-decoration: none;
+ display: inline-flex;
+ align-items: center;
+ gap: 0.25rem;
+ transition: color 0.3s ease;
+}
+
+.footer a:hover {
+ color: var(--primary-color-hover);
+}
+
+.footer i {
+ font-size: 0.8rem;
+}
+
+/* Info Banner Styles */
+.info-banner {
+ background: linear-gradient(
+ 135deg,
+ rgba(59, 130, 246, 0.1) 0%,
+ rgba(168, 85, 247, 0.1) 100%
+ );
+ border: 2px solid rgba(59, 130, 246, 0.3);
+ margin-bottom: 2rem;
+}
+
+.info-banner .card-header {
+ background: linear-gradient(
+ 135deg,
+ rgba(59, 130, 246, 0.2) 0%,
+ rgba(168, 85, 247, 0.2) 100%
+ );
+ border-bottom: 1px solid rgba(59, 130, 246, 0.3);
+}
+
+.info-banner .card-header h2 {
+ color: var(--primary-color);
+ font-weight: 600;
+}
+
+.info-content {
+ line-height: 1.6;
+}
+
+.info-content p {
+ margin-bottom: 1rem;
+}
+
+.info-content p:last-child {
+ margin-bottom: 0;
+}
+
+.info-content ul {
+ margin: 1rem 0;
+ padding-left: 2rem;
+}
+
+.info-content li {
+ margin-bottom: 0.5rem;
+}
+
+.info-content code {
+ background: rgba(59, 130, 246, 0.1);
+ padding: 0.2rem 0.4rem;
+ border-radius: 4px;
+ font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
+ font-size: 0.875em;
+ color: var(--primary-color);
+ border: 1px solid rgba(59, 130, 246, 0.2);
+}
+
+.info-content a {
+ color: var(--primary-color);
+ text-decoration: none;
+ transition: all 0.3s ease;
+}
+
+.info-content a:hover {
+ color: var(--primary-hover);
+ text-decoration: underline;
+}
+
+[data-theme="dark"] .info-banner {
+ background: linear-gradient(
+ 135deg,
+ rgba(59, 130, 246, 0.15) 0%,
+ rgba(168, 85, 247, 0.15) 100%
+ );
+ border-color: rgba(59, 130, 246, 0.4);
+}
+
+[data-theme="dark"] .info-banner .card-header {
+ background: linear-gradient(
+ 135deg,
+ rgba(59, 130, 246, 0.25) 0%,
+ rgba(168, 85, 247, 0.25) 100%
+ );
+ border-bottom-color: rgba(59, 130, 246, 0.4);
+}
+
+[data-theme="dark"] .info-content code {
+ background: rgba(59, 130, 246, 0.2);
+ border-color: rgba(59, 130, 246, 0.3);
+ color: #93c5fd;
+}
+
+/* Additional styles for optimized info banner content */
+.info-content h3 {
+ margin: 1.5rem 0 1rem 0;
+ font-size: 1.1rem;
+ font-weight: 600;
+ color: var(--text-primary);
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.info-content h3 i {
+ color: var(--primary-color);
+ font-size: 1rem;
+}
+
+.info-content .example {
+ display: inline-block;
+ margin-left: 0.5rem;
+ font-style: italic;
+ color: var(--text-muted);
+ font-size: 0.9em;
+}
+
+.tip-box {
+ background: rgba(34, 197, 94, 0.1);
+ border: 1px solid rgba(34, 197, 94, 0.3);
+ border-radius: 8px;
+ padding: 1rem;
+ margin-top: 1.5rem;
+ display: flex;
+ align-items: flex-start;
+ gap: 0.75rem;
+}
+
+.tip-box i {
+ color: #059669;
+ margin-top: 0.2rem;
+ flex-shrink: 0;
+}
+
+.tip-box strong {
+ color: #059669;
+}
+
+[data-theme="dark"] .tip-box {
+ background: rgba(34, 197, 94, 0.15);
+ border-color: rgba(34, 197, 94, 0.4);
+}
+
+[data-theme="dark"] .tip-box i,
+[data-theme="dark"] .tip-box strong {
+ color: #10b981;
+}
+
+/* Responsive adjustments for info banner */
+@media (max-width: 768px) {
+ .info-content ul {
+ padding-left: 1.5rem;
+ }
+
+ .info-content code {
+ font-size: 0.8em;
+ }
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+ .container {
+ padding: 1rem;
+ padding-top: 3rem;
+ }
+
+ .header h1 {
+ font-size: 2rem;
+ }
+
+ .btn {
+ min-width: 100px;
+ padding: 10px 20px;
+ }
+
+ #theme-toggle {
+ width: 45px;
+ height: 45px;
+ }
+}
+
+/* Scrollbar styling */
+.search-results::-webkit-scrollbar {
+ width: 6px;
+}
+
+.search-results::-webkit-scrollbar-track {
+ background: var(--bg-secondary);
+}
+
+.search-results::-webkit-scrollbar-thumb {
+ background: var(--border-color);
+ border-radius: 3px;
+}
+
+.search-results::-webkit-scrollbar-thumb:hover {
+ background: var(--border-hover);
+}
+
+/* Animation for theme transition */
+* {
+ transition: background-color 0.3s ease, color 0.3s ease,
+ border-color 0.3s ease;
+}
+
+.services-controls {
+ margin-bottom: 1.5rem;
+}
+
+.search-container {
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+@media (min-width: 768px) {
+ .search-container {
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: center;
+ }
+}
+
+.search-input-wrapper {
+ position: relative;
+ flex: 1;
+}
+
+.search-icon {
+ position: absolute;
+ left: 12px;
+ top: 50%;
+ transform: translateY(-50%);
+ color: var(--text-muted);
+ font-size: 14px;
+}
+
+#search-input {
+ width: 100%;
+ padding: 12px 16px 12px 40px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ font-size: 16px;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ color: var(--text-primary);
+ transition: all 0.3s ease;
+}
+
+#search-input:focus {
+ outline: none;
+ border-color: rgba(37, 99, 235, 0.5);
+ background: rgba(255, 255, 255, 0.15);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1), 0 8px 32px 0 rgba(0, 0, 0, 0.1);
+}
+
+#search-input::placeholder {
+ color: var(--text-muted);
+}
+
+.services-count {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-size: 14px;
+ color: var(--text-secondary);
+ font-weight: 500;
+ white-space: nowrap;
+}
+
+.services-count i {
+ color: var(--success-color);
+}
+
+.search-results {
+ position: relative;
+ background: rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(15px);
+ -webkit-backdrop-filter: blur(15px);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 12px;
+ margin-top: 0.5rem;
+ max-height: 250px;
+ overflow-y: auto;
+ display: none;
+ z-index: 10;
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
+}
+
+.search-results.show {
+ display: block;
+ animation: slideDown 0.3s ease-out;
+}
+
+@keyframes slideDown {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.search-result-item {
+ padding: 12px 16px;
+ cursor: pointer;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ transition: all 0.2s ease;
+ color: var(--text-primary);
+ position: relative;
+ overflow: hidden;
+}
+
+.search-result-item:last-child {
+ border-bottom: none;
+}
+
+.search-result-item:hover {
+ background: rgba(255, 255, 255, 0.1);
+ transform: translateX(5px);
+}
+
+.search-result-item:hover::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(90deg, rgba(255, 255, 255, 0.1), transparent);
+ pointer-events: none;
+}
+
+#selected-services {
+ min-height: 120px;
+}
+
+.no-services {
+ text-align: center;
+ padding: 3rem 2rem;
+ background: rgba(255, 255, 255, 0.05);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 2px dashed rgba(255, 255, 255, 0.2);
+ border-radius: 16px;
+ color: var(--text-muted);
+ position: relative;
+ overflow: hidden;
+}
+
+.no-services::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: radial-gradient(
+ circle at 30% 40%,
+ rgba(255, 255, 255, 0.1) 0%,
+ transparent 50%
+ ),
+ radial-gradient(
+ circle at 70% 60%,
+ rgba(255, 255, 255, 0.05) 0%,
+ transparent 50%
+ );
+ pointer-events: none;
+}
+
+.no-services i {
+ font-size: 2rem;
+ margin-bottom: 1rem;
+ color: var(--text-muted);
+}
+
+.no-services p {
+ font-size: 1.1rem;
+ margin-bottom: 0.5rem;
+}
+
+.no-services small {
+ font-size: 0.9rem;
+}
+
+.selected-service-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 12px 16px;
+ margin-bottom: 0.75rem;
+ background: rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(10px);
+ -webkit-backdrop-filter: blur(10px);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ border-radius: 12px;
+ transition: all 0.3s ease;
+ position: relative;
+ overflow: hidden;
+}
+
+.selected-service-item::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0.1) 50%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ transition: left 0.5s ease;
+}
+
+.selected-service-item:hover {
+ transform: translateX(4px);
+ background: rgba(255, 255, 255, 0.12);
+ border-color: rgba(255, 255, 255, 0.25);
+ box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.1);
+}
+
+.selected-service-item:hover::before {
+ left: 100%;
+}
+
+.selected-service-item .service-name {
+ font-weight: 500;
+ color: var(--text-primary);
+}
+
+.delete-button {
+ background: linear-gradient(
+ 135deg,
+ rgba(220, 38, 38, 0.9) 0%,
+ rgba(185, 28, 28, 0.8) 50%,
+ rgba(220, 38, 38, 0.9) 100%
+ );
+ border: 1px solid rgba(248, 113, 113, 0.4);
+ color: white;
+ border-radius: 10px;
+ padding: 8px 14px;
+ font-size: 12px;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ backdrop-filter: blur(15px);
+ -webkit-backdrop-filter: blur(15px);
+ position: relative;
+ overflow: hidden;
+ text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+ box-shadow: 0 6px 24px 0 rgba(220, 38, 38, 0.4),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.2),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.2);
+}
+
+.delete-button::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: -100%;
+ width: 100%;
+ height: 100%;
+ background: linear-gradient(
+ 90deg,
+ rgba(255, 255, 255, 0) 0%,
+ rgba(255, 255, 255, 0.3) 20%,
+ rgba(255, 255, 255, 0.5) 50%,
+ rgba(255, 255, 255, 0.3) 80%,
+ rgba(255, 255, 255, 0) 100%
+ );
+ transform: skewX(-25deg);
+ transition: left 0.5s ease-out;
+ z-index: 1;
+}
+
+.delete-button::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ border-radius: 10px;
+ background: inherit;
+ filter: blur(0.5px);
+ z-index: -1;
+}
+
+.delete-button:hover {
+ background: linear-gradient(
+ 135deg,
+ rgba(185, 28, 28, 0.95) 0%,
+ rgba(153, 27, 27, 0.9) 50%,
+ rgba(185, 28, 28, 0.95) 100%
+ );
+ transform: scale(1.08) translateY(-1px);
+ border-color: rgba(248, 113, 113, 0.6);
+ box-shadow: 0 10px 32px 0 rgba(220, 38, 38, 0.5),
+ inset 0 1px 0 0 rgba(255, 255, 255, 0.3),
+ inset 0 -1px 0 0 rgba(0, 0, 0, 0.3);
+}
+
+.delete-button:hover::before {
+ left: 100%;
+}
+
+.delete-button:active {
+ transform: scale(1.05) translateY(0px);
+ transition: all 0.1s ease;
+}