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:
1 parent
e3b7c7ecbd
commit
1e0dafbda9
6 files changed
+2388
No files matched your search
@@ -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
@@ -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>
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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();
|
||||
});
|
||||
Reference in new issue
Block a user