small-package/luci-app-nekobox/htdocs/nekobox/mihomo_manager.php

1270 lines
50 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
ob_start();
include './cfg.php';
$uploadDir = '/etc/neko/proxy_provider/';
$configDir = '/etc/neko/config/';
ini_set('memory_limit', '256M');
if (!is_dir($uploadDir)) {
mkdir($uploadDir, 0755, true);
}
if (!is_dir($configDir)) {
mkdir($configDir, 0755, true);
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_FILES['fileInput'])) {
$file = $_FILES['fileInput'];
$uploadFilePath = $uploadDir . basename($file['name']);
if ($file['error'] === UPLOAD_ERR_OK) {
if (move_uploaded_file($file['tmp_name'], $uploadFilePath)) {
echo '文件上传成功:' . htmlspecialchars(basename($file['name']));
} else {
echo '文件上传失败!';
}
} else {
echo '上传错误:' . $file['error'];
}
}
if (isset($_FILES['configFileInput'])) {
$file = $_FILES['configFileInput'];
$uploadFilePath = $configDir . basename($file['name']);
if ($file['error'] === UPLOAD_ERR_OK) {
if (move_uploaded_file($file['tmp_name'], $uploadFilePath)) {
echo '配置文件上传成功:' . htmlspecialchars(basename($file['name']));
} else {
echo '配置文件上传失败!';
}
} else {
echo '上传错误:' . $file['error'];
}
}
if (isset($_POST['deleteFile'])) {
$fileToDelete = $uploadDir . basename($_POST['deleteFile']);
if (file_exists($fileToDelete) && unlink($fileToDelete)) {
echo '文件删除成功:' . htmlspecialchars(basename($_POST['deleteFile']));
} else {
echo '文件删除失败!';
}
}
if (isset($_POST['deleteConfigFile'])) {
$fileToDelete = $configDir . basename($_POST['deleteConfigFile']);
if (file_exists($fileToDelete) && unlink($fileToDelete)) {
echo '配置文件删除成功:' . htmlspecialchars(basename($_POST['deleteConfigFile']));
} else {
echo '配置文件删除失败!';
}
}
if (isset($_POST['oldFileName'], $_POST['newFileName'], $_POST['fileType'])) {
$oldFileName = basename($_POST['oldFileName']);
$newFileName = basename($_POST['newFileName']);
$fileType = $_POST['fileType'];
if ($fileType === 'proxy') {
$oldFilePath = $uploadDir. $oldFileName;
$newFilePath = $uploadDir. $newFileName;
} elseif ($fileType === 'config') {
$oldFilePath = $configDir . $oldFileName;
$newFilePath = $configDir . $newFileName;
} else {
echo '无效的文件类型';
exit;
}
if (file_exists($oldFilePath) && !file_exists($newFilePath)) {
if (rename($oldFilePath, $newFilePath)) {
echo '文件重命名成功:' . htmlspecialchars($oldFileName) . ' -> ' . htmlspecialchars($newFileName);
} else {
echo '文件重命名失败!';
}
} else {
echo '文件重命名失败,文件不存在或新文件名已存在。';
}
}
if (isset($_POST['saveContent'], $_POST['fileName'], $_POST['fileType'])) {
$fileToSave = ($_POST['fileType'] === 'proxy') ? $uploadDir . basename($_POST['fileName']) : $configDir . basename($_POST['fileName']);
$contentToSave = $_POST['saveContent'];
file_put_contents($fileToSave, $contentToSave);
echo '<p>文件内容已更新:' . htmlspecialchars(basename($fileToSave)) . '</p>';
}
}
function formatFileModificationTime($filePath) {
if (file_exists($filePath)) {
$fileModTime = filemtime($filePath);
return date('Y-m-d H:i:s', $fileModTime);
} else {
return '文件不存在';
}
}
$proxyFiles = scandir($uploadDir);
$configFiles = scandir($configDir);
if ($proxyFiles !== false) {
$proxyFiles = array_diff($proxyFiles, array('.', '..'));
} else {
$proxyFiles = [];
}
if ($configFiles !== false) {
$configFiles = array_diff($configFiles, array('.', '..'));
} else {
$configFiles = [];
}
function formatSize($size) {
$units = array('B', 'KB', 'MB', 'GB', 'TB');
$unit = 0;
while ($size >= 1024 && $unit < count($units) - 1) {
$size /= 1024;
$unit++;
}
return round($size, 2) . ' ' . $units[$unit];
}
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['editFile'], $_GET['fileType'])) {
$filePath = ($_GET['fileType'] === 'proxy') ? $uploadDir. basename($_GET['editFile']) : $configDir . basename($_GET['editFile']);
if (file_exists($filePath)) {
header('Content-Type: text/plain');
echo file_get_contents($filePath);
exit;
} else {
echo '文件不存在';
exit;
}
}
?>
<?php
$subscriptionPath = '/etc/neko/proxy_provider/';
$subscriptionFile = $subscriptionPath . 'subscriptions.json';
$clashFile = $subscriptionPath . 'subscription_6.yaml';
$message = "";
$decodedContent = "";
$subscriptions = [];
$updateCompleted = false;
function outputMessage($message) {
if (!isset($_SESSION['update_messages'])) {
$_SESSION['update_messages'] = array();
}
$_SESSION['update_messages'][] = $message;
}
if (!file_exists($subscriptionPath)) {
mkdir($subscriptionPath, 0755, true);
}
if (!file_exists($subscriptionFile)) {
file_put_contents($subscriptionFile, json_encode([]));
}
$subscriptions = json_decode(file_get_contents($subscriptionFile), true);
if (!$subscriptions) {
for ($i = 0; $i < 6; $i++) {
$subscriptions[$i] = [
'url' => '',
'file_name' => "subscription_{$i}.yaml",
];
}
}
if (isset($_POST['update'])) {
$index = intval($_POST['index']);
$url = $_POST['subscription_url'] ?? '';
$customFileName = $_POST['custom_file_name'] ?? "subscription_{$index}.yaml";
$subscriptions[$index]['url'] = $url;
$subscriptions[$index]['file_name'] = $customFileName;
if (!empty($url)) {
$finalPath = $subscriptionPath . $customFileName;
$command = "wget -q --show-progress -O {$finalPath} {$url}";
exec($command . ' 2>&1', $output, $return_var);
if ($return_var === 0) {
$_SESSION['update_messages'] = array();
$_SESSION['update_messages'][] = '<div class="alert alert-warning" style="margin-bottom: 8px;">
<strong>⚠️ 使用说明:</strong>
<ul class="mb-0 pl-3">
<li>通用模板mihomo.yaml最多支持<strong>6个</strong>订阅链接</li>
<li>请勿更改默认文件名称</li>
<li>该模板支持所有格式订阅链接,无需额外转换</li>
</ul>
</div>';
$_SESSION['update_messages'][] = "订阅链接 {$url} 更新成功!文件已保存到: {$finalPath}";
$message = '更新成功';
$updateCompleted = true;
} else {
$_SESSION['update_messages'] = array();
$_SESSION['update_messages'][] = "配置更新失败!错误信息: " . implode("\n", $output);
$message = '更新失败';
}
} else {
$_SESSION['update_messages'] = array();
$_SESSION['update_messages'][] = "第" . ($index + 1) . "个订阅链接为空!";
$message = '更新失败';
}
file_put_contents($subscriptionFile, json_encode($subscriptions));
}
if (isset($_POST['convert_base64'])) {
$base64Content = $_POST['base64_content'] ?? '';
if (!empty($base64Content)) {
$decodedContent = base64_decode($base64Content);
if ($decodedContent === false) {
$message = "Base64 解码失败,请检查输入!";
} else {
$clashConfig = "# Clash Meta Config\n\n";
$clashConfig .= $decodedContent;
file_put_contents($clashFile, $clashConfig);
$message = "Clash 配置文件已生成并保存到: {$clashFile}";
}
} else {
$message = "Base64 内容为空!";
}
}
?>
<?php
function parseVmess($base) {
$decoded = base64_decode($base['host']);
$arrjs = json_decode($decoded, true);
if (json_last_error() !== JSON_ERROR_NONE || empty($arrjs['v'])) {
return "DECODING FAILED! PLEASE CHECK YOUR URL!";
}
return [
'cfgtype' => $base['scheme'] ?? '',
'name' => $arrjs['ps'] ?? '',
'host' => $arrjs['add'] ?? '',
'port' => $arrjs['port'] ?? '',
'uuid' => $arrjs['id'] ?? '',
'alterId' => $arrjs['aid'] ?? '',
'type' => $arrjs['net'] ?? '',
'path' => $arrjs['path'] ?? '',
'security' => $arrjs['type'] ?? '',
'sni' => $arrjs['host'] ?? '',
'tls' => $arrjs['tls'] ?? ''
];
}
function parseShadowsocks($basebuff, &$urlparsed) {
$urlparsed['uuid'] = $basebuff['user'] ?? '';
$basedata = explode(":", base64_decode($urlparsed['uuid']));
if (count($basedata) == 2) {
$urlparsed['cipher'] = $basedata[0];
$urlparsed['uuid'] = $basedata[1];
}
}
function parseUrl($basebuff) {
$urlparsed = [
'cfgtype' => $basebuff['scheme'] ?? '',
'name' => $basebuff['fragment'] ?? '',
'host' => $basebuff['host'] ?? '',
'port' => $basebuff['port'] ?? ''
];
if ($urlparsed['cfgtype'] == 'ss') {
parseShadowsocks($basebuff, $urlparsed);
} else {
$urlparsed['uuid'] = $basebuff['user'] ?? '';
}
$querybuff = [];
$tmpquery = $basebuff['query'] ?? '';
if ($urlparsed['cfgtype'] == 'ss') {
parse_str(str_replace(";", "&", $tmpquery), $querybuff);
$urlparsed['mux'] = $querybuff['mux'] ?? '';
$urlparsed['host2'] = $querybuff['host2'] ?? '';
} else {
parse_str($tmpquery, $querybuff);
}
$urlparsed['type'] = $querybuff['type'] ?? '';
$urlparsed['path'] = $querybuff['path'] ?? '';
$urlparsed['mode'] = $querybuff['mode'] ?? '';
$urlparsed['plugin'] = $querybuff['plugin'] ?? '';
$urlparsed['security'] = $querybuff['security'] ?? '';
$urlparsed['encryption'] = $querybuff['encryption'] ?? '';
$urlparsed['serviceName'] = $querybuff['serviceName'] ?? '';
$urlparsed['sni'] = $querybuff['sni'] ?? '';
return $urlparsed;
}
function generateConfig($data) {
$outcfg = "";
if (empty($GLOBALS['isProxiesPrinted'])) {
$outcfg .= "proxies:\n";
$GLOBALS['isProxiesPrinted'] = true;
}
switch ($data['cfgtype']) {
case 'vless':
$outcfg .= generateVlessConfig($data);
break;
case 'trojan':
$outcfg .= generateTrojanConfig($data);
break;
case 'hysteria2':
case 'hy2':
$outcfg .= generateHysteria2Config($data);
break;
case 'ss':
$outcfg .= generateShadowsocksConfig($data);
break;
case 'vmess':
$outcfg .= generateVmessConfig($data);
break;
}
return $outcfg;
}
function generateVlessConfig($data) {
$config = " - name: " . ($data['name'] ?: "VLESS") . "\n";
$config .= " type: {$data['cfgtype']}\n";
$config .= " server: {$data['host']}\n";
$config .= " port: {$data['port']}\n";
$config .= " uuid: {$data['uuid']}\n";
$config .= " cipher: auto\n";
$config .= " tls: true\n";
if ($data['type'] == "ws") {
$config .= " network: ws\n";
$config .= " ws-opts:\n";
$config .= " path: {$data['path']}\n";
$config .= " Headers:\n";
$config .= " Host: {$data['host']}\n";
$config .= " flow:\n";
$config .= " client-fingerprint: chrome\n";
} elseif ($data['type'] == "grpc") {
$config .= " network: grpc\n";
$config .= " grpc-opts:\n";
$config .= " grpc-service-name: {$data['serviceName']}\n";
}
$config .= " udp: true\n";
$config .= " skip-cert-verify: true\n";
return $config;
}
function generateTrojanConfig($data) {
$config = " - name: " . ($data['name'] ?: "TROJAN") . "\n";
$config .= " type: {$data['cfgtype']}\n";
$config .= " server: {$data['host']}\n";
$config .= " port: {$data['port']}\n";
$config .= " password: {$data['uuid']}\n";
$config .= " sni: " . (!empty($data['sni']) ? $data['sni'] : $data['host']) . "\n";
if ($data['type'] == "ws") {
$config .= " network: ws\n";
$config .= " ws-opts:\n";
$config .= " path: {$data['path']}\n";
$config .= " Headers:\n";
$config .= " Host: {$data['sni']}\n";
} elseif ($data['type'] == "grpc") {
$config .= " network: grpc\n";
$config .= " grpc-opts:\n";
$config .= " grpc-service-name: {$data['serviceName']}\n";
}
$config .= " udp: true\n";
$config .= " skip-cert-verify: true\n";
return $config;
}
function generateHysteria2Config($data) {
return " - name: " . ($data['name'] ?: "HYSTERIA2") . "\n" .
" server: {$data['host']}\n" .
" port: {$data['port']}\n" .
" type: {$data['cfgtype']}\n" .
" password: {$data['uuid']}\n" .
" udp: true\n" .
" ports: 20000-55000\n" .
" mport: 20000-55000\n" .
" skip-cert-verify: true\n" .
" sni: " . (!empty($data['sni']) ? $data['sni'] : $data['host']) . "\n";
}
function generateShadowsocksConfig($data) {
$config = " - name: " . ($data['name'] ?: "SHADOWSOCKS") . "\n";
$config .= " type: {$data['cfgtype']}\n";
$config .= " server: {$data['host']}\n";
$config .= " port: {$data['port']}\n";
$config .= " cipher: {$data['cipher']}\n";
$config .= " password: {$data['uuid']}\n";
if (!empty($data['plugin'])) {
$config .= " plugin: {$data['plugin']}\n";
$config .= " plugin-opts:\n";
if ($data['plugin'] == "v2ray-plugin" || $data['plugin'] == "xray-plugin") {
$config .= " mode: websocket\n";
$config .= " mux: {$data['mux']}\n";
} elseif ($data['plugin'] == "obfs") {
$config .= " mode: tls\n";
}
}
$config .= " udp: true\n";
$config .= " skip-cert-verify: true\n";
return $config;
}
function generateVmessConfig($data) {
$config = " - name: " . ($data['name'] ?: "VMESS") . "\n";
$config .= " type: {$data['cfgtype']}\n";
$config .= " server: {$data['host']}\n";
$config .= " port: {$data['port']}\n";
$config .= " uuid: {$data['uuid']}\n";
$config .= " alterId: {$data['alterId']}\n";
$config .= " cipher: auto\n";
$config .= " tls: " . ($data['tls'] === "tls" ? "true" : "false") . "\n";
$config .= " servername: " . (!empty($data['sni']) ? $data['sni'] : $data['host']) . "\n";
$config .= " network: {$data['type']}\n";
if ($data['type'] == "ws") {
$config .= " ws-opts:\n";
$config .= " path: {$data['path']}\n";
$config .= " Headers:\n";
$config .= " Host: {$data['sni']}\n";
} elseif ($data['type'] == "grpc") {
$config .= " grpc-opts:\n";
$config .= " grpc-service-name: {$data['serviceName']}\n";
}
$config .= " udp: true\n";
$config .= " skip-cert-verify: true\n";
return $config;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$input = $_POST['input'] ?? '';
if (empty($input)) {
echo ".";
} else {
$lines = explode("\n", trim($input));
$allcfgs = "";
$GLOBALS['isProxiesPrinted'] = false;
foreach ($lines as $line) {
$base64url = parse_url($line);
if ($base64url === false) {
$allcfgs .= "Invalid URL provided.\n";
continue;
}
$base64url = array_map('urldecode', $base64url);
if (isset($base64url['scheme']) && $base64url['scheme'] === 'vmess') {
$parsedData = parseVmess($base64url);
} else {
$parsedData = parseUrl($base64url);
}
if (is_array($parsedData)) {
$allcfgs .= generateConfig($parsedData);
} else {
$allcfgs .= $parsedData . "\n";
}
}
$file_path = '/etc/neko/proxy_provider/subscription_7.json';
file_put_contents($file_path, $allcfgs);
echo "<h2 style=\"color: #00FFFF;\">转换完成</h2>";
echo "<p>配置文件已经成功保存到 <strong>$file_path</strong></p>";
}
}
?>
<!doctype html>
<html lang="en" data-bs-theme="<?php echo substr($neko_theme, 0, -4) ?>">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mihomo - Neko</title>
<link rel="icon" href="./assets/img/nekobox.png">
<link href="./assets/css/bootstrap.min.css" rel="stylesheet">
<link href="./assets/css/custom.css" rel="stylesheet">
<link href="./assets/theme/<?php echo $neko_theme ?>" rel="stylesheet">
<script src="./assets/js/feather.min.js"></script>
<script src="./assets/js/jquery-2.1.3.min.js"></script>
<script src="./assets/js/neko.js"></script>
<script src="./assets/bootstrap/popper.min.js"></script>
<script src="./assets/bootstrap/bootstrap.min.js"></script>
</head>
<?php if ($updateCompleted): ?>
<script>
if (!sessionStorage.getItem('refreshed')) {
sessionStorage.setItem('refreshed', 'true');
window.location.reload();
} else {
sessionStorage.removeItem('refreshed');
}
</script>
<?php endif; ?>
<body>
<div class="position-fixed w-100 d-flex justify-content-center" style="top: 20px; z-index: 1050">
<div id="updateAlert" class="alert alert-success alert-dismissible fade" role="alert" style="display: none; min-width: 300px; max-width: 600px;">
<div class="d-flex align-items-center">
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
<strong>更新完成</strong>
</div>
<div id="updateMessages" class="small">
</div>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
</div>
<style>
.alert-success {
background-color: #2b3035 !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
border-radius: 8px !important;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3) !important;
padding: 16px 20px !important;
position: relative;
color: #fff !important;
backdrop-filter: blur(10px);
margin-top: 15px !important;
}
.alert .close {
position: absolute !important;
right: 10px !important;
top: 10px !important;
background-color: #dc3545 !important;
opacity: 1 !important;
width: 20px !important;
height: 20px !important;
border-radius: 50% !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
font-size: 14px !important;
color: #fff !important;
border: none !important;
padding: 0 !important;
margin: 0 !important;
transition: all 0.2s ease !important;
text-shadow: none !important;
line-height: 1 !important;
}
.alert .close:hover {
background-color: #bd2130 !important;
transform: rotate(90deg);
}
#updateMessages {
margin-top: 12px;
padding-right: 20px;
font-size: 14px;
line-height: 1.5;
color: rgba(255, 255, 255, 0.9);
}
#updateMessages .alert-warning {
background-color: rgba(255, 193, 7, 0.1) !important;
border-radius: 6px;
padding: 12px 15px;
border: 1px solid rgba(255, 193, 7, 0.2);
}
#updateMessages ul {
margin-bottom: 0;
padding-left: 20px;
}
#updateMessages li {
margin-bottom: 6px;
color: rgba(255, 255, 255, 0.9);
}
.spinner-border-sm {
margin-right: 10px;
border-color: #fff;
border-right-color: transparent;
}
#updateMessages > div:not(.alert-warning) {
padding: 8px 0;
color: #00ff9d;
}
@media (max-width: 767px) {
.table th,
.table td {
padding: 6px 8px;
font-size: 14px;
}
.table th:nth-child(1), .table td:nth-child(1) {
width: 25%;
}
.table th:nth-child(2), .table td:nth-child(2) {
width: 20%;
}
.table th:nth-child(3), .table td:nth-child(3) {
width: 25%;
}
.table th:nth-child(4), .table td:nth-child(4) {
width: 100%;
}
.btn-group, .d-flex {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px;
}
.btn-group .btn {
flex: 1 1 auto;
font-size: 12px;
padding: 6px 8px;
}
.btn-group .btn:last-child {
margin-right: 0;
}
}
@media (max-width: 767px) {
.btn-rename {
width: 70px !important;
font-size: 0.6rem;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: inline-block;
text-align: center;
}
.btn-group {
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
margin: 0;
}
td {
vertical-align: middle;
}
.action-btn {
padding: 6px 12px;
font-size: 0.85rem;
display: inline-block;
}
.btn-group.d-flex {
flex-wrap: wrap;
}
</style>
<script>
function showUpdateAlert() {
const alert = $('#updateAlert');
const messages = <?php echo json_encode($_SESSION['update_messages'] ?? []); ?>;
if (messages.length > 0) {
const messagesHtml = messages.map(msg => `<div>${msg}</div>`).join('');
$('#updateMessages').html(messagesHtml);
}
alert.show().addClass('show');
setTimeout(function() {
alert.removeClass('show');
setTimeout(function() {
alert.hide();
$('#updateMessages').html('');
}, 150);
}, 12000);
}
<?php if (!empty($message)): ?>
$(document).ready(function() {
showUpdateAlert();
});
<?php endif; ?>
</script>
<div class="container-sm container-bg callout border border-3 rounded-4 col-11">
<div class="row">
<a href="./index.php" class="col btn btn-lg">🏠 首页</a>
<a href="./mihomo_manager.php" class="col btn btn-lg">📂 Mihomo</a>
<a href="./singbox_manager.php" class="col btn btn-lg">🗂️ Sing-box</a>
<a href="./box.php" class="col btn btn-lg">💹 订阅转换</a>
<a href="./filekit.php" class="col btn btn-lg">📦 文件助手</a>
</div>
<div class="text-center">
<h1 style="margin-top: 40px; margin-bottom: 20px;">Mihomo 文件管理</h1>
<div class="card mb-4">
<div class="card-body">
<div class="container">
<h5>代理文件管理</h5>
<div class="table-responsive">
<table class="table table-striped table-bordered text-center">
<thead class="thead-dark">
<tr>
<th style="width: 30%;">文件名</th>
<th style="width: 10%;">大小</th>
<th style="width: 20%;">修改时间</th>
<th style="width: 40%;">执行操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($proxyFiles as $file): ?>
<?php $filePath = $uploadDir. $file; ?>
<tr>
<td class="align-middle"><a href="download.php?file=<?php echo urlencode($file); ?>"><?php echo htmlspecialchars($file); ?></a></td>
<td class="align-middle"><?php echo file_exists($filePath) ? formatSize(filesize($filePath)) : '文件不存在'; ?></td>
<td class="align-middle"><?php echo htmlspecialchars(date('Y-m-d H:i:s', filemtime($filePath) + 8 * 60 * 60)); ?></td>
<td>
<div class="d-flex justify-content-center">
<form action="" method="post" class="d-inline">
<input type="hidden" name="deleteFile" value="<?php echo htmlspecialchars($file); ?>">
<button type="submit" class="btn btn-danger btn-sm mx-1" onclick="return confirm('确定要删除这个文件吗?');"><i>🗑️</i> 删除</button>
</form>
<form action="" method="post" class="d-inline">
<input type="hidden" name="oldFileName" value="<?php echo htmlspecialchars($file); ?>">
<input type="hidden" name="fileType" value="proxy">
<button type="button" class="btn btn-success btn-sm mx-1 btn-rename" data-toggle="modal" data-target="#renameModal" data-filename="<?php echo htmlspecialchars($file); ?>" data-filetype="proxy"><i>✏️</i> 重命名</button>
</form>
<form action="" method="post" class="d-inline">
<button type="button" class="btn btn-warning btn-sm mx-1" onclick="openEditModal('<?php echo htmlspecialchars($file); ?>', 'proxy')"><i>📝</i> 编辑</button>
</form>
<form action="" method="post" enctype="multipart/form-data" class="d-inline upload-btn">
<input type="file" name="fileInput" class="form-control-file" required id="fileInput-<?php echo htmlspecialchars($file); ?>" style="display: none;" onchange="this.form.submit()">
<button type="button" class="btn btn-info btn-sm mx-1" onclick="document.getElementById('fileInput-<?php echo htmlspecialchars($file); ?>').click();"><i>📤</i> 上传</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="container">
<h5 class="text-center">配置文件管理</h5>
<div class="table-responsive">
<table class="table table-striped table-bordered text-center">
<thead class="thead-dark">
<tr>
<th style="width: 30%;">文件名</th>
<th style="width: 10%;">大小</th>
<th style="width: 20%;">修改时间</th>
<th style="width: 40%;">执行操作</th>
</tr>
</thead>
<tbody>
<?php foreach ($configFiles as $file): ?>
<?php $filePath = $configDir . $file; ?>
<tr>
<td class="align-middle"><a href="download.php?file=<?php echo urlencode($file); ?>"><?php echo htmlspecialchars($file); ?></a></td>
<td class="align-middle"><?php echo file_exists($filePath) ? formatSize(filesize($filePath)) : '文件不存在'; ?></td>
<td class="align-middle"><?php echo htmlspecialchars(date('Y-m-d H:i:s', filemtime($filePath) + 8 * 60 * 60)); ?></td>
<td>
<div class="d-flex justify-content-center">
<form action="" method="post" class="d-inline">
<input type="hidden" name="deleteConfigFile" value="<?php echo htmlspecialchars($file); ?>">
<button type="submit" class="btn btn-danger btn-sm mx-1" onclick="return confirm('确定要删除这个文件吗?');"><i>🗑️</i> 删除</button>
</form>
<form action="" method="post" class="d-inline">
<input type="hidden" name="oldFileName" value="<?php echo htmlspecialchars($file); ?>">
<input type="hidden" name="fileType" value="config">
<button type="button" class="btn btn-success btn-sm mx-1 btn-rename" data-toggle="modal" data-target="#renameModal" data-filename="<?php echo htmlspecialchars($file); ?>" data-filetype="config"><i>✏️</i> 重命名</button>
</form>
<form action="" method="post" class="d-inline">
<button type="button" class="btn btn-warning btn-sm mx-1" onclick="openEditModal('<?php echo htmlspecialchars($file); ?>', 'config')"><i>📝</i> 编辑</button>
</form>
<form action="" method="post" enctype="multipart/form-data" class="d-inline upload-btn">
<input type="file" name="configFileInput" class="form-control-file" required id="fileInput-<?php echo htmlspecialchars($file); ?>" style="display: none;" onchange="this.form.submit()">
<button type="button" class="btn btn-info btn-sm mx-1" onclick="document.getElementById('fileInput-<?php echo htmlspecialchars($file); ?>').click();"><i>📤</i> 上传</button>
</form>
</div>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
<div class="modal fade" id="renameModal" tabindex="-1" role="dialog" aria-labelledby="renameModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="renameModalLabel">重命名文件</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="renameForm" action="" method="post">
<input type="hidden" name="oldFileName" id="oldFileName">
<input type="hidden" name="fileType" id="fileType">
<div class="form-group">
<label for="newFileName">新文件名</label>
<input type="text" class="form-control" id="newFileName" name="newFileName" required>
</div>
<div class="form-group text-right">
<button type="button" class="btn btn-secondary" data-dismiss="modal">取消</button>
<button type="submit" class="btn btn-primary">确定</button>
</div>
</form>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.4.12/ace.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-beautify/1.14.0/beautify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js"></script>
<div class="modal fade" id="editModal" tabindex="-1" role="dialog" aria-labelledby="editModalLabel" aria-hidden="true" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog modal-xl" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">编辑文件: <span id="editingFileName"></span></h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="editForm" action="" method="post" onsubmit="syncEditorContent()">
<textarea name="saveContent" id="fileContent" class="form-control" style="height: 500px;"></textarea>
<input type="hidden" name="fileName" id="hiddenFileName">
<input type="hidden" name="fileType" id="hiddenFileType">
<div class="mt-3">
<button type="submit" class="btn btn-primary">保存</button>
<button type="button" class="btn btn-pink" onclick="openFullScreenEditor()">高级编辑</button>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal fade" id="fullScreenEditorModal" tabindex="-1" role="dialog" aria-hidden="true" data-backdrop="static" data-keyboard="false">
<div class="modal-dialog modal-fullscreen" role="document">
<div class="modal-content" style="border: none;">
<div class="modal-header d-flex justify-content-between align-items-center" style="border-bottom: none;">
<div class="d-flex align-items-center">
<h5 class="modal-title mr-3">高级编辑 - 全屏模式</h5>
<select id="fontSize" onchange="changeFontSize()" class="form-select mx-1" style="width: auto; font-size: 0.8rem;">
<option value="18px">18px</option>
<option value="20px" selected>20px</option>
<option value="22px">22px</option>
<option value="24px">24px</option>
<option value="26px">26px</option>
<option value="28px">28px</option>
<option value="30px">30px</option>
<option value="32px">32px</option>
<option value="34px">34px</option>
<option value="36px">36px</option>
<option value="38px">38px</option>
<option value="40px">40px</option>
</select>
<select id="editorTheme" onchange="changeEditorTheme()" class="form-select mx-1" style="width: auto; font-size: 0.9rem;">
<option value="ace/theme/vibrant_ink">Vibrant Ink</option>
<option value="ace/theme/monokai">Monokai</option>
<option value="ace/theme/github">GitHub</option>
<option value="ace/theme/tomorrow">Tomorrow</option>
<option value="ace/theme/twilight">Twilight</option>
<option value="ace/theme/solarized_dark">Solarized Dark</option>
<option value="ace/theme/solarized_light">Solarized Light</option>
<option value="ace/theme/textmate">TextMate</option>
<option value="ace/theme/terminal">Terminal</option>
<option value="ace/theme/chrome">Chrome</option>
<option value="ace/theme/eclipse">Eclipse</option>
<option value="ace/theme/dreamweaver">Dreamweaver</option>
<option value="ace/theme/xcode">Xcode</option>
<option value="ace/theme/kuroir">Kuroir</option>
<option value="ace/theme/katzenmilch">KatzenMilch</option>
<option value="ace/theme/sqlserver">SQL Server</option>
<option value="ace/theme/ambiance">Ambiance</option>
<option value="ace/theme/chaos">Chaos</option>
<option value="ace/theme/clouds_midnight">Clouds Midnight</option>
<option value="ace/theme/cobalt">Cobalt</option>
<option value="ace/theme/gruvbox">Gruvbox</option>
<option value="ace/theme/idle_fingers">Idle Fingers</option>
<option value="ace/theme/kr_theme">krTheme</option>
<option value="ace/theme/merbivore">Merbivore</option>
<option value="ace/theme/mono_industrial">Mono Industrial</option>
<option value="ace/theme/pastel_on_dark">Pastel on Dark</option>
</select>
<button type="button" class="btn btn-success btn-sm mx-1" onclick="formatContent()">格式化缩进</button>
<button type="button" class="btn btn-success btn-sm mx-1" id="yamlFormatBtn" onclick="formatYamlContent()" style="display: none;">格式化 YAML</button>
<button type="button" class="btn btn-info btn-sm mx-1" id="jsonValidationBtn" onclick="validateJsonSyntax()">验证 JSON 语法</button>
<button type="button" class="btn btn-info btn-sm mx-1" id="yamlValidationBtn" onclick="validateYamlSyntax()" style="display: none;">验证 YAML 语法</button>
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="saveFullScreenContent()">保存并关闭</button>
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="openSearch()">搜索</button>
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="closeFullScreenEditor()">取消</button>
<button type="button" class="btn btn-warning btn-sm mx-1" id="toggleFullscreenBtn" onclick="toggleFullscreen()">全屏</button>
</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close" onclick="closeFullScreenEditor()">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="d-flex justify-content-center align-items-center my-1" id="editorStatus" style="font-weight: bold; font-size: 0.9rem;">
<span id="lineColumnDisplay" style="color: blue; font-size: 1.1rem;">行: 1, 列: 1</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<span id="charCountDisplay" style="color: blue; font-size: 1.1rem;">字符数: 0</span>
</div>
<div class="modal-body" style="padding: 0; height: 100%;">
<div id="aceEditorContainer" style="height: 100%; width: 100%;"></div>
</div>
</div>
</div>
</div>
<script>
let isJsonDetected = false;
let aceEditorInstance;
function initializeAceEditor() {
aceEditorInstance = ace.edit("aceEditorContainer");
const savedTheme = localStorage.getItem("editorTheme") || "ace/theme/vibrant_ink";
aceEditorInstance.setTheme(savedTheme);
aceEditorInstance.session.setMode("ace/mode/javascript");
aceEditorInstance.setOptions({
fontSize: "20px",
wrap: true
});
document.getElementById("editorTheme").value = savedTheme;
aceEditorInstance.getSession().on('change', () => {
updateEditorStatus();
detectContentFormat();
});
aceEditorInstance.selection.on('changeCursor', updateEditorStatus);
detectContentFormat();
}
function openFullScreenEditor() {
aceEditorInstance.setValue(document.getElementById('fileContent').value, -1);
$('#fullScreenEditorModal').modal('show');
updateEditorStatus();
}
function saveFullScreenContent() {
document.getElementById('fileContent').value = aceEditorInstance.getValue();
$('#fullScreenEditorModal').modal('hide');
$('#editModal').modal('hide');
document.getElementById('editForm').submit();
}
function closeFullScreenEditor() {
$('#fullScreenEditorModal').modal('hide');
}
function changeFontSize() {
const fontSize = document.getElementById("fontSize").value;
aceEditorInstance.setFontSize(fontSize);
}
function changeEditorTheme() {
const theme = document.getElementById("editorTheme").value;
aceEditorInstance.setTheme(theme);
localStorage.setItem("editorTheme", theme);
}
function openSearch() {
aceEditorInstance.execCommand("find");
}
function isYamlFormat(content) {
const yamlPattern = /^(---|\w+:\s)/m;
return yamlPattern.test(content);
}
function validateJsonSyntax() {
const content = aceEditorInstance.getValue();
let annotations = [];
try {
JSON.parse(content);
alert("JSON 语法正确");
} catch (e) {
const line = e.lineNumber ? e.lineNumber - 1 : 0;
annotations.push({
row: line,
column: 0,
text: e.message,
type: "error"
});
aceEditorInstance.session.setAnnotations(annotations);
alert("JSON 语法错误: " + e.message);
}
}
function validateYamlSyntax() {
const content = aceEditorInstance.getValue();
let annotations = [];
try {
jsyaml.load(content);
alert("YAML 语法正确");
} catch (e) {
const line = e.mark ? e.mark.line : 0;
annotations.push({
row: line,
column: 0,
text: e.message,
type: "error"
});
aceEditorInstance.session.setAnnotations(annotations);
alert("YAML 语法错误: " + e.message);
}
}
function formatContent() {
const content = aceEditorInstance.getValue();
const mode = aceEditorInstance.session.$modeId;
let formattedContent;
try {
if (mode === "ace/mode/json") {
formattedContent = JSON.stringify(JSON.parse(content), null, 4);
aceEditorInstance.setValue(formattedContent, -1);
alert("JSON 格式化成功");
} else if (mode === "ace/mode/javascript") {
formattedContent = js_beautify(content, { indent_size: 4 });
aceEditorInstance.setValue(formattedContent, -1);
alert("JavaScript 格式化成功");
} else {
alert("当前模式不支持格式化缩进");
}
} catch (e) {
alert("格式化错误: " + e.message);
}
}
function formatYamlContent() {
const content = aceEditorInstance.getValue();
try {
const yamlObject = jsyaml.load(content);
const formattedYaml = jsyaml.dump(yamlObject, { indent: 4 });
aceEditorInstance.setValue(formattedYaml, -1);
alert("YAML 格式化成功");
} catch (e) {
alert("YAML 格式化错误: " + e.message);
}
}
function detectContentFormat() {
const content = aceEditorInstance.getValue().trim();
if (isJsonDetected) {
document.getElementById("jsonValidationBtn").style.display = "inline-block";
document.getElementById("yamlValidationBtn").style.display = "none";
document.getElementById("yamlFormatBtn").style.display = "none";
return;
}
try {
JSON.parse(content);
document.getElementById("jsonValidationBtn").style.display = "inline-block";
document.getElementById("yamlValidationBtn").style.display = "none";
document.getElementById("yamlFormatBtn").style.display = "none";
isJsonDetected = true;
} catch {
if (isYamlFormat(content)) {
document.getElementById("jsonValidationBtn").style.display = "none";
document.getElementById("yamlValidationBtn").style.display = "inline-block";
document.getElementById("yamlFormatBtn").style.display = "inline-block";
} else {
document.getElementById("jsonValidationBtn").style.display = "none";
document.getElementById("yamlValidationBtn").style.display = "none";
document.getElementById("yamlFormatBtn").style.display = "none";
}
}
}
function openEditModal(fileName, fileType) {
document.getElementById('editingFileName').textContent = fileName;
document.getElementById('hiddenFileName').value = fileName;
document.getElementById('hiddenFileType').value = fileType;
fetch(`?editFile=${encodeURIComponent(fileName)}&fileType=${fileType}`)
.then(response => response.text())
.then(data => {
document.getElementById('fileContent').value = data;
$('#editModal').modal('show');
})
.catch(error => console.error('获取文件内容失败:', error));
}
function syncEditorContent() {
document.getElementById('fileContent').value = document.getElementById('fileContent').value;
}
function updateEditorStatus() {
const cursor = aceEditorInstance.getCursorPosition();
const line = cursor.row + 1;
const column = cursor.column + 1;
const charCount = aceEditorInstance.getValue().length;
document.getElementById('lineColumnDisplay').textContent = `行: ${line}, 列: ${column}`;
document.getElementById('charCountDisplay').textContent = `字符数: ${charCount}`;
}
$(document).ready(function() {
initializeAceEditor();
});
document.addEventListener("DOMContentLoaded", function() {
const renameButtons = document.querySelectorAll(".btn-rename");
renameButtons.forEach(button => {
button.addEventListener("click", function() {
const oldFileName = this.getAttribute("data-filename");
const fileType = this.getAttribute("data-filetype");
document.getElementById("oldFileName").value = oldFileName;
document.getElementById("fileType").value = fileType;
document.getElementById("newFileName").value = oldFileName;
$('#renameModal').modal('show');
});
});
});
function toggleFullscreen() {
const modal = document.getElementById('fullScreenEditorModal');
if (!document.fullscreenElement) {
modal.requestFullscreen()
.then(() => {
document.getElementById('toggleFullscreenBtn').textContent = '退出全屏';
})
.catch((err) => console.error(`Error attempting to enable full-screen mode: ${err.message}`));
} else {
document.exitFullscreen()
.then(() => {
document.getElementById('toggleFullscreenBtn').textContent = '全屏';
})
.catch((err) => console.error(`Error attempting to exit full-screen mode: ${err.message}`));
}
}
</script>
<h2 class="text-success text-center mt-4 mb-4">订阅管理</h2>
<?php if (isset($message) && $message): ?>
<div class="alert alert-info">
<?php echo nl2br(htmlspecialchars($message)); ?>
</div>
<?php endif; ?>
<?php if (isset($subscriptions) && is_array($subscriptions)): ?>
<div class="row">
<?php for ($i = 0; $i < 6; $i++): ?>
<div class="col-md-4 mb-3">
<form method="post" class="card">
<div class="card-body">
<div class="form-group">
<h5 for="subscription_url_<?php echo $i; ?>" class="mb-2">订阅链接 <?php echo ($i + 1); ?></h5>
<input type="text" name="subscription_url" id="subscription_url_<?php echo $i; ?>" value="<?php echo htmlspecialchars($subscriptions[$i]['url'] ?? ''); ?>" required class="form-control">
</div>
<div class="form-group">
<label for="custom_file_name_<?php echo $i; ?>">自定义文件名</label>
<input type="text" name="custom_file_name" id="custom_file_name_<?php echo $i; ?>" value="subscription_<?php echo ($i + 1); ?>.yaml" class="form-control">
</div>
<input type="hidden" name="index" value="<?php echo $i; ?>">
<div class="text-center mt-3">
<button type="submit" name="update" class="btn btn-info">🔄 更新订阅 <?php echo ($i + 1); ?></button>
</div>
</div>
</form>
</div>
<?php if (($i + 1) % 3 == 0 && $i < 5): ?>
</div><div class="row">
<?php endif; ?>
<?php endfor; ?>
</div>
<?php else: ?>
<p>未找到订阅信息。</p>
<?php endif; ?>
</div>
</section>
<div class="container text-center">
<section id="subscription-management" class="section-gap">
<div class="btn-group mt-2 mb-4">
<button id="pasteButton" class="btn btn-primary">生成订阅链接网站</button>
<button id="base64Button" class="btn btn-primary">Base64 在线编码解码</button>
</div>
<section id="base64-conversion" class="section-gap">
<h2 class="text-success">Base64 节点信息转换</h2>
<form method="post">
<div class="form-group">
<textarea name="base64_content" id="base64_content" rows="4" class="form-control" placeholder="粘贴 Base64 内容..." required></textarea>
</div>
<button type="submit" name="convert_base64" class="btn btn-primary btn-custom mt-3"><i>🔄</i> 生成节点信息</button>
</form>
</section>
<section id="node-conversion" class="section-gap">
<h1 class="text-success">节点转换工具</h1>
<form method="post">
<div class="form-group">
<textarea name="input" rows="10" class="form-control" placeholder="粘贴 ss//vless//vmess//trojan//hysteria2 节点信息..." required></textarea>
</div>
<button type="submit" name="convert" class="btn btn-primary mt-3"><i>🔄</i> 转换</button>
</form>
</section>
<script>
document.getElementById('pasteButton').onclick = function() {
window.open('https://paste.gg', '_blank');
}
document.getElementById('base64Button').onclick = function() {
window.open('https://base64.us', '_blank');
}
</script>
<style>
.btn-group {
display: flex;
gap: 10px;
justify-content: center;
}
.btn {
margin: 0;
}
.table-dark {
background-color: #6f42c1;
color: white;
}
.table-dark th, .table-dark td {
background-color: #5a32a3;
}
</style>