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 + + + + +
+ +
+ +
+
+

META.js Customizer

+

+ Create your custom META.js of + + xixu-me/META + with service rulesets from + + xixu-me/RFM +

+
+ +
+
+

Services

+
+
+
+
+
+ + +
+
+ + 0 selected +
+
+
+
+
+
+ +

No services selected

+ Search for services above to get started +
+
+
+
+ +
+
+

Generated META.js

+
+
+
+ + +
+
+
+ +
+
+

Icon Configuration Guide

+
+
+
+

+ 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; +}