1395 lines
60 KiB
PHP
1395 lines
60 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 '<div class="alert alert-success" role="alert">文件上传成功:' . htmlspecialchars(basename($file['name'])) . '</div>';
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">文件上传失败!</div>';
|
||
}
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">上传错误:' . $file['error'] . '</div>';
|
||
}
|
||
}
|
||
|
||
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 '<div class="alert alert-success" role="alert">配置文件上传成功:' . htmlspecialchars(basename($file['name'])) . '</div>';
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">配置文件上传失败!</div>';
|
||
}
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">上传错误:' . $file['error'] . '</div>';
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['deleteFile'])) {
|
||
$fileToDelete = $uploadDir . basename($_POST['deleteFile']);
|
||
if (file_exists($fileToDelete) && unlink($fileToDelete)) {
|
||
echo '<div class="alert alert-success" role="alert">
|
||
文件删除成功:' . htmlspecialchars(basename($_POST['deleteFile'])) . '</div>';
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">文件删除失败!</div>';
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['deleteConfigFile'])) {
|
||
$fileToDelete = $configDir . basename($_POST['deleteConfigFile']);
|
||
if (file_exists($fileToDelete) && unlink($fileToDelete)) {
|
||
echo '<div class="alert alert-success" role="alert">
|
||
配置文件删除成功:' . htmlspecialchars(basename($_POST['deleteConfigFile'])) . '</div>';
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">配置文件删除失败!</div>';
|
||
}
|
||
}
|
||
|
||
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 '<div class="alert alert-success" role="alert">
|
||
文件重命名成功:' . htmlspecialchars($oldFileName) . ' -> ' . htmlspecialchars($newFileName) . ' </div>';
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">文件重命名失败!</div>';
|
||
}
|
||
} else {
|
||
echo '<div class="alert alert-danger" role="alert">文件重命名失败,文件不存在或新文件名已存在。</div>';
|
||
}
|
||
}
|
||
|
||
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 '<div class="alert alert-info" role="alert">文件内容已更新:' . htmlspecialchars(basename($fileToSave)) . '</div>';
|
||
}
|
||
}
|
||
|
||
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('.', '..'));
|
||
$proxyFiles = array_filter($proxyFiles, function($file) {
|
||
return pathinfo($file, PATHINFO_EXTENSION) !== 'txt';
|
||
});
|
||
} 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;
|
||
}
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['downloadFile'], $_GET['fileType'])) {
|
||
$fileType = $_GET['fileType'];
|
||
$fileName = basename($_GET['downloadFile']);
|
||
$filePath = ($fileType === 'proxy') ? $uploadDir . $fileName : $configDir . $fileName;
|
||
|
||
if (file_exists($filePath)) {
|
||
header('Content-Description: File Transfer');
|
||
header('Content-Type: application/octet-stream');
|
||
header('Content-Disposition: attachment; filename="' . $fileName . '"');
|
||
header('Expires: 0');
|
||
header('Cache-Control: must-revalidate');
|
||
header('Pragma: public');
|
||
header('Content-Length: ' . filesize($filePath));
|
||
readfile($filePath);
|
||
exit;
|
||
} else {
|
||
echo '文件不存在。';
|
||
}
|
||
}
|
||
?>
|
||
|
||
<?php
|
||
|
||
|
||
$subscriptionPath = '/etc/neko/proxy_provider/';
|
||
$subscriptionFile = $subscriptionPath . 'subscriptions.json';
|
||
$notificationMessage = "";
|
||
$subscriptions = [];
|
||
$updateCompleted = false;
|
||
|
||
function storeUpdateLog($message) {
|
||
if (!isset($_SESSION['update_logs'])) {
|
||
$_SESSION['update_logs'] = [];
|
||
}
|
||
$_SESSION['update_logs'][] = $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 + 1) . ".yaml",
|
||
];
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['update'])) {
|
||
$index = intval($_POST['index']);
|
||
$url = trim($_POST['subscription_url'] ?? '');
|
||
$customFileName = trim($_POST['custom_file_name'] ?? "subscription_" . ($index + 1) . ".yaml");
|
||
|
||
$subscriptions[$index]['url'] = $url;
|
||
$subscriptions[$index]['file_name'] = $customFileName;
|
||
|
||
if (!empty($url)) {
|
||
$tempPath = $subscriptionPath . $customFileName . ".temp";
|
||
$finalPath = $subscriptionPath . $customFileName;
|
||
|
||
$command = "curl -s -L -o {$tempPath} {$url}";
|
||
exec($command . ' 2>&1', $output, $return_var);
|
||
|
||
if ($return_var !== 0) {
|
||
$command = "wget -q --show-progress -O {$tempPath} {$url}";
|
||
exec($command . ' 2>&1', $output, $return_var);
|
||
}
|
||
|
||
if ($return_var === 0) {
|
||
$_SESSION['update_logs'] = [];
|
||
storeUpdateLog("✅ 订阅 " . htmlspecialchars($url) . " 已下载并保存到临时文件: " . htmlspecialchars($tempPath));
|
||
|
||
$fileContent = file_get_contents($tempPath);
|
||
|
||
if (base64_encode(base64_decode($fileContent, true)) === $fileContent) {
|
||
$decodedContent = base64_decode($fileContent);
|
||
if ($decodedContent !== false && strlen($decodedContent) > 0) {
|
||
file_put_contents($finalPath, "# Clash Meta Config\n\n" . $decodedContent);
|
||
storeUpdateLog("📂 Base64 解码成功,配置已保存到: " . htmlspecialchars($finalPath));
|
||
unlink($tempPath);
|
||
$notificationMessage = '更新成功';
|
||
$updateCompleted = true;
|
||
} else {
|
||
storeUpdateLog("⚠️ Base64 解码失败,请检查订阅链接内容!");
|
||
unlink($tempPath);
|
||
$notificationMessage = '更新失败';
|
||
}
|
||
}
|
||
elseif (substr($fileContent, 0, 2) === "\x1f\x8b") {
|
||
$decompressedContent = gzdecode($fileContent);
|
||
if ($decompressedContent !== false) {
|
||
file_put_contents($finalPath, "# Clash Meta Config\n\n" . $decompressedContent);
|
||
storeUpdateLog("📂 Gzip 解压成功,配置已保存到: " . htmlspecialchars($finalPath));
|
||
unlink($tempPath);
|
||
$notificationMessage = '更新成功';
|
||
$updateCompleted = true;
|
||
} else {
|
||
storeUpdateLog("⚠️ Gzip 解压失败,请检查订阅链接格式!");
|
||
unlink($tempPath);
|
||
$notificationMessage = '更新失败';
|
||
}
|
||
}
|
||
else {
|
||
rename($tempPath, $finalPath);
|
||
storeUpdateLog("✅ 订阅内容已成功下载,无需解码");
|
||
$notificationMessage = '更新成功';
|
||
$updateCompleted = true;
|
||
}
|
||
} else {
|
||
storeUpdateLog("❌ 订阅更新失败!错误信息: " . implode("\n", $output));
|
||
unlink($tempPath);
|
||
$notificationMessage = '更新失败';
|
||
}
|
||
} else {
|
||
storeUpdateLog("⚠️ 第" . ($index + 1) . "个订阅链接为空!");
|
||
$notificationMessage = '更新失败';
|
||
}
|
||
|
||
file_put_contents($subscriptionFile, json_encode($subscriptions));
|
||
}
|
||
|
||
?>
|
||
|
||
<?php
|
||
$shellScriptPath = '/etc/neko/core/update_mihomo.sh';
|
||
$LOG_FILE = '/etc/neko/tmp/log.txt';
|
||
$JSON_FILE = '/etc/neko/proxy_provider/subscriptions.json';
|
||
$SAVE_DIR = '/etc/neko/proxy_provider';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||
if (isset($_POST['createShellScript'])) {
|
||
$shellScriptContent = <<<EOL
|
||
#!/bin/bash
|
||
|
||
LOG_FILE="/etc/neko/tmp/log.txt"
|
||
JSON_FILE="/etc/neko/proxy_provider/subscriptions.json"
|
||
SAVE_DIR="/etc/neko/proxy_provider"
|
||
|
||
log() {
|
||
echo "$(date '+[ %H:%M:%S ]') \$1" >> "\$LOG_FILE"
|
||
}
|
||
|
||
log "开始处理订阅更新任务..."
|
||
|
||
if [ ! -f "\$JSON_FILE" ]; then
|
||
log "❌ 错误: JSON 文件不存在: \$JSON_FILE"
|
||
exit 1
|
||
fi
|
||
|
||
jq -c '.[]' "\$JSON_FILE" | while read -r ITEM; do
|
||
URL=\$(echo "\$ITEM" | jq -r '.url')
|
||
FILE_NAME=\$(echo "\$ITEM" | jq -r '.file_name')
|
||
|
||
if [ -z "\$URL" ] || [ "\$URL" == "null" ]; then
|
||
log "⚠️ 跳过空的订阅链接,文件名: \$FILE_NAME"
|
||
continue
|
||
fi
|
||
|
||
if [ -z "\$FILE_NAME" ] || [ "\$FILE_NAME" == "null" ]; then
|
||
log "❌ 错误: 文件名为空,跳过此链接: \$URL"
|
||
continue
|
||
fi
|
||
|
||
SAVE_PATH="\$SAVE_DIR/\$FILE_NAME"
|
||
TEMP_PATH="\$SAVE_PATH.temp"
|
||
|
||
log "🔄 正在下载: \$URL 到临时文件: \$TEMP_PATH"
|
||
|
||
curl -s -L -o "\$TEMP_PATH" "\$URL"
|
||
|
||
if [ \$? -ne 0 ]; then
|
||
wget -q -O "\$TEMP_PATH" "\$URL"
|
||
fi
|
||
|
||
if [ \$? -eq 0 ]; then
|
||
log "✅ 文件下载成功: \$TEMP_PATH"
|
||
|
||
if base64 -d "\$TEMP_PATH" > /dev/null 2>&1; then
|
||
base64 -d "\$TEMP_PATH" > "\$SAVE_PATH"
|
||
if [ \$? -eq 0 ]; then
|
||
log "📂 Base64 解码成功,配置已保存: \$SAVE_PATH"
|
||
rm -f "\$TEMP_PATH"
|
||
else
|
||
log "⚠️ Base64 解码失败: \$SAVE_PATH"
|
||
rm -f "\$TEMP_PATH"
|
||
fi
|
||
elif file "\$TEMP_PATH" | grep -q "gzip compressed"; then
|
||
gunzip -c "\$TEMP_PATH" > "\$SAVE_PATH"
|
||
if [ \$? -eq 0 ]; then
|
||
log "📂 Gzip 解压成功,配置已保存: \$SAVE_PATH"
|
||
rm -f "\$TEMP_PATH"
|
||
else
|
||
log "⚠️ Gzip 解压失败: \$SAVE_PATH"
|
||
rm -f "\$TEMP_PATH"
|
||
fi
|
||
else
|
||
mv "\$TEMP_PATH" "\$SAVE_PATH"
|
||
log "✅ 订阅内容已成功下载,无需解码"
|
||
fi
|
||
else
|
||
log "❌ 订阅更新失败: \$URL"
|
||
rm -f "\$TEMP_PATH"
|
||
fi
|
||
done
|
||
|
||
log "🚀 所有订阅链接更新完成!"
|
||
EOL;
|
||
|
||
if (file_put_contents($shellScriptPath, $shellScriptContent) !== false) {
|
||
chmod($shellScriptPath, 0755);
|
||
echo "<div class='alert alert-success'>Shell 脚本已创建成功!路径: $shellScriptPath</div>";
|
||
} else {
|
||
echo "<div class='alert alert-danger'>无法创建 Shell 脚本,请检查权限。</div>";
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
|
||
<?php
|
||
$CRON_LOG_FILE = '/etc/neko/tmp/log.txt';
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||
if (isset($_POST['createCronJob'])) {
|
||
$cronExpression = trim($_POST['cronExpression']);
|
||
|
||
if (empty($cronExpression)) {
|
||
file_put_contents($CRON_LOG_FILE, date('[ H:i:s ] ') . "错误: Cron 表达式不能为空。\n", FILE_APPEND);
|
||
echo "<div class='alert alert-warning'>Cron 表达式不能为空。</div>";
|
||
exit;
|
||
}
|
||
|
||
$cronJob = "$cronExpression /etc/neko/core/update_mihomo.sh";
|
||
|
||
exec("crontab -l | grep -v '/etc/neko/core/update_mihomo.sh' | crontab -", $output, $returnVarRemove);
|
||
if ($returnVarRemove === 0) {
|
||
file_put_contents($CRON_LOG_FILE, date('[ H:i:s ] ') . "成功移除旧的 Cron 任务。\n", FILE_APPEND);
|
||
} else {
|
||
file_put_contents($CRON_LOG_FILE, date('[ H:i:s ] ') . "移除旧的 Cron 任务失败。\n", FILE_APPEND);
|
||
}
|
||
|
||
exec("(crontab -l; echo '$cronJob') | crontab -", $output, $returnVarAdd);
|
||
if ($returnVarAdd === 0) {
|
||
file_put_contents($CRON_LOG_FILE, date('[ H:i:s ] ') . "成功添加新的 Cron 任务: $cronJob\n", FILE_APPEND);
|
||
echo "<div class='alert alert-success'>Cron 任务已成功添加或更新!</div>";
|
||
} else {
|
||
file_put_contents($CRON_LOG_FILE, date('[ H:i:s ] ') . "添加新的 Cron 任务失败。\n", FILE_APPEND);
|
||
echo "<div class='alert alert-danger'>无法添加或更新 Cron 任务,请检查服务器权限。</div>";
|
||
}
|
||
}
|
||
}
|
||
?>
|
||
|
||
<?php
|
||
$file_urls = [
|
||
'geoip' => 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb',
|
||
'geosite' => 'https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geosite.dat',
|
||
'cache' => 'https://github.com/Thaolga/neko/raw/main/cache.db'
|
||
];
|
||
|
||
$download_directories = [
|
||
'geoip' => '/etc/neko/',
|
||
'geosite' => '/etc/neko/',
|
||
'cache' => '/www/nekobox/'
|
||
];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['file'])) {
|
||
$file = $_GET['file'];
|
||
|
||
if (isset($file_urls[$file])) {
|
||
$file_url = $file_urls[$file];
|
||
$destination_directory = $download_directories[$file];
|
||
$destination_path = $destination_directory . basename($file_url);
|
||
|
||
if (download_file($file_url, $destination_path)) {
|
||
echo "<div class='alert alert-success'>文件成功下载到 $destination_path</div>";
|
||
} else {
|
||
echo "<div class='alert alert-danger'>文件下载失败</div>";
|
||
}
|
||
} else {
|
||
echo "无效的文件请求";
|
||
}
|
||
}
|
||
|
||
function download_file($url, $destination) {
|
||
$ch = curl_init($url);
|
||
$fp = fopen($destination, 'wb');
|
||
|
||
curl_setopt($ch, CURLOPT_FILE, $fp);
|
||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||
|
||
$result = curl_exec($ch);
|
||
curl_close($ch);
|
||
fclose($fp);
|
||
|
||
return $result !== false;
|
||
}
|
||
?>
|
||
<!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 - NekoBox</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/bootstrap/bootstrap-icons.css" rel="stylesheet">
|
||
<link href="./assets/theme/<?php echo $neko_theme ?>" rel="stylesheet">
|
||
<script type="text/javascript" src="./assets/js/bootstrap.min.js"></script>
|
||
<script type="text/javascript" src="./assets/js/feather.min.js"></script>
|
||
<script type="text/javascript" src="./assets/bootstrap/bootstrap.bundle.min.js"></script>
|
||
<script type="text/javascript" src="./assets/js/jquery-2.1.3.min.js"></script>
|
||
<script type="text/javascript" src="./assets/js/neko.js"></script>
|
||
<?php include './ping.php'; ?>
|
||
</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 flex-column align-items-center" style="top: 20px; z-index: 1050;">
|
||
<div id="updateNotification" class="alert alert-info alert-dismissible fade show shadow-lg" role="alert" style="display: none; min-width: 320px; max-width: 600px; opacity: 0.95;">
|
||
<div class="d-flex align-items-center">
|
||
<div class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></div>
|
||
<strong data-translate="update_notification">🔔 更新通知</strong>
|
||
</div>
|
||
|
||
<div class="alert alert-info mt-2 p-2 small">
|
||
<strong data-translate="usage_instruction">⚠️ 使用说明</strong>
|
||
<ul class="mb-0 pl-3">
|
||
<li data-translate="max_subscriptions">通用模板(mihomo.yaml)最多支持<strong>6个</strong>订阅链接</li>
|
||
<li data-translate="no_rename">请勿更改默认文件名称</li>
|
||
<li data-translate="supports_all_formats">该模板支持所有格式订阅链接,无需额外转换</li>
|
||
</ul>
|
||
</div>
|
||
|
||
<div id="updateLogContainer" class="small mt-2"></div>
|
||
|
||
<button type="button" class="btn-close custom-btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<style>
|
||
.alert-success {
|
||
background-color: #4CAF50 !important;
|
||
border: 1px solid rgba(255, 255, 255, 0.3) !important;
|
||
border-radius: 8px !important;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1) !important;
|
||
padding: 16px 20px !important;
|
||
position: relative;
|
||
color: #fff !important;
|
||
backdrop-filter: blur(8px);
|
||
margin-top: 15px !important;
|
||
}
|
||
|
||
.alert .close,
|
||
.alert .btn-close {
|
||
position: absolute !important;
|
||
right: 10px !important;
|
||
top: 10px !important;
|
||
background-color: #dc3545 !important;
|
||
opacity: 1 !important;
|
||
width: 24px !important;
|
||
height: 24px !important;
|
||
border-radius: 50% !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
font-size: 16px !important;
|
||
color: #fff !important;
|
||
border: none !important;
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
transition: all 0.2s ease !important;
|
||
cursor: pointer !important;
|
||
}
|
||
|
||
.alert .close:hover,
|
||
.alert .btn-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);
|
||
}
|
||
|
||
html {
|
||
font-size: 16px;
|
||
}
|
||
|
||
.container-fluid {
|
||
max-width: 2400px;
|
||
width: 100%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
#dropZone i {
|
||
font-size: 50px;
|
||
color: #007bff;
|
||
animation: iconPulse 1.5s infinite;
|
||
}
|
||
|
||
.popup {
|
||
display: none;
|
||
}
|
||
|
||
@keyframes iconPulse {
|
||
0% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.2);
|
||
opacity: 0.7;
|
||
}
|
||
100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.table thead {
|
||
display: none;
|
||
}
|
||
|
||
.table tbody,
|
||
.table tr,
|
||
.table td {
|
||
display: block;
|
||
width: 100%;
|
||
}
|
||
|
||
.table td::before {
|
||
content: attr(data-label);
|
||
font-weight: bold;
|
||
display: block;
|
||
text-transform: uppercase;
|
||
color: #23407E;
|
||
}
|
||
|
||
.table tr {
|
||
margin-bottom: 10px;
|
||
border: 1px solid #ddd;
|
||
padding: 10px;
|
||
border-radius: 5px;
|
||
background-color: #f9f9f9;
|
||
}
|
||
}
|
||
|
||
</style>
|
||
|
||
<script>
|
||
function displayUpdateNotification() {
|
||
const notification = $('#updateNotification');
|
||
const updateLogs = <?php echo json_encode($_SESSION['update_logs'] ?? []); ?>;
|
||
|
||
if (updateLogs.length > 0) {
|
||
const logsHtml = updateLogs.map(log => `<div>${log}</div>`).join('');
|
||
$('#updateLogContainer').html(logsHtml);
|
||
}
|
||
|
||
notification.fadeIn().addClass('show');
|
||
|
||
setTimeout(function() {
|
||
notification.fadeOut(300, function() {
|
||
notification.hide();
|
||
$('#updateLogContainer').html('');
|
||
});
|
||
}, 10000);
|
||
}
|
||
|
||
<?php if (!empty($notificationMessage)): ?>
|
||
$(document).ready(function() {
|
||
displayUpdateNotification();
|
||
});
|
||
<?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 text-nowrap"><i class="bi bi-house-door"></i> <span data-translate="home">Home</span></a>
|
||
<a href="./mihomo_manager.php" class="col btn btn-lg text-nowrap"><i class="bi bi-folder"></i> <span data-translate="manager">Manager</span></a>
|
||
<a href="./singbox.php" class="col btn btn-lg text-nowrap"><i class="bi bi-shop"></i> <span data-translate="template_i">Template I</span></a>
|
||
<a href="./subscription.php" class="col btn btn-lg text-nowrap"><i class="bi bi-bank"></i> <span data-translate="template_ii">Template II</span></a>
|
||
<a href="./mihomo.php" class="col btn btn-lg text-nowrap"><i class="bi bi-building"></i> <span data-translate="template_iii">Template III</span></a>
|
||
</div>
|
||
<div class="text-center">
|
||
<h2 style="margin-top: 40px; margin-bottom: 20px;" data-translate="fileManagement"></h2>
|
||
<div class="container-fluid">
|
||
<div class="table-responsive">
|
||
<table class="table table-striped table-bordered text-center">
|
||
<thead class="thead-dark">
|
||
<tr>
|
||
<th style="width: 20%;" data-translate="fileName"></th>
|
||
<th style="width: 10%;" data-translate="fileSize"></th>
|
||
<th style="width: 20%;" data-translate="lastModified"></th>
|
||
<th style="width: 10%;" data-translate="fileType"></th>
|
||
<th style="width: 30%;" data-translate="actions"></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php
|
||
$allFiles = array_merge($proxyFiles, $configFiles);
|
||
$allFilePaths = array_merge(array_map(function($file) use ($uploadDir) {
|
||
return $uploadDir . $file;
|
||
}, $proxyFiles), array_map(function($file) use ($configDir) {
|
||
return $configDir . $file;
|
||
}, $configFiles));
|
||
$fileTypes = array_merge(array_fill(0, count($proxyFiles), 'Proxy File'), array_fill(0, count($configFiles), 'Config File'));
|
||
|
||
foreach ($allFiles as $index => $file) {
|
||
$filePath = $allFilePaths[$index];
|
||
$fileType = $fileTypes[$index];
|
||
?>
|
||
<tr>
|
||
<td class="align-middle" data-label="文件名">
|
||
<?php echo htmlspecialchars($file); ?>
|
||
</td>
|
||
<td class="align-middle" data-label="大小">
|
||
<?php echo file_exists($filePath) ? formatSize(filesize($filePath)) : '文件不存在'; ?>
|
||
</td>
|
||
<td class="align-middle" data-label="最后修改时间">
|
||
<?php echo htmlspecialchars(date('Y-m-d H:i:s', filemtime($filePath))); ?>
|
||
</td>
|
||
<td class="align-middle" data-label="文件类型">
|
||
<?php echo htmlspecialchars($fileType); ?>
|
||
</td>
|
||
<td class="align-middle">
|
||
<div class="action-buttons">
|
||
<?php if ($fileType == 'Proxy File'): ?>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<input type="hidden" name="deleteFile" value="<?php echo htmlspecialchars($file); ?>">
|
||
<button type="submit" class="btn btn-danger btn-sm" title="🗑️ 删除" onclick="return confirm('确定要删除这个文件吗?');" data-translate-title="delete"><i class="bi bi-trash"></i></button>
|
||
</form>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<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 btn-rename" title="✏️ 重命名" data-toggle="modal" data-target="#renameModal" data-filename="<?php echo htmlspecialchars($file); ?>" data-filetype="proxy" data-translate-title="rename"><i class="bi bi-pencil"></i></button>
|
||
</form>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<button type="button" class="btn btn-warning btn-sm" title="📝 编辑" onclick="openEditModal('<?php echo htmlspecialchars($file); ?>', 'proxy')" data-translate-title="edit"><i class="bi bi-pen"></i></button>
|
||
</form>
|
||
<form action="" method="post" enctype="multipart/form-data" class="d-inline upload-btn mb-1">
|
||
<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" title="📤 上传" onclick="openUploadModal('proxy')" data-translate-title="upload"><i class="bi bi-upload"></i></button>
|
||
</form>
|
||
<form action="" method="get" class="d-inline mb-1">
|
||
<input type="hidden" name="downloadFile" value="<?php echo htmlspecialchars($file); ?>">
|
||
<input type="hidden" name="fileType" value="proxy">
|
||
<button type="submit" class="btn btn-primary btn-sm" title="📥 下载" data-translate-title="download"><i class="bi bi-download"></i></button>
|
||
</form>
|
||
<?php else: ?>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<input type="hidden" name="deleteConfigFile" value="<?php echo htmlspecialchars($file); ?>">
|
||
<button type="submit" class="btn btn-danger btn-sm" title="🗑️ 删除" onclick="return confirm('确定要删除这个文件吗?');" data-translate-title="delete"><i class="bi bi-trash"></i></button>
|
||
</form>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<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 btn-rename" title="✏️ 重命名" data-toggle="modal" data-target="#renameModal" data-filename="<?php echo htmlspecialchars($file); ?>" data-filetype="config" data-translate-title="rename"><i class="bi bi-pencil"></i></button>
|
||
</form>
|
||
<form action="" method="post" class="d-inline mb-1">
|
||
<button type="button" class="btn btn-warning btn-sm" title="📝 编辑" onclick="openEditModal('<?php echo htmlspecialchars($file); ?>', 'config')" data-translate-title="edit"><i class="bi bi-pen"></i></button>
|
||
</form>
|
||
<form action="" method="post" enctype="multipart/form-data" class="d-inline upload-btn mb-1">
|
||
<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" title="📤 上传" onclick="openUploadModal('config')" data-translate-title="upload"><i class="bi bi-upload"></i></button>
|
||
</form>
|
||
<form action="" method="get" class="d-inline mb-1">
|
||
<input type="hidden" name="downloadFile" value="<?php echo htmlspecialchars($file); ?>">
|
||
<input type="hidden" name="fileType" value="config">
|
||
<button type="submit" class="btn btn-primary btn-sm" title="📥 下载" data-translate-title="download"><i class="bi bi-download"></i></button>
|
||
</form>
|
||
<?php endif; ?>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
<?php } ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="uploadModal" tabindex="-1" aria-labelledby="uploadModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="uploadModalLabel" data-translate="uploadFile"></h5>
|
||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="dropZone" class="border border-primary rounded text-center py-4 position-relative">
|
||
<i class="fas fa-cloud-upload-alt"></i>
|
||
<p class="mb-0 mt-3" data-translate="dragOrClickToUpload"></p>
|
||
</div>
|
||
<input type="file" id="fileInputModal" class="form-control mt-3" hidden>
|
||
<button id="selectFileBtn" class="btn btn-primary btn-block mt-3 w-100" data-translate="selectFile"></button>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="close"></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="renameModal" tabindex="-1" aria-labelledby="renameModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="renameModalLabel">重命名文件</h5>
|
||
<button type="button" class="close" data-bs-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="mb-3">
|
||
<label for="newFileName" class="form-label">新文件名</label>
|
||
<input type="text" class="form-control" id="newFileName" name="newFileName" required>
|
||
</div>
|
||
|
||
<div class="d-flex justify-content-end gap-2">
|
||
<button type="button" class="btn btn-secondary" data-bs-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" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-xl" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="editModalLabel"><?php echo $langData[$currentLang]['editFile']; ?>: <span id="editingFileName"></span></h5>
|
||
|
||
<button type="button" class="close" data-bs-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 d-flex justify-content-start gap-2">
|
||
<button type="submit" class="btn btn-primary" data-translate="save">保存</button>
|
||
<button type="button" class="btn btn-pink" onclick="openFullScreenEditor()" data-translate="advancedEdit">高级编辑</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" data-translate="advancedEditorTitle"></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/gob">Gob</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/iplastic">Iplastic</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()" data-translate="formatIndentation">格式化缩进</button>
|
||
<button type="button" class="btn btn-success btn-sm mx-1" id="yamlFormatBtn" onclick="formatYamlContent()" style="display: none;" data-translate="formatYaml">格式化 YAML</button>
|
||
<button type="button" class="btn btn-info btn-sm mx-1" id="jsonValidationBtn" onclick="validateJsonSyntax()" data-translate="validateJson">验证 JSON 语法</button>
|
||
<button type="button" class="btn btn-info btn-sm mx-1" id="yamlValidationBtn" onclick="validateYamlSyntax()" style="display: none;" data-translate="validateYaml">验证 YAML 语法</button>
|
||
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="saveFullScreenContent()" data-translate="saveAndClose">保存并关闭</button>
|
||
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="openSearch()" data-translate="search">搜索</button>
|
||
<button type="button" class="btn btn-primary btn-sm mx-1" onclick="closeFullScreenEditor()" data-translate="cancel">取消</button>
|
||
<button type="button" class="btn btn-warning btn-sm mx-1" id="toggleFullscreenBtn" onclick="toggleFullscreen()" data-translate="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;" data-translate="lineColumnDisplay">行: 1, 列: 1</span> <span id="charCountDisplay" style="color: blue; font-size: 1.1rem;" data-translate="charCountDisplay">字符数: 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(langData[currentLang]['validateJson'] + " " + langData[currentLang]['jsonSyntaxCorrect']);
|
||
} 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(langData[currentLang]['validateJson'] + " " + langData[currentLang]['jsonSyntaxError'] + ": " + e.message);
|
||
}
|
||
}
|
||
|
||
function validateYamlSyntax() {
|
||
const content = aceEditorInstance.getValue();
|
||
let annotations = [];
|
||
try {
|
||
jsyaml.load(content);
|
||
alert(langData[currentLang]['validateYaml'] + " " + langData[currentLang]['yamlSyntaxCorrect']);
|
||
} 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(langData[currentLang]['validateYaml'] + " " + langData[currentLang]['yamlSyntaxError'] + ": " + 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(langData[currentLang]['formatIndentation'] + " " + langData[currentLang]['jsonFormatSuccess']);
|
||
} else if (mode === "ace/mode/javascript") {
|
||
formattedContent = js_beautify(content, { indent_size: 4 });
|
||
aceEditorInstance.setValue(formattedContent, -1);
|
||
alert(langData[currentLang]['formatIndentation'] + " " + langData[currentLang]['jsFormatSuccess']);
|
||
} else {
|
||
alert(langData[currentLang]['formatIndentation'] + " " + langData[currentLang]['unsupportedMode']);
|
||
}
|
||
} catch (e) {
|
||
alert(langData[currentLang]['formatIndentation'] + " " + langData[currentLang]['formatError'] + ": " + 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(langData[currentLang]['yamlFormatSuccess']);
|
||
} catch (e) {
|
||
alert(langData[currentLang]['yamlSyntaxError'] + ": " + 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;
|
||
|
||
const lineColumnText = langData[currentLang]['lineColumnDisplay'].replace("{line}", line).replace("{column}", column);
|
||
const charCountText = langData[currentLang]['charCountDisplay'].replace("{charCount}", charCount);
|
||
|
||
document.getElementById('lineColumnDisplay').textContent = lineColumnText;
|
||
document.getElementById('charCountDisplay').textContent = charCountText;
|
||
}
|
||
|
||
$(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}`));
|
||
}
|
||
}
|
||
|
||
let fileType = '';
|
||
function openUploadModal(type) {
|
||
fileType = type;
|
||
const modal = new bootstrap.Modal(document.getElementById('uploadModal'));
|
||
modal.show();
|
||
}
|
||
|
||
const dropZone = document.getElementById('dropZone');
|
||
dropZone.addEventListener('dragover', (event) => {
|
||
event.preventDefault();
|
||
dropZone.classList.add('bg-light');
|
||
});
|
||
|
||
dropZone.addEventListener('dragleave', () => {
|
||
dropZone.classList.remove('bg-light');
|
||
});
|
||
|
||
dropZone.addEventListener('drop', (event) => {
|
||
event.preventDefault();
|
||
dropZone.classList.remove('bg-light');
|
||
const files = event.dataTransfer.files;
|
||
if (files.length > 0) {
|
||
handleFileUpload(files[0]);
|
||
}
|
||
});
|
||
|
||
document.getElementById('selectFileBtn').addEventListener('click', () => {
|
||
document.getElementById('fileInputModal').click();
|
||
});
|
||
|
||
document.getElementById('fileInputModal').addEventListener('change', (event) => {
|
||
const files = event.target.files;
|
||
if (files.length > 0) {
|
||
handleFileUpload(files[0]);
|
||
}
|
||
});
|
||
|
||
function handleFileUpload(file) {
|
||
const formData = new FormData();
|
||
formData.append(fileType === 'proxy' ? 'fileInput' : 'configFileInput', file);
|
||
|
||
fetch('', {
|
||
method: 'POST',
|
||
body: formData,
|
||
})
|
||
.then((response) => response.text())
|
||
.then((result) => {
|
||
alert(result);
|
||
location.reload();
|
||
})
|
||
.catch((error) => {
|
||
alert('上传失败:' + error.message);
|
||
});
|
||
}
|
||
|
||
</script>
|
||
<h2 class="text-center mt-4 mb-4" data-translate="subscriptionManagement"></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="container-fluid">
|
||
<div class="row">
|
||
<?php
|
||
$maxSubscriptions = 6;
|
||
for ($i = 0; $i < $maxSubscriptions; $i++):
|
||
$displayIndex = $i + 1;
|
||
$url = $subscriptions[$i]['url'] ?? '';
|
||
$fileName = $subscriptions[$i]['file_name'] ?? "subscription_" . ($displayIndex) . ".yaml";
|
||
?>
|
||
<div class="col-md-4 mb-3">
|
||
<form method="post" class="card shadow-sm">
|
||
<div class="card-body">
|
||
<div class="form-group">
|
||
<h5 for="subscription_url_<?php echo $displayIndex; ?>" class="mb-2" data-translate="subscriptionLink"><?php echo $displayIndex; ?></h5>
|
||
<input type="text" name="subscription_url" id="subscription_url_<?php echo $displayIndex; ?>" value="<?php echo htmlspecialchars($url); ?>" class="form-control" data-translate-placeholder="enterSubscriptionUrl">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="custom_file_name_<?php echo $displayIndex; ?>"data-translate="customFileName"></label>
|
||
<input type="text" name="custom_file_name" id="custom_file_name_<?php echo $displayIndex; ?>" value="<?php echo htmlspecialchars($fileName); ?>" 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 btn-block"><i class="bi bi-arrow-repeat"></i> <span data-translate="updateSubscription">Settings</span> <?php echo $displayIndex; ?></button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<?php if (($displayIndex) % 3 == 0 && $displayIndex < $maxSubscriptions): ?>
|
||
</div><div class="row">
|
||
<?php endif; ?>
|
||
|
||
<?php endfor; ?>
|
||
</div>
|
||
</div>
|
||
<?php else: ?>
|
||
<p>未找到订阅信息。</p>
|
||
<?php endif; ?>
|
||
<!DOCTYPE html>
|
||
<html lang="zh">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h2 class="mt-4 mb-4 text-center" data-translate="auto_update_title"></h2>
|
||
<form method="post" class="text-center">
|
||
<div class="d-flex flex-wrap justify-content-center gap-2">
|
||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#cronModal">
|
||
<i class="bi bi-clock"></i> <span data-translate="set_cron_job"></span>
|
||
</button>
|
||
<button type="submit" name="createShellScript" value="true" class="btn btn-success">
|
||
<i class="bi bi-terminal"></i> <span data-translate="generate_update_script"></span>
|
||
</button>
|
||
<button type="button" class="btn btn-info" data-bs-toggle="modal" data-bs-target="#downloadModal">
|
||
<i class="bi bi-download"></i> <span data-translate="update_database"></span>
|
||
</button>
|
||
<a class="btn btn-pink btn-sm text-white" target="_blank" href="./filekit.php" style="font-size: 14px; font-weight: bold;">
|
||
<i class="bi bi-file-earmark-text"></i> <span data-translate="open_file_helper"></span>
|
||
</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="modal fade" id="downloadModal" tabindex="-1" aria-labelledby="downloadModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="downloadModalLabel" data-translate="select_database_download"></h5>
|
||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form method="GET" action="">
|
||
<div class="mb-3">
|
||
<label for="fileSelect" class="form-label" data-translate="select_file"></label>
|
||
<select class="form-select" id="fileSelect" name="file">
|
||
<option value="geoip">geoip.metadb</option>
|
||
<option value="geosite">geosite.dat</option>
|
||
<option value="cache">cache.db</option>
|
||
</select>
|
||
</div>
|
||
<div class="d-flex justify-content-end">
|
||
<button type="submit" class="btn btn-primary me-2" data-translate="download_button"></button>
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="cancel_button"></button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<form method="POST">
|
||
<div class="modal fade" id="cronModal" tabindex="-1" aria-labelledby="cronModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="cronModalLabel" data-translate="cron_task_title">设置 Cron 计划任务</h5>
|
||
<button type="button" class="close" data-bs-dismiss="modal" aria-label="Close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="mb-3">
|
||
<label for="cronExpression" class="form-label" data-translate="cron_expression_label">Cron 表达式</label>
|
||
<input type="text" class="form-control" id="cronExpression" name="cronExpression" value="0 2 * * *" required>
|
||
</div>
|
||
<div class="alert alert-info">
|
||
<strong data-translate="cron_hint">提示:</strong> <span data-translate="cron_expression_format">Cron 表达式格式:</span>
|
||
<ul>
|
||
<li><code>分钟 小时 日 月 星期</code></li>
|
||
<li><span data-translate="cron_example">示例: 每天凌晨 2 点: </span><code>0 2 * * *</code></li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer d-flex justify-content-end gap-3">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="cancel_button">取消</button>
|
||
<button type="submit" name="createCronJob" class="btn btn-primary" data-translate="save_button">保存</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<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;
|
||
}
|
||
|
||
#cronModal .alert {
|
||
text-align: left;
|
||
}
|
||
|
||
#cronModal code {
|
||
white-space: pre-wrap;
|
||
}
|
||
|
||
</style>
|
||
|
||
</div>
|
||
<footer class="text-center">
|
||
<p><?php echo $footer ?></p>
|
||
</footer>
|