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.
This commit is contained in:
xixu-me committed 2025-07-13 22:07:38 +08:00
1 parent e3b7c7ecbd
commit 1e0dafbda9
6 files changed
+2388

No files matched your search

+1
View File
@@ -0,0 +1 @@
metajs.xi-xu.me
+117
View File
@@ -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.
+150
View File
@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>META.js Customizer</title>
<link rel="stylesheet" href="style.css" />
<link
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"
rel="stylesheet"
/>
</head>
<body>
<div class="theme-toggle">
<button id="theme-toggle" aria-label="Toggle dark mode">
<i class="fas fa-moon"></i>
</button>
</div>
<div class="container">
<header class="header">
<h1><i class="fas fa-cogs"></i> META.js Customizer</h1>
<p class="subtitle">
Create your custom META.js of
<a href="https://github.com/xixu-me/META" target="_blank">
<i class="fab fa-github"></i> xixu-me/META</a
>
with service rulesets from
<a href="https://github.com/xixu-me/RFM" target="_blank">
<i class="fab fa-github"></i> xixu-me/RFM</a
>
</p>
</header>
<div id="services-container" class="card">
<div class="card-header">
<h2><i class="fas fa-list"></i> Services</h2>
</div>
<div class="card-content">
<div class="services-controls">
<div class="search-container">
<div class="search-input-wrapper">
<i class="fas fa-search search-icon"></i>
<input
type="text"
id="search-input"
placeholder="Search and add services..."
/>
</div>
<div class="services-count">
<i class="fas fa-check-circle"></i>
<span id="selected-count">0 selected</span>
</div>
</div>
<div id="search-results" class="search-results"></div>
</div>
<div id="selected-services">
<div class="no-services">
<i class="fas fa-inbox"></i>
<p>No services selected</p>
<small>Search for services above to get started</small>
</div>
</div>
</div>
</div>
<div id="output-container" class="card">
<div class="card-header">
<h2><i class="fas fa-code"></i> Generated META.js</h2>
</div>
<div class="card-content">
<div class="output-actions">
<button id="copyButton" class="btn btn-primary">
<i class="fas fa-copy"></i> Copy
</button>
<button id="downloadButton" class="btn btn-secondary">
<i class="fas fa-download"></i> Download
</button>
</div>
</div>
</div>
<div class="info-banner card">
<div class="card-header">
<h2><i class="fas fa-info-circle"></i> Icon Configuration Guide</h2>
</div>
<div class="card-content">
<div class="info-content">
<p>
Service icons are sourced from
<a href="https://github.com/xixu-me/favicons" target="_blank">
<i class="fab fa-github"></i> xixu-me/favicons</a
>. Auto-generated configurations may require manual adjustments
based on availability.
</p>
<h3><i class="fas fa-cog"></i> Configuration Rules</h3>
<ul>
<li>
<strong>Icon unavailable:</strong> Uses specific
<code>domain</code>
<span class="example"
>(e.g., "xiaohongshu.com" for rednote)</span
>
</li>
<li>
<strong>Name matches SLD:</strong> Uses only <code>tld</code>
<span class="example"
>(e.g., "github" → <code>tld: "com"</code>)</span
>
</li>
<li>
<strong>Name does not match SLD:</strong> Adds <code>sld</code>
<span class="example"
>(e.g., "xai" → <code>sld: "x", tld: "ai"</code>)</span
>
</li>
</ul>
<div class="tip-box">
<i class="fas fa-lightbulb"></i>
<strong>Pro Tip:</strong> Browse the
<a
href="https://github.com/xixu-me/favicons/tree/assets"
target="_blank"
>
assets branch</a
>
to verify icon availability before finalizing your configuration.
</div>
</div>
</div>
</div>
<footer class="footer">
<p>
<i class="fas fa-code-branch"></i>
This web app is open source at
<a
href="https://github.com/xixu-me/META.js-Customizer"
target="_blank"
><i class="fab fa-github"></i> xixu-me/META.js-Customizer</a
>
</p>
</footer>
</div>
<script src="meta_template.js"></script>
<script src="script.js"></script>
</body>
</html>
+662
View File
@@ -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 };
}
}
`;
+371
View File
@@ -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 = `<p style="padding: 1rem;">Error loading services: ${error.message}</p>`;
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 =
'<div class="search-result-item">No matching services found</div>';
} else {
searchResults.innerHTML = filteredServices
.map(
(service) => `
<div class="search-result-item" data-service='${JSON.stringify(
service
)}'>
${service.name}
</div>
`
)
.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 = `
<div class="no-services">
<i class="fas fa-inbox"></i>
<p>No services selected</p>
<small>Search for services above to get started</small>
</div>
`;
return;
}
selectedServices.innerHTML = selectedServicesList
.map(
(service) => `
<div class="selected-service-item">
<span class="service-name">${service.name}</span>
<button class="delete-button" data-service="${service.name}">
<i class="fas fa-times"></i> Remove
</button>
</div>
`
)
.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 = '<i class="fas fa-check"></i> 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 =
'<i class="fas fa-spinner fa-spin"></i> 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();
});
+1087
View File
File diff suppressed because it is too large. Load diff