1270 lines
50 KiB
PHP
1270 lines
50 KiB
PHP
<?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">×</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">×</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">×</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> <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>
|