mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-15 04:53:00 +08:00
8023 lines
246 KiB
PHP
8023 lines
246 KiB
PHP
<?php
|
||
$timezone = trim(shell_exec("uci get system.@system[0].zonename 2>/dev/null"));
|
||
|
||
date_default_timezone_set($timezone ?: 'UTC');
|
||
?>
|
||
|
||
<?php
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'clearNekoTmpDir') {
|
||
$nekoDir = '/tmp/neko';
|
||
$response = [
|
||
'success' => false,
|
||
'message' => ''
|
||
];
|
||
|
||
if (is_dir($nekoDir)) {
|
||
if (deleteNekoTmpDirectory($nekoDir)) {
|
||
$response['success'] = true;
|
||
$response['message'] = 'Directory cleared successfully.';
|
||
} else {
|
||
$response['message'] = 'Failed to delete the directory.';
|
||
}
|
||
} else {
|
||
$response['message'] = 'The directory does not exist.';
|
||
}
|
||
|
||
header('Content-Type: application/json');
|
||
echo json_encode($response);
|
||
exit;
|
||
}
|
||
|
||
function deleteNekoTmpDirectory($dir) {
|
||
if (!file_exists($dir)) {
|
||
return true;
|
||
}
|
||
if (!is_dir($dir)) {
|
||
return unlink($dir);
|
||
}
|
||
|
||
foreach (scandir($dir) as $item) {
|
||
if ($item === '.' || $item === '..') {
|
||
continue;
|
||
}
|
||
|
||
if (!deleteNekoTmpDirectory($dir . DIRECTORY_SEPARATOR . $item)) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return rmdir($dir);
|
||
}
|
||
?>
|
||
|
||
<?php
|
||
$singbox_autostart = exec("uci -q get neko.cfg.singbox_autostart") ?: '0';
|
||
|
||
if (isset($_POST['save_autostart'])) {
|
||
$autostart = isset($_POST['autostart']) ? '1' : '0';
|
||
shell_exec("uci set neko.cfg.singbox_autostart='$autostart'");
|
||
shell_exec("uci commit neko");
|
||
writeToLog("Autostart setting changed to: $autostart");
|
||
echo "<div class='log-message alert alert-danger'><span data-translate='settingSaved'></span></div>";
|
||
$singbox_autostart = $autostart;
|
||
}
|
||
?>
|
||
|
||
<style>
|
||
.modal {
|
||
opacity: 0;
|
||
visibility: hidden;
|
||
}
|
||
|
||
.modal.show {
|
||
opacity: 1;
|
||
visibility: visible;
|
||
}
|
||
|
||
#theme-loader {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: linear-gradient(135deg,
|
||
var(--accent-color),
|
||
color-mix(in oklch, var(--accent-color), white 30%),
|
||
color-mix(in oklch, var(--accent-color), black 30%));
|
||
background-size: 300% 300%;
|
||
animation: animated-gradient 6s ease infinite;
|
||
z-index: 9999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: opacity 0.6s ease;
|
||
color: var(--text-primary);
|
||
font-family: "Segoe UI", sans-serif;
|
||
}
|
||
|
||
.theme-loader-content {
|
||
text-align: center;
|
||
backdrop-filter: var(--glass-blur);
|
||
padding: 2rem 3rem;
|
||
border-radius: var(--radius);
|
||
background: color-mix(in oklch, var(--bg-container), white 10%);
|
||
box-shadow: 0 0 20px oklch(0 0 0 / 0.2);
|
||
}
|
||
|
||
.loader-icon {
|
||
font-size: 3.5rem;
|
||
animation: bounceIn 1s ease-in-out infinite alternate;
|
||
}
|
||
|
||
.loader-title {
|
||
font-size: 1.25rem;
|
||
margin-top: 1rem;
|
||
opacity: 0.85;
|
||
}
|
||
|
||
@keyframes animated-gradient {
|
||
0% {
|
||
background-position: 0% 50%;
|
||
}
|
||
|
||
50% {
|
||
background-position: 100% 50%;
|
||
}
|
||
|
||
100% {
|
||
background-position: 0% 50%;
|
||
}
|
||
}
|
||
|
||
@keyframes bounceIn {
|
||
0% {
|
||
transform: scale(1) translateY(0);
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1.1) translateY(-10px);
|
||
}
|
||
}
|
||
|
||
.navbar {
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
border: none;
|
||
padding: 0.8rem 1.2rem;
|
||
border-radius: 0;
|
||
box-shadow: none;
|
||
color: var(--text-primary);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.navbar-brand {
|
||
font-size: 1.5rem !important;
|
||
font-weight: 900 !important;
|
||
color: var(--accent-color) !important;
|
||
text-decoration: none;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.nav-link::after {
|
||
content: '';
|
||
position: absolute;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 0;
|
||
height: 2px;
|
||
background: var(--accent-color);
|
||
transition: width var(--transition-speed) ease;
|
||
}
|
||
|
||
.nav-link:hover::after {
|
||
width: 70%;
|
||
}
|
||
|
||
.navbar-brand:hover {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
.navbar .nav-link {
|
||
color: var(--text-primary) !important;
|
||
padding: 0.5rem 1rem;
|
||
border-radius: var(--radius);
|
||
transition: var(--transition);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.4rem;
|
||
font-weight: 500;
|
||
position: relative;
|
||
}
|
||
|
||
.navbar .nav-link.active:hover,
|
||
.navbar .nav-link:hover {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
.navbar .nav-link.active {
|
||
color: var(--accent-color) !important;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.navbar-toggler {
|
||
color: var(--accent-color) !important;
|
||
border: none;
|
||
background: transparent;
|
||
outline: none;
|
||
}
|
||
|
||
.navbar-toggler i {
|
||
color: var(--accent-color) !important;
|
||
font-size: 1.8rem;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.icon-btn-group {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 0.7rem !important;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.icon-btn {
|
||
display: inline-flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
width: 2rem !important;
|
||
height: 2rem !important;
|
||
padding: 0 !important;
|
||
border-radius: 1rem !important;
|
||
font-size: 1rem !important;
|
||
line-height: 1 !important;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.log-message.alert {
|
||
position: fixed;
|
||
left: 20px;
|
||
padding: 12px 16px;
|
||
background: rgb(45, 125, 55) !important;
|
||
color: #fff !important;
|
||
color: white;
|
||
border-radius: 8px;
|
||
z-index: 9999;
|
||
max-width: 370px;
|
||
font-size: 15px;
|
||
word-wrap: break-word;
|
||
line-height: 1.5;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
|
||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(2px);
|
||
transform: translateY(0);
|
||
opacity: 0;
|
||
animation: scrollUp 12s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||
display: inline-block;
|
||
margin-bottom: 10px;
|
||
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
@keyframes scrollUp {
|
||
0% {
|
||
top: 90%;
|
||
opacity: 0;
|
||
}
|
||
|
||
20% {
|
||
opacity: 1;
|
||
}
|
||
|
||
80% {
|
||
top: 50%;
|
||
opacity: 1;
|
||
}
|
||
|
||
100% {
|
||
top: 45%;
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.white-text-table,
|
||
.white-text-table td,
|
||
.white-text-table input {
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.custom-tooltip {
|
||
position: fixed;
|
||
background: var(--accent-color);
|
||
color: #fff;
|
||
padding: 8px 12px;
|
||
border-radius: var(--radius);
|
||
font-size: 0.85rem;
|
||
pointer-events: none;
|
||
z-index: 10000000;
|
||
white-space: normal;
|
||
max-width: 300px;
|
||
line-height: 1.4;
|
||
backdrop-filter: var(--glass-blur);
|
||
border: var(--glass-border);
|
||
box-shadow: 0 4px 20px color-mix(in oklch, var(--bg-container), black 85%),
|
||
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
transition: opacity 0.2s ease-out,
|
||
transform 0.2s ease-out;
|
||
word-wrap: break-word;
|
||
overflow-wrap: break-word;
|
||
}
|
||
|
||
.custom-tooltip.show {
|
||
opacity: var(--glass-opacity);
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.custom-tooltip::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: -5px;
|
||
left: 50%;
|
||
transform: translateX(-50%) rotate(45deg);
|
||
width: 10px;
|
||
height: 10px;
|
||
background: inherit;
|
||
border-top: var(--glass-border);
|
||
border-left: var(--glass-border);
|
||
z-index: -1;
|
||
}
|
||
|
||
.custom-tooltip[data-position="top"]::after {
|
||
top: auto;
|
||
bottom: -5px;
|
||
border: none;
|
||
border-right: var(--glass-border);
|
||
border-bottom: var(--glass-border);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.custom-tooltip {
|
||
max-width: 200px;
|
||
white-space: normal;
|
||
font-size: 0.8rem;
|
||
}
|
||
}
|
||
|
||
.modal-body .alert.alert-warning .note-text {
|
||
color: #ff0000 !important;
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
.navbar-brand {
|
||
flex-shrink: 1;
|
||
min-width: 0;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
#dynamicTitle {
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.navbar-toggler i {
|
||
font-size: 1.2rem !important;
|
||
}
|
||
|
||
.navbar-toggler {
|
||
padding: 0.15rem 0.35rem;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.row>.col {
|
||
flex: 0 0 50%;
|
||
max-width: 50%;
|
||
}
|
||
|
||
.row>.col:nth-child(n+3) {
|
||
flex: 0 0 33.3333%;
|
||
max-width: 33.3333%;
|
||
}
|
||
|
||
.row {
|
||
row-gap: 13px;
|
||
}
|
||
|
||
.d-flex.flex-wrap.mb-4 {
|
||
flex-wrap: nowrap !important;
|
||
}
|
||
}
|
||
|
||
.centered-img {
|
||
transition: transform 0.6s ease-in-out;
|
||
transform-style: preserve-3d;
|
||
transform: rotateY(0deg);
|
||
}
|
||
</style>
|
||
|
||
<div id="theme-loader" style="display: none;">
|
||
<div class="theme-loader-content">
|
||
<div class="loader-icon">🚀</div>
|
||
<div class="loader-title">Updating...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const loader = document.getElementById("theme-loader");
|
||
const MAX_WAIT_TIME = 15000;
|
||
|
||
document.querySelectorAll("form").forEach(form => {
|
||
form.addEventListener("submit", (e) => {
|
||
if (e.submitter?.classList.contains('cancel-btn')) {
|
||
e.preventDefault();
|
||
return;
|
||
}
|
||
|
||
if (form.classList.contains('no-loader')) {
|
||
return;
|
||
}
|
||
|
||
if (loader) {
|
||
loader.style.display = "flex";
|
||
setTimeout(() => {
|
||
loader.style.opacity = "0";
|
||
setTimeout(() => {
|
||
loader.style.display = "none";
|
||
loader.style.opacity = "1";
|
||
}, 600);
|
||
}, MAX_WAIT_TIME);
|
||
}
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const tooltip = document.createElement('div');
|
||
tooltip.className = 'custom-tooltip';
|
||
document.body.appendChild(tooltip);
|
||
|
||
document.querySelectorAll('[data-tooltip]').forEach(el => {
|
||
el.addEventListener('mouseenter', e => {
|
||
tooltip.textContent = el.getAttribute('data-tooltip');
|
||
tooltip.classList.add('show');
|
||
|
||
requestAnimationFrame(() => {
|
||
positionTooltip(e);
|
||
});
|
||
});
|
||
|
||
el.addEventListener('mousemove', e => {
|
||
positionTooltip(e);
|
||
});
|
||
|
||
el.addEventListener('mouseleave', () => {
|
||
tooltip.classList.remove('show');
|
||
});
|
||
});
|
||
|
||
function positionTooltip(e) {
|
||
const spacing = 20;
|
||
const tooltipRect = tooltip.getBoundingClientRect();
|
||
const targetRect = e.target.getBoundingClientRect();
|
||
|
||
const top = targetRect.bottom + spacing;
|
||
const left = targetRect.left + (targetRect.width - tooltipRect.width) / 2;
|
||
|
||
tooltip.style.top = `${top}px`;
|
||
tooltip.style.left = `${left}px`;
|
||
tooltip.dataset.position = 'bottom';
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', e => {
|
||
if (e.target.closest('.btn-refresh-page')) {
|
||
location.reload();
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<div class="modal fade" id="autostartModal" tabindex="-1" aria-labelledby="autostartModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg">
|
||
<form method="post" class="no-loader">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="autostartModalLabel" data-translate="singboxAutostartTitle">
|
||
Sing-box Auto Start
|
||
</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="form-check form-switch">
|
||
<input class="form-check-input" type="checkbox" name="autostart" id="autostart"
|
||
<?php echo ($singbox_autostart === '1') ? 'checked' : ''; ?>>
|
||
<label class="form-check-label" for="autostart">
|
||
<strong data-translate="enableAutostart">Enable Auto Start</strong>
|
||
</label>
|
||
</div>
|
||
<div class="mt-3">
|
||
<small class="text-muted" data-translate="autostartTip">
|
||
When checked, Sing-box will start automatically on router reboot (if Mihomo is not running)
|
||
</small>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="cancel">Cancel</button>
|
||
<button type="submit" name="save_autostart" class="btn btn-primary" data-translate="saveButton">Save</button>
|
||
</div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="portModal" tabindex="-1" aria-labelledby="portModalLabel" aria-hidden="true">
|
||
<div class="modal-dialog modal-dialog-centered modal-xl">
|
||
<form id="portForm" method="POST" action="./save_ports.php" class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="portModalLabel" data-translate="portInfoTitle">Port Information</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<table class="table table-light table-striped text-center align-middle w-100 mb-0 white-text-table">
|
||
<thead class="table-light">
|
||
<tr>
|
||
<th style="width: 20%;" class="text-center" data-translate="componentName">Component Name</th>
|
||
<th style="width: 16%;" class="text-center">socks-port</th>
|
||
<th style="width: 16%;" class="text-center">mixed-port</th>
|
||
<th style="width: 16%;" class="text-center">redir-port</th>
|
||
<th style="width: 16%;" class="text-center">port</th>
|
||
<th style="width: 16%;" class="text-center">tproxy-port</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Mihomo</td>
|
||
<td><input type="number" class="form-control text-center" name="mihomo_socks" value="<?= htmlspecialchars($neko_cfg['socks']) ?>"></td>
|
||
<td><input type="number" class="form-control text-center" name="mihomo_mixed" value="<?= htmlspecialchars($neko_cfg['mixed']) ?>"></td>
|
||
<td><input type="number" class="form-control text-center" name="mihomo_redir" value="<?= htmlspecialchars($neko_cfg['redir']) ?>"></td>
|
||
<td><input type="number" class="form-control text-center" name="mihomo_port" value="<?= htmlspecialchars($neko_cfg['port']) ?>"></td>
|
||
<td><input type="number" class="form-control text-center" name="mihomo_tproxy" value="<?= htmlspecialchars($neko_cfg['tproxy']) ?>"></td>
|
||
</tr>
|
||
<tr>
|
||
<td>Sing-box</td>
|
||
<td><input type="number" class="form-control text-center" name="singbox_http" value="<?= htmlspecialchars($http_port) ?>"></td>
|
||
<td><input type="number" class="form-control text-center" name="singbox_mixed" value="<?= htmlspecialchars($mixed_port) ?>"></td>
|
||
<td><input type="text" class="form-control text-center" name="singbox_mixed" value="—" disabled></td>
|
||
<td><input type="text" class="form-control text-center" name="singbox_mixed" value="—" disabled></td>
|
||
<td><input type="text" class="form-control text-center" name="singbox_mixed" value="—" disabled></td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<div class="text-danger text-center fw-bold mt-3 mb-1" data-translate="portChangeNotice">
|
||
Port changes will take effect after restarting the service.
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer justify-content-end">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="closeButton">Close</button>
|
||
<button type="submit" class="btn btn-primary" data-translate="save">Save</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php include './language.php'; include './cfg.php'; $current = basename($_SERVER['PHP_SELF']); ?>
|
||
<html lang="<?php echo $currentLang; ?>">
|
||
<div class="modal fade" id="langModal" tabindex="-1" aria-labelledby="langModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="langModalLabel" data-translate="select_language">Select Language</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<select id="langSelect" class="form-select" onchange="changeLanguage(this.value)">
|
||
<option value="zh" data-translate="simplified_chinese">Simplified Chinese</option>
|
||
<option value="hk" data-translate="traditional_chinese">Traditional Chinese</option>
|
||
<option value="en" data-translate="english">English</option>
|
||
<option value="ko" data-translate="korean">Korean</option>
|
||
<option value="vi" data-translate="vietnamese">Vietnamese</option>
|
||
<option value="ja" data-translate="japanese"></option>
|
||
<option value="ru" data-translate="russian"></option>
|
||
<option value="de" data-translate="germany">Germany</option>
|
||
<option value="fr" data-translate="france">France</option>
|
||
<option value="ar" data-translate="arabic"></option>
|
||
<option value="es" data-translate="spanish">spanish</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="close">Close</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<?php
|
||
$default_url = 'https://raw.githubusercontent.com/Thaolga/Rules/main/music/songs.txt';
|
||
$file_path = __DIR__ . '/lib/url_config.txt';
|
||
$message = '';
|
||
|
||
if (!file_exists($file_path)) {
|
||
if (file_put_contents($file_path, $default_url) !== false) {
|
||
chmod($file_path, 0644);
|
||
}
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
if (isset($_POST['new_url'])) {
|
||
$new_url = $_POST['new_url'];
|
||
if (file_put_contents($file_path, $new_url) !== false) {
|
||
chmod($file_path, 0644);
|
||
$message = 'Update successful!';
|
||
} else {
|
||
$message = 'Update failed, please check permissions.';
|
||
}
|
||
}
|
||
|
||
if (isset($_POST['reset_default'])) {
|
||
if (file_put_contents($file_path, $default_url) !== false) {
|
||
chmod($file_path, 0644);
|
||
$message = 'Default URL restored!';
|
||
} else {
|
||
$message = 'Restore failed, please check permissions.';
|
||
}
|
||
}
|
||
} else {
|
||
$new_url = file_exists($file_path) ? file_get_contents($file_path) : $default_url;
|
||
}
|
||
?>
|
||
|
||
<div class="modal fade" id="colorModal" tabindex="-1" aria-hidden="true">
|
||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" data-translate="advanced_color_control">Advanced Color Control</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="row">
|
||
<div class="col-md-6">
|
||
<div class="card h-100">
|
||
<div class="card-header bg-primary text-white">
|
||
<i class="bi bi-sliders"></i> <span data-translate="color_control">Color Control</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="primary_hue">Primary Hue</label>
|
||
<div class="d-flex align-items-center">
|
||
<input type="range" class="form-range flex-grow-1" id="hueSlider" min="0" max="360" step="1">
|
||
<span class="ms-2" style="min-width: 50px;" id="hueValue">0°</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="chroma">Chroma</label>
|
||
<div class="d-flex align-items-center">
|
||
<input type="range" class="form-range flex-grow-1" id="chromaSlider" min="0" max="0.3" step="0.01">
|
||
<span class="ms-2" style="min-width: 50px;" id="chromaValue">0.10</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="lightness">Lightness</label>
|
||
<div class="d-flex align-items-center">
|
||
<input type="range" class="form-range flex-grow-1" id="lightnessSlider" min="0" max="100" step="1">
|
||
<span class="ms-2" style="min-width: 50px;" id="lightnessValue">30%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="or_use_palette">Or use palette:</label>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#4d79ff" data-h="240" data-c="0.2" data-l="30"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#ff4d94" data-h="340" data-c="0.25" data-l="35"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#4dff88" data-h="150" data-c="0.18" data-l="40"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#ffb84d" data-h="40" data-c="0.22" data-l="45"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#bf4dff" data-h="280" data-c="0.23" data-l="50"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#ff6b6b" data-h="10" data-c="0.24" data-l="55"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#4eca9e" data-h="160" data-c="0.19" data-l="60"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#ff9ff3" data-h="310" data-c="0.21" data-l="65"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#6c757d" data-h="200" data-c="0.05" data-l="50"></button>
|
||
<button class="btn btn-sm p-3 rounded-circle" style="background-color:#ffc107" data-h="50" data-c="0.26" data-l="70"></button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mt-3">
|
||
<button class="btn btn-secondary w-100" id="resetColorBtn">
|
||
<i class="bi bi-arrow-counterclockwise"></i> <span data-translate="reset_to_default">Reset to Default</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<div class="card h-100">
|
||
<div class="card-header bg-primary text-white">
|
||
<i class="bi bi-eye"></i> <span data-translate="color_preview">Color Preview</span>
|
||
</div>
|
||
<div class="card-body">
|
||
<div id="colorPreview" style="height: 100px; border-radius: 5px; margin-bottom: 15px;"></div>
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="oklch_values">OKLCH Values:</label>
|
||
<div id="oklchValue" class="text-monospace">OKLCH(30%, 0.10, 260°)</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="contrast_ratio">Contrast Ratio:</label>
|
||
<div id="contrastRatio">21.00:1</div>
|
||
<div id="contrastRating" class="mt-1 text-success fw-bold"><i class="bi bi-check-circle-fill"></i> Excellent (AAA)</div>
|
||
</div>
|
||
<div class="mb-3">
|
||
<label class="form-label" data-translate="recent_colors">Recent Colors:</label>
|
||
<div id="recentColors" class="d-flex flex-wrap gap-2"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||
<i class="bi bi-x"></i> <span data-translate="cancel">Cancel</span>
|
||
</button>
|
||
<button type="button" class="btn btn-danger" id="removeAppColorBtn">
|
||
<i class="bi bi-eraser"></i> <span data-translate="reset">Reset</span>
|
||
</button>
|
||
<button type="button" class="btn btn-primary" id="applyColorBtn">
|
||
<i class="bi bi-check"></i> <span data-translate="apply_color">Apply Color</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="colorPickerModal" tabindex="-1" aria-labelledby="colorPickerModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-xl modal-dialog-centered">
|
||
<div class="modal-content bg-dark text-white border border-secondary rounded">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="colorPickerModalLabel" data-translate="color_width_panel">Color & Width Panel</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="row mb-3">
|
||
<div class="col-md-6">
|
||
<label for="containerWidth" class="form-label" data-translate="page_width">Page Width</label>
|
||
<input type="range" class="form-range" name="containerWidth" id="containerWidth" min="800" max="5400" step="50" value="1800" style="width: 100%;">
|
||
<div id="widthValue" class="mt-2" style="color: #FF00FF;" data-translate="current_width">Current Width: 1800px</div>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label for="modalMaxWidth" class="form-label" data-translate="settings.modal.maxWidth">Modal Max Width</label>
|
||
<input type="range" class="form-range" name="modalMaxWidth" id="modalMaxWidth" min="500" max="5400" step="50" value="800" style="width: 100%;">
|
||
<div id="modalWidthValue" class="mt-2" style="color: #00FF00;" data-translate="current_max_width">Current Max Width: 800px</div>
|
||
</div>
|
||
</div>
|
||
<div id="color-preview" class="rounded border mb-3" style="height: 100px; background: #333;"></div>
|
||
<div class="d-flex align-items-center gap-2 mb-3">
|
||
<div class="d-flex align-items-center justify-content-center rounded"
|
||
style="width: 80px; height: 50px; background: rgba(255,255,255,0.05); border: var(--border-strong);">
|
||
<input type="color" id="color-selector"
|
||
class="form-control form-control-color p-0"
|
||
value="#0f3460"
|
||
data-tooltip="choose_color"
|
||
style="width: 36px; height: 36px; cursor: pointer;">
|
||
</div>
|
||
<div id="current-color-block"
|
||
class="rounded d-flex align-items-center justify-content-center text-white position-relative"
|
||
style="height: 50px; flex: 1; cursor: pointer; background: #333; border: var(--border-strong); transition: all 0.3s ease;">
|
||
<input type="text"
|
||
id="color-input"
|
||
class="form-control text-center border-0 bg-transparent text-white p-0 m-0"
|
||
value=" "
|
||
style="width: 100%; font-size: 0.9rem;">
|
||
<span class="fake-cursor">|</span>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex gap-2 mb-3">
|
||
<button id="apply-color" class="btn btn-success flex-fill"><i class="fa fa-paint-brush"></i> <span data-translate="apply_color">Apply</span> </button>
|
||
<button id="reset-color" class="btn btn-danger flex-fill"><i class="fa fa-undo"></i> <span data-translate="reset">Reset</span></button>
|
||
</div>
|
||
<div id="preset-colors" class="d-grid gap-2"></div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal"><i class="fa fa-times"></i> <span data-translate="close">Close</span></button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="control-panel-overlay" id="controlPanelOverlay" style="display:none;">
|
||
<div class="control-panel">
|
||
<div class="panel-header">
|
||
<h3><i class="bi bi-gear"></i> <span data-translate="control_panel_title">Control Panel</span></h3>
|
||
<button class="close-icon" onclick="toggleControlPanel()" data-tooltip="close">
|
||
<i class="bi bi-x-lg"></i>
|
||
</button>
|
||
</div>
|
||
<div class="buttons-grid">
|
||
<button class="panel-btn" data-bs-toggle="modal" data-bs-target="#musicModal" data-tooltip="music_player">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-music-note-beamed"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="music_player">Music Player</div>
|
||
<small class="opacity-75" data-translate="music_desc">Control background music settings</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" id="color-panel-btn" data-bs-toggle="modal" data-bs-target="#colorPickerModal">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-palette"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="color_panel">Color Panel</div>
|
||
<small class="opacity-75" data-translate="color_desc">Customize interface colors</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" id="advancedColorBtn" data-tooltip="advanced_color_settings">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-palette2"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="advanced_color">Advanced Color Settings</div>
|
||
<small class="opacity-75" data-translate="advanced_color_desc">Professional color adjustments</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" id="clear-cache-btn" data-tooltip="clear_cache">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-trash"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="clear_cache">Clear Cache</div>
|
||
<small class="opacity-75" data-translate="cache_desc">Free up system resources</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" id="startCheckBtn" data-tooltip="start_check">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-globe2"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="start_check">Start Website Check</div>
|
||
<small class="opacity-75" data-translate="check_desc">Diagnose website status</small>
|
||
</div>
|
||
<i class="bi bi-toggle-off" id="autoCheckIcon" style="cursor: pointer; margin-left: 10px;"></i>
|
||
</button>
|
||
<button class="panel-btn" id="openModalBtn" data-tooltip="open_animation">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-sliders"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="open_animation">Animation Control</div>
|
||
<small class="opacity-75" data-translate="animation_desc">Adjust animation effects</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" data-bs-toggle="modal" data-bs-target="#langModal">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-translate"></i>
|
||
</div>
|
||
<div class="language-container">
|
||
<div>
|
||
<div data-translate="set_language">Set Language</div>
|
||
<small class="opacity-75" data-translate="language_desc">Choose interface language</small>
|
||
</div>
|
||
<img id="flagIcon" src="https://flagcdn.com/w20/cn.png" class="flag-icon" alt="Country Flag">
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" onclick="window.open('./monaco.php', '_blank')">
|
||
<div class="btn-icon">
|
||
<i class="bi bi-file-earmark"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="fileHelper">File Helper</div>
|
||
<small class="opacity-75" data-translate="file_desc">Manage your files</small>
|
||
</div>
|
||
</button>
|
||
<button class="panel-btn" id="translationToggleBtn" data-tooltip="enable">
|
||
<div class="btn-icon" id="translationToggleIcon">
|
||
<i class="bi bi-toggle-off"></i>
|
||
</div>
|
||
<div>
|
||
<div data-translate="ip_info">IP Info</div>
|
||
<div class="opacity-75" id="translationToggleText" style="font-size: 0.85rem;">enable</div>
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<div class="action-row">
|
||
<div class="color-picker">
|
||
<button class="btn btn-info ms-2" id="fontSwitchBtn" data-tooltip="toggle_font">
|
||
<i id="fontSwitchIcon" class="fa-solid fa-font" style="color: white;"></i>
|
||
</button>
|
||
<label for="colorPicker" data-translate="component_bg_color">Component Background</label>
|
||
<input type="color" id="colorPicker" value="#0f3460">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="animationModal" class="animation-modal" style="display: none;">
|
||
<div class="animation-modal-content">
|
||
<button id="toggleAnimationBtn" data-translate="start_cube_animation">🖥️ Start Cube Animation</button>
|
||
<button id="toggleSnowBtn" data-translate="start_snow_animation">❄️ Start Snow Animation</button>
|
||
<button id="toggleLightEffectBtn" data-translate="start_light_effect_animation">✨Start Light Effect Animation</button>
|
||
<button class="modal-close-btn" onclick="closeModal()" data-translate="close">Close</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="floatingLyrics" style="display: none;">
|
||
<div class="floating-controls">
|
||
<button class="ctrl-btn" onclick="changeTrack(-1, true)" data-tooltip="previous_track">
|
||
<i class="fas fa-backward"></i>
|
||
</button>
|
||
<button class="ctrl-btn" id="floatingPlayBtn" onclick="togglePlay()" data-tooltip="play_pause">
|
||
<i class="bi bi-play-fill"></i>
|
||
</button>
|
||
<button class="ctrl-btn" onclick="changeTrack(1, true)" data-tooltip="next_track">
|
||
<i class="fas fa-forward"></i>
|
||
</button>
|
||
<button class="ctrl-btn" id="floatingRepeatBtn" onclick="toggleRepeat()">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
</button>
|
||
<button class="ctrl-btn" id="speedToggle" data-tooltip="playback_speed">
|
||
<span id="speedLabel">1×</span>
|
||
</button>
|
||
<button class="ctrl-btn" id="muteToggle" data-tooltip="volume">
|
||
<i class="bi bi-volume-up-fill"></i>
|
||
</button>
|
||
<button class="ctrl-btn" id="updatePlaylistBtn" onclick="updatePlaylist()"
|
||
data-tooltip="update_playlist">
|
||
<i class="fa fa-sync-alt"></i>
|
||
</button>
|
||
<button class="ctrl-btn" id="goFirstBtn">
|
||
<i class="fas fa-circle-left"></i>
|
||
</button>
|
||
<button id="toggleFloatingLyricsBtn" class="ctrl-btn toggleFloatingLyricsBtn" data-tooltip="toggle_floating_lyrics">
|
||
<i class="bi bi-display floatingIcon"></i>
|
||
</button>
|
||
</div>
|
||
<div id="floatingCurrentSong" class="vertical-title"></div>
|
||
<div class="vertical-lyrics"></div>
|
||
</div>
|
||
|
||
<span id="clearConfirmText" data-translate="clear_confirm" class="d-none"></span>
|
||
|
||
<div class="modal fade" id="musicModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content bg-dark text-white">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="langModalLabel" data-translate="music_player"></h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div id="floatingLyrics"></div>
|
||
<div id="currentSong" class="mb-3 text-center font-weight-bold fs-4"></div>
|
||
<div class="lyrics-container" id="lyricsContainer" style="height: 300px; overflow-y: auto;"></div>
|
||
<div class="non-lyrics-content">
|
||
<div class="progress-container mt-3">
|
||
<div class="progress">
|
||
<div class="progress-bar bg-success" role="progressbar" id="progressBar" style="width: 0%" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
|
||
</div>
|
||
</div>
|
||
<div class="d-flex justify-content-between mt-2 small">
|
||
<span id="currentTime">0:00</span>
|
||
<span id="duration">0:00</span>
|
||
</div>
|
||
<div class="controls d-flex justify-content-center gap-3 mt-4">
|
||
<button class="control-btn toggleFloatingLyricsBtn" data-tooltip="toggle_floating_lyrics">
|
||
<i class="bi bi-display floatingIcon"></i>
|
||
</button>
|
||
<button class="control-btn" id="repeatBtn" onclick="toggleRepeat()">
|
||
<i class="bi bi-arrow-repeat"></i>
|
||
</button>
|
||
<button class="control-btn" onclick="changeTrack(-1, true)" data-tooltip="previous_track">
|
||
<i class="bi bi-caret-left-fill"></i>
|
||
</button>
|
||
<button class="control-btn" id="playPauseBtn" onclick="togglePlay()" data-tooltip="play_pause">
|
||
<i class="bi bi-play-fill"></i>
|
||
</button>
|
||
<button class="control-btn" onclick="changeTrack(1, true)" data-tooltip="next_track">
|
||
<i class="bi bi-caret-right-fill"></i>
|
||
</button>
|
||
<button class="control-btn" type="button" data-bs-toggle="modal" data-bs-target="#urlModal" data-tooltip="custom_playlist">
|
||
<i class="bi bi-music-note-list"></i>
|
||
</button>
|
||
<button class="btn btn-volume position-relative" id="volumeToggle" data-tooltip="volume">
|
||
<i class="bi bi-volume-up-fill"></i>
|
||
<div class="volume-slider-container position-absolute bottom-100 start-50 translate-middle-x mb-1 p-2" id="volumePanel" style="display: none; width: 120px;">
|
||
<input type="range" class="form-range volume-slider" id="volumeSlider" min="0" max="1" step="0.01" value="1">
|
||
</div>
|
||
</button>
|
||
</div>
|
||
<div class="playlist mt-3" id="playlist"></div>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button class="btn btn-info" id="lyricsToggle"><i class="bi bi-chevron-down" id="lyricsIcon"></i></button>
|
||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal" data-translate="cancel">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="urlModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-xl">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" data-translate="update_playlist"></h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<form method="POST">
|
||
<div class="mb-3">
|
||
<label data-translate="playlist_url"></label>
|
||
<input
|
||
type="text"
|
||
name="new_url"
|
||
id="new_url"
|
||
class="form-control"
|
||
value="<?= htmlspecialchars($new_url) ?>"
|
||
required
|
||
>
|
||
</div>
|
||
<div class="d-flex gap-2">
|
||
<button type="submit" class="btn btn-primary" data-translate="save"></button>
|
||
<button type="submit" name="reset_default" class="btn btn-secondary" data-translate="reset_default"></button>
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="cancel"></button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal fade" id="confirmModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false" style="z-index: 9999;">
|
||
<div class="modal-dialog modal-lg modal-dialog-centered">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" data-translate="confirm_title">Confirm Action</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body" id="confirmModalMessage"></div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" data-translate="cancel">Cancel</button>
|
||
<button type="button" class="btn btn-danger" id="confirmModalYes" data-translate="confirm">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
const navbar = document.getElementById('navbarContent');
|
||
const toggler = document.querySelector('.navbar-toggler');
|
||
|
||
if (!navbar || !toggler) return;
|
||
|
||
function isMobile() {
|
||
return window.innerWidth < 992;
|
||
}
|
||
|
||
navbar.querySelectorAll('.nav-link').forEach(link => {
|
||
link.addEventListener('click', () => {
|
||
if (isMobile() && navbar.classList.contains('show')) {
|
||
toggler.click();
|
||
}
|
||
});
|
||
});
|
||
|
||
document.addEventListener('click', (e) => {
|
||
const isInside = navbar.contains(e.target) || toggler.contains(e.target);
|
||
if (!isInside && navbar.classList.contains('show') && isMobile()) {
|
||
toggler.click();
|
||
}
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
function clearNekoTmpDir() {
|
||
fetch('', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: 'action=clearNekoTmpDir'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert(translations["tmp_neko_cleared"] || "The /tmp/neko directory has been cleared successfully.");
|
||
} else {
|
||
if (data.message === 'The directory does not exist.') {
|
||
alert(translations["tmp_neko_not_exist"] || "The /tmp/neko directory does not exist. No action was taken.");
|
||
} else {
|
||
alert('Failed to clear the /tmp/neko directory: ' + data.message);
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('An error occurred while trying to clear the /tmp/neko directory.');
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function () {
|
||
const animationModal = document.getElementById('animationModal');
|
||
const openModalBtn = document.getElementById('openModalBtn');
|
||
openModalBtn.addEventListener('click', function () {
|
||
animationModal.style.display = 'flex';
|
||
});
|
||
|
||
animationModal.addEventListener('click', function (e) {
|
||
if (e.target === animationModal) {
|
||
animationModal.style.display = 'none';
|
||
}
|
||
});
|
||
});
|
||
|
||
function closeModal() {
|
||
document.getElementById('animationModal').style.display = 'none';
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', function () {
|
||
interact('.modal-dialog.draggable').draggable({
|
||
allowFrom: '.modal-header',
|
||
modifiers: [
|
||
interact.modifiers.restrictRect({
|
||
restriction: 'parent',
|
||
endOnly: true
|
||
})
|
||
],
|
||
listeners: {
|
||
start(event) {
|
||
event.target.style.transition = 'none';
|
||
event.target.classList.add('dragging');
|
||
},
|
||
move(event) {
|
||
const target = event.target;
|
||
const x = (parseFloat(target.dataset.x) || 0) + event.dx;
|
||
const y = (parseFloat(target.dataset.y) || 0) + event.dy;
|
||
|
||
target.style.transform = `translate(${x}px, ${y}px)`;
|
||
target.dataset.x = x;
|
||
target.dataset.y = y;
|
||
},
|
||
end(event) {
|
||
event.target.style.transition = '';
|
||
event.target.classList.remove('dragging');
|
||
}
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('.modal').forEach(modal => {
|
||
const dialog = modal.querySelector('.modal-dialog');
|
||
dialog.classList.add('draggable');
|
||
|
||
modal.addEventListener('show.bs.modal', () => {
|
||
dialog.style.transform = '';
|
||
dialog.dataset.x = 0;
|
||
dialog.dataset.y = 0;
|
||
});
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
const langData = <?php echo json_encode($langData); ?>;
|
||
const currentLang = "<?php echo $currentLang; ?>";
|
||
|
||
let translations = langData[currentLang] || langData['en'];
|
||
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
updateLanguage(currentLang);
|
||
updateFlagIcon(currentLang);
|
||
document.getElementById("langSelect").value = currentLang;
|
||
});
|
||
|
||
function updateLanguage(lang) {
|
||
translations = langData[lang] || langData['en'];
|
||
|
||
const translateElement = (el, attribute, property) => {
|
||
const translationKey = el.getAttribute(attribute);
|
||
if (translations[translationKey]) {
|
||
el[property] = translations[translationKey];
|
||
}
|
||
};
|
||
|
||
document.querySelectorAll('[data-translate]').forEach(el => {
|
||
const translationKey = el.getAttribute('data-translate');
|
||
const dynamicContent = el.getAttribute('data-dynamic-content') || '';
|
||
|
||
if (translations[translationKey]) {
|
||
let translatedText = translations[translationKey];
|
||
|
||
const indexValue = el.getAttribute('data-index');
|
||
if (indexValue) {
|
||
translatedText = translatedText.replace('{index}', indexValue);
|
||
}
|
||
|
||
const countValue = el.getAttribute('data-count');
|
||
if (countValue) {
|
||
translatedText = translatedText.replace('{count}', countValue);
|
||
}
|
||
|
||
if (el.tagName === 'OPTGROUP') {
|
||
el.setAttribute('label', translatedText);
|
||
} else {
|
||
el.innerText = translatedText + dynamicContent;
|
||
}
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('[data-translate-title]').forEach(el => {
|
||
const translationKey = el.getAttribute('data-translate-title');
|
||
if (translations[translationKey]) {
|
||
el.title = translations[translationKey];
|
||
el.setAttribute('data-tooltip', translations[translationKey]);
|
||
} else {
|
||
el.removeAttribute('title');
|
||
el.removeAttribute('data-tooltip');
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('[data-tooltip]').forEach(el => {
|
||
const key = el.getAttribute('data-tooltip');
|
||
if (translations[key]) {
|
||
el.setAttribute('data-tooltip', translations[key]);
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('[data-translate-placeholder]').forEach(el => {
|
||
const translationKey = el.getAttribute('data-translate-placeholder');
|
||
if (translations[translationKey]) {
|
||
el.setAttribute('placeholder', translations[translationKey]);
|
||
el.setAttribute('aria-label', translations[translationKey]);
|
||
el.setAttribute('title', translations[translationKey]);
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('pre[data-translate]').forEach(el => {
|
||
const translationKey = el.getAttribute('data-translate');
|
||
if (translationKey && translations[translationKey]) {
|
||
const text = el.innerText;
|
||
const suffix = text.includes(':') ? text.split(':').slice(1).join(':').trim() : '';
|
||
el.innerText = translations[translationKey] + (suffix ? ': ' + suffix : '');
|
||
}
|
||
});
|
||
|
||
document.querySelectorAll('[data-translate]').forEach(el => {
|
||
const translationKey = el.getAttribute('data-translate');
|
||
if (translationKey && translations[translationKey]) {
|
||
el.setAttribute('label', translations[translationKey]);
|
||
}
|
||
});
|
||
}
|
||
|
||
const languageStandardMap = {
|
||
'zh': 'zh-CN',
|
||
'hk': 'zh-TW',
|
||
'en': 'en-US',
|
||
'ko': 'ko-KR',
|
||
'ja': 'ja-JP',
|
||
'ru': 'ru-RU',
|
||
'ar': 'ar-EG',
|
||
'es': 'es-ES',
|
||
'de': 'de-DE',
|
||
'fr': 'fr-FR',
|
||
'th': 'th-TH',
|
||
'bn': 'bn-BD',
|
||
'vi': 'vi-VN'
|
||
};
|
||
|
||
const langToVoiceLangMap = {
|
||
'zh': ['zh-CN', 'zh-HK', 'zh-TW'],
|
||
'hk': ['zh-CN', 'zh-HK', 'zh-TW'],
|
||
'en': ['en-US', 'en-GB', 'en-AU', 'en-CA', 'en-IN'],
|
||
'ko': ['ko-KR'],
|
||
'ja': ['ja-JP'],
|
||
'vi': ['vi-VN'],
|
||
'th': ['th-TH'],
|
||
'ru': ['ru-RU'],
|
||
'ar': ['ar-SA', 'ar-EG', 'ar-AE'],
|
||
'es': ['es-ES', 'es-MX', 'es-US'],
|
||
'de': ['de-DE', 'de-AT', 'de-CH'],
|
||
'fr': ['fr-FR', 'fr-CA', 'fr-CH'],
|
||
'bn': ['bn-BD', 'bn-IN']
|
||
};
|
||
|
||
function speakMessage(message) {
|
||
const globalVoiceEnabled = localStorage.getItem('colorVoiceEnabled') !== 'false';
|
||
if (!globalVoiceEnabled) return;
|
||
|
||
function speakWithVoices() {
|
||
const voices = speechSynthesis.getVoices();
|
||
if (voices.length === 0) {
|
||
setTimeout(speakWithVoices, 100);
|
||
return;
|
||
}
|
||
|
||
const xhr = new XMLHttpRequest();
|
||
xhr.open('GET', './lib/language.txt', true);
|
||
xhr.onreadystatechange = function() {
|
||
if (xhr.readyState === 4 && xhr.status === 200) {
|
||
const lang = xhr.responseText.trim();
|
||
|
||
const voiceKey = getVoicePreferenceKey(lang);
|
||
const savedVoiceIndex = localStorage.getItem(voiceKey);
|
||
|
||
let selectedVoice = null;
|
||
|
||
if (savedVoiceIndex !== null) {
|
||
const targetLangs = langToVoiceLangMap[lang] || [languageStandardMap[lang] || lang];
|
||
const filteredVoices = voices.filter(voice =>
|
||
targetLangs.some(targetLang => voice.lang.startsWith(targetLang))
|
||
);
|
||
selectedVoice = filteredVoices[savedVoiceIndex];
|
||
}
|
||
|
||
if (!selectedVoice) {
|
||
const chineseVoiceKey = getChineseVoiceKey();
|
||
const savedChineseIndex = localStorage.getItem(chineseVoiceKey);
|
||
const chineseVoices = voices.filter(voice =>
|
||
['zh-CN', 'zh-HK', 'zh-TW'].some(lang => voice.lang.startsWith(lang))
|
||
);
|
||
|
||
if (savedChineseIndex !== null && chineseVoices[savedChineseIndex]) {
|
||
selectedVoice = chineseVoices[savedChineseIndex];
|
||
} else if (chineseVoices.length > 0) {
|
||
selectedVoice = chineseVoices[0];
|
||
}
|
||
}
|
||
|
||
if (!selectedVoice) {
|
||
selectedVoice = voices.find(voice => voice.lang.includes('zh')) || voices[0];
|
||
}
|
||
|
||
if (selectedVoice) {
|
||
const utterance = new SpeechSynthesisUtterance(message);
|
||
utterance.voice = selectedVoice;
|
||
speechSynthesis.speak(utterance);
|
||
}
|
||
}
|
||
};
|
||
xhr.send();
|
||
}
|
||
|
||
speakWithVoices();
|
||
}
|
||
|
||
function getVoicePreferenceKey(lang) {
|
||
if (lang === 'zh' || lang === 'hk') {
|
||
return 'voicePreference_chinese';
|
||
}
|
||
return `voicePreference_${lang}`;
|
||
}
|
||
|
||
function getChineseVoiceKey() {
|
||
return 'voicePreference_chinese';
|
||
}
|
||
|
||
function updateFlagIcon(lang) {
|
||
const flagImg = document.getElementById('flagIcon');
|
||
if (!flagImg) return;
|
||
|
||
const flagMap = {
|
||
'zh': './assets/neko/flags/cn.png',
|
||
'hk': './assets/neko/flags/hk.png',
|
||
'en': './assets/neko/flags/us.png',
|
||
'ko': './assets/neko/flags/kr.png',
|
||
'ja': './assets/neko/flags/jp.png',
|
||
'ru': './assets/neko/flags/ru.png',
|
||
'ar': './assets/neko/flags/sa.png',
|
||
'es': './assets/neko/flags/es.png',
|
||
'de': './assets/neko/flags/de.png',
|
||
'fr': './assets/neko/flags/fr.png',
|
||
'vi': './assets/neko/flags/vn.png'
|
||
};
|
||
flagImg.src = flagMap[lang] || flagMap['en'];
|
||
}
|
||
|
||
function changeLanguage(lang) {
|
||
fetch('', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded'
|
||
},
|
||
body: 'lang=' + lang
|
||
}).then(response => response.text())
|
||
.then(data => {
|
||
console.log(data);
|
||
updateLanguage(lang);
|
||
updateFlagIcon(lang);
|
||
const langLabelMap = {
|
||
'zh': '语言已切换为简体中文',
|
||
'hk': '語言已切換為繁體中文',
|
||
'en': 'Language switched to English',
|
||
'ko': '언어가 한국어로 변경되었습니다',
|
||
'ja': '言語が日本語に変更されました',
|
||
'ru': 'Язык переключен на русский',
|
||
'ar': 'تم تغيير اللغة إلى العربية',
|
||
'es': 'El idioma ha cambiado a español',
|
||
'de': 'Sprache auf Deutsch umgestellt',
|
||
'fr': 'Langue changée en français',
|
||
'vi': 'Đã chuyển ngôn ngữ sang tiếng Việt'
|
||
};
|
||
|
||
const message = langLabelMap[lang] || 'Language switched';
|
||
|
||
if (typeof speakMessage === 'function') {
|
||
speakMessage(message);
|
||
}
|
||
if (typeof showLogMessage === 'function') {
|
||
showLogMessage(message);
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
function manageNotifications() {
|
||
const messages = document.querySelectorAll('.log-message:not(.initialized)');
|
||
let basePosition = 20;
|
||
|
||
const iconPaths = {
|
||
error: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z',
|
||
warning: 'M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z',
|
||
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'
|
||
};
|
||
|
||
function generateIcon(type) {
|
||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="${iconPaths[type] || iconPaths.info}"/></svg>`;
|
||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
||
}
|
||
|
||
messages.forEach((msg, index) => {
|
||
msg.classList.add('initialized');
|
||
|
||
const type = msg.dataset.type || (msg.classList.contains('error') ? 'error' : msg.classList.contains('warning') ? 'warning' : 'info');
|
||
|
||
const originalContent = msg.innerHTML;
|
||
msg.innerHTML = `
|
||
<div class="log-content">
|
||
<span class="log-icon" style="background-image:url('${generateIcon(type)}')"></span>
|
||
${originalContent}
|
||
<button class="log-close-btn">×</button>
|
||
</div>
|
||
`;
|
||
|
||
msg.style.transform = `translateY(${index * 70}px)`;
|
||
|
||
setTimeout(() => {
|
||
msg.classList.add('show');
|
||
|
||
setTimeout(() => {
|
||
msg.classList.remove('show');
|
||
msg.classList.add('hiding');
|
||
|
||
setTimeout(() => msg.remove(), 500);
|
||
}, 12000);
|
||
}, index * 150);
|
||
|
||
const closeBtn = msg.querySelector('.log-close-btn');
|
||
closeBtn.addEventListener('click', () => {
|
||
msg.classList.remove('show');
|
||
msg.classList.add('hiding');
|
||
setTimeout(() => msg.remove(), 500);
|
||
});
|
||
});
|
||
}
|
||
|
||
manageNotifications();
|
||
|
||
new MutationObserver(manageNotifications).observe(document.body, {
|
||
childList: true,
|
||
subtree: true
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.img-con {
|
||
width: 65px;
|
||
height: 45px;
|
||
display: flex;
|
||
justify-content: center;
|
||
overflow: visible;
|
||
}
|
||
|
||
#flag {
|
||
width: auto;
|
||
height: auto;
|
||
max-width: 65px;
|
||
max-height: 45px;
|
||
object-fit: contain;
|
||
}
|
||
|
||
.status-icon {
|
||
width: 48px;
|
||
height: 48px;
|
||
object-fit: contain;
|
||
display: block;
|
||
}
|
||
|
||
.status-icons {
|
||
display: flex;
|
||
height: 45px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.site-icon {
|
||
display: flex;
|
||
justify-content: center;
|
||
height: 45px;
|
||
margin: 0 6px;
|
||
}
|
||
|
||
.mx-1 {
|
||
margin: 0 4px;
|
||
}
|
||
|
||
.site-icon[onclick*="github"] .status-icon {
|
||
width: 51px;
|
||
height: 49px;
|
||
}
|
||
|
||
.site-icon[onclick*="github"] {
|
||
width: 50px;
|
||
height: 47px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.site-icon[onclick*="openai"] .status-icon {
|
||
width: 52px;
|
||
height: 54px;
|
||
margin-top: -2px;
|
||
}
|
||
|
||
.site-icon[onclick*="openai"] {
|
||
width: 52px;
|
||
height: 54px;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.container-sm.container-bg.callout.border {
|
||
padding: 12px 15px;
|
||
min-height: 70px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.row.align-items-center {
|
||
width: 100%;
|
||
margin: 0;
|
||
display: flex;
|
||
gap: 12px;
|
||
height: 45px;
|
||
}
|
||
|
||
.col-3 {
|
||
height: 45px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: center;
|
||
}
|
||
|
||
.col.text-center {
|
||
position: static;
|
||
left: auto;
|
||
transform: none;
|
||
}
|
||
|
||
.container-sm .row .col-4 {
|
||
position: static !important;
|
||
order: 2 !important;
|
||
width: 100% !important;
|
||
padding-left: 54px !important;
|
||
margin-top: 5px !important;
|
||
text-align: left !important;
|
||
}
|
||
|
||
#ping-result {
|
||
font-weight: bold;
|
||
}
|
||
|
||
#d-ip {
|
||
color: #09B63F;
|
||
font-weight: 700 !important;
|
||
}
|
||
|
||
#d-ip > .ip-main {
|
||
font-size: 14px !important;
|
||
}
|
||
|
||
#d-ip .badge-primary {
|
||
font-size: 13px !important;
|
||
}
|
||
|
||
.info.small {
|
||
color: #ff69b4;
|
||
font-weight: 600;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.site-icon, .img-con {
|
||
cursor: pointer !important;
|
||
transition: all 0.2s ease !important;
|
||
position: relative !important;
|
||
user-select: none !important;
|
||
}
|
||
|
||
.site-icon:hover, .img-con:hover {
|
||
transform: translateY(-2px) !important;
|
||
}
|
||
|
||
.site-icon:active, .img-con:active {
|
||
transform: translateY(1px) !important;
|
||
opacity: 0.8 !important;
|
||
}
|
||
|
||
@media (max-width: 1206px) {
|
||
.site-icon[onclick*="baidu"],
|
||
.site-icon[onclick*="taobao"],
|
||
.site-icon[onclick*="google"],
|
||
.site-icon[onclick*="openai"],
|
||
.site-icon[onclick*="youtube"],
|
||
.site-icon[onclick*="github"] {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
.animation-modal {
|
||
position: absolute;
|
||
top: 100px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
z-index: 1050;
|
||
display: none;
|
||
}
|
||
|
||
.animation-modal-content {
|
||
background: linear-gradient(135deg, #1e3c72, #2a5298);
|
||
padding: 30px 40px;
|
||
border-radius: 20px;
|
||
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
|
||
max-width: 400px;
|
||
width: 90%;
|
||
text-align: center;
|
||
color: white;
|
||
animation: scaleIn 0.3s ease;
|
||
}
|
||
|
||
.animation-modal-content button {
|
||
display: block;
|
||
width: 100%;
|
||
padding: 12px 20px;
|
||
margin: 10px 0;
|
||
font-size: 1rem;
|
||
border: none;
|
||
border-radius: 12px;
|
||
background: rgba(255, 255, 255, 0.2);
|
||
color: white;
|
||
cursor: pointer;
|
||
transition: background 0.3s, transform 0.2s;
|
||
}
|
||
|
||
.animation-modal-content button:hover {
|
||
background: rgba(255, 255, 255, 0.3);
|
||
transform: scale(1.03);
|
||
}
|
||
|
||
@keyframes scaleIn {
|
||
from {
|
||
transform: scale(0.85);
|
||
opacity: 0;
|
||
}
|
||
to {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<link rel="stylesheet" href="./assets/bootstrap/bootstrap.min.css">
|
||
<link rel="stylesheet" href="./assets/bootstrap/all.min.css">
|
||
<link rel="stylesheet" href="./assets/bootstrap/bootstrap-icons.css">
|
||
<link rel="stylesheet" href="./assets/bootstrap/leaflet.css">
|
||
<link rel="stylesheet" href="./assets/bootstrap/Control.FullScreen.min.css">
|
||
<link rel="stylesheet" href="./assets/bootstrap/custom.css">
|
||
<link rel="icon" href="./assets/img/nekobox.png">
|
||
|
||
<script src="./assets/bootstrap/jquery.min.js"></script>
|
||
<script src="./assets/bootstrap/bootstrap.bundle.min.js"></script>
|
||
<script src="./assets/js/feather.min.js"></script>
|
||
<script src="./assets/bootstrap/interact.min.js"></script>
|
||
<script src="./assets/bootstrap/Sortable.min.js"></script>
|
||
<script src="./assets/bootstrap/leaflet.js"></script>
|
||
<script src="./assets/bootstrap/Control.FullScreen.min.js"></script>
|
||
<script src="./assets/js/neko.js"></script>
|
||
|
||
<?php
|
||
$translate = [
|
||
|
||
];
|
||
$lang = $_GET['lang'] ?? 'en';
|
||
?>
|
||
|
||
<?php if (in_array($currentLang, ['zh', 'en', 'hk', 'vi', 'ja', 'ru', 'ar', 'es', 'ko', 'de', 'fr'])): ?>
|
||
<div id="status-bar-component" class="container-sm container-bg mt-4" style="display: none;">
|
||
<div class="row align-items-center">
|
||
<div class="col-auto">
|
||
<div class="img-con">
|
||
<img src="./assets/neko/img/loading.svg" id="flag" title="<?php echo $translations['refresh_ip']; ?>" onclick="IP.getIpipnetIP()">
|
||
</div>
|
||
</div>
|
||
<div class="col-3">
|
||
<p id="d-ip" class="ip-address mb-0">Checking...</p>
|
||
<p id="ipip" class="info small mb-0"></p>
|
||
</div>
|
||
<div class="col text-center">
|
||
<p id="ping-result" class="mb-0"></p>
|
||
</div>
|
||
<div class="col-auto ms-auto">
|
||
<div class="status-icons d-flex">
|
||
<div class="site-icon mx-1" onclick="pingHost('baidu', 'Baidu')">
|
||
<img src="./assets/neko/img/site_icon_01.png" id="baidu-normal" title="<?php echo sprintf($translations['test_latency'], 'Baidu'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_01.png" id="baidu-gray" class="status-icon">
|
||
</div>
|
||
<div class="site-icon mx-1" onclick="pingHost('taobao', 'Taobao')">
|
||
<img src="./assets/neko/img/site_icon_02.png" id="taobao-normal" title="<?php echo sprintf($translations['test_latency'], 'Taobao'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_02.png" id="taobao-gray" class="status-icon">
|
||
</div>
|
||
<div class="site-icon mx-1" onclick="pingHost('google', 'Google')">
|
||
<img src="./assets/neko/img/site_icon_03.png" id="google-normal" title="<?php echo sprintf($translations['test_latency'], 'Google'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_03.png" id="google-gray" class="status-icon">
|
||
</div>
|
||
<div class="site-icon mx-1" onclick="pingHost('openai', 'OpenAI')">
|
||
<img src="./assets/neko/img/site_icon_06.png" id="openai-normal" title="<?php echo sprintf($translations['test_latency'], 'OpenAI'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_06.png" id="openai-gray" class="status-icon">
|
||
</div>
|
||
<div class="site-icon mx-1" onclick="pingHost('youtube', 'YouTube')">
|
||
<img src="./assets/neko/img/site_icon_04.png" id="youtube-normal" title="<?php echo sprintf($translations['test_latency'], 'YouTube'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_04.png" id="youtube-gray" class="status-icon">
|
||
</div>
|
||
<div class="site-icon mx-1" onclick="pingHost('github', 'GitHub')">
|
||
<img src="./assets/neko/img/site_icon_05.png" id="github-normal" title="<?php echo sprintf($translations['test_latency'], 'GitHub'); ?>" class="status-icon" style="display: none;">
|
||
<img src="./assets/neko/img/site_icon1_05.png" id="github-gray" class="status-icon">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<script type="text/javascript">
|
||
const _IMG = './assets/neko/';
|
||
const translate = <?php echo json_encode($translate, JSON_UNESCAPED_UNICODE); ?>;
|
||
let cachedIP = null;
|
||
let cachedInfo = null;
|
||
let random = parseInt(Math.random() * 100000000);
|
||
|
||
const sitesToPing = {
|
||
baidu: { url: 'https://www.baidu.com', name: 'Baidu' },
|
||
taobao: { url: 'https://www.taobao.com', name: 'Taobao' },
|
||
google: { url: 'https://www.google.com', name: 'Google' },
|
||
youtube: { url: 'https://www.youtube.com', name: 'YouTube' },
|
||
github: { url: 'https://www.github.com', name: 'GitHub' },
|
||
openai : { url: 'https://www.openai.com', name: 'OpenAI' }
|
||
};
|
||
|
||
async function checkAllPings() {
|
||
const pingResults = {};
|
||
for (const [key, site] of Object.entries(sitesToPing)) {
|
||
const { url, name } = site;
|
||
try {
|
||
const startTime = performance.now();
|
||
await fetch(url, { mode: 'no-cors', cache: 'no-cache' });
|
||
const endTime = performance.now();
|
||
const pingTime = Math.round(endTime - startTime);
|
||
pingResults[key] = { name, pingTime };
|
||
} catch (error) {
|
||
pingResults[key] = { name, pingTime: '超时' };
|
||
}
|
||
}
|
||
return pingResults;
|
||
}
|
||
|
||
const checkSiteStatus = {
|
||
sites: {
|
||
baidu: 'https://www.baidu.com',
|
||
taobao: 'https://www.taobao.com',
|
||
google: 'https://www.google.com',
|
||
youtube: 'https://www.youtube.com',
|
||
github: 'https://www.github.com',
|
||
openai: 'https://www.openai.com'
|
||
},
|
||
|
||
check: async function() {
|
||
for (let [site, url] of Object.entries(this.sites)) {
|
||
try {
|
||
const response = await fetch(url, {
|
||
mode: 'no-cors',
|
||
cache: 'no-cache'
|
||
});
|
||
|
||
document.getElementById(`${site}-normal`).style.display = 'inline';
|
||
document.getElementById(`${site}-gray`).style.display = 'none';
|
||
} catch (error) {
|
||
document.getElementById(`${site}-normal`).style.display = 'none';
|
||
document.getElementById(`${site}-gray`).style.display = 'inline';
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
async function pingHost(site, siteName) {
|
||
const url = checkSiteStatus.sites[site];
|
||
const resultElement = document.getElementById('ping-result');
|
||
|
||
try {
|
||
resultElement.innerHTML = `<span style="font-size: 22px">${translations.testing_latency.replace('%s', siteName)}...</span>`;
|
||
resultElement.style.color = '#87CEFA';
|
||
resultElement.style.display = 'block';
|
||
|
||
const startTime = performance.now();
|
||
await fetch(url, {
|
||
mode: 'no-cors',
|
||
cache: 'no-cache'
|
||
});
|
||
const endTime = performance.now();
|
||
const pingTime = Math.round(endTime - startTime);
|
||
|
||
resultElement.innerHTML = `<span style="font-size: 22px">${translations.latency_result.replace('%s', siteName).replace('%d', pingTime)}</span>`;
|
||
|
||
if (pingTime <= 300) {
|
||
resultElement.style.color = '#09B63F';
|
||
} else if (pingTime <= 700) {
|
||
resultElement.style.color = '#FFA500';
|
||
} else {
|
||
resultElement.style.color = '#ff6b6b';
|
||
}
|
||
} catch (error) {
|
||
resultElement.innerHTML = `<span style="font-size: 22px">${translations.connection_timeout.replace('%s', siteName)}</span>`;
|
||
resultElement.style.color = '#ff6b6b';
|
||
resultElement.style.display = 'block';
|
||
}
|
||
setTimeout(() => {
|
||
resultElement.style.display = 'none';
|
||
}, 6000);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const translationBtn = document.getElementById('translationToggleBtn');
|
||
const translationText = document.getElementById('translationToggleText');
|
||
const translationIcon = document.getElementById('translationToggleIcon').querySelector('i');
|
||
|
||
if (!localStorage.getItem('translationEnabled')) {
|
||
localStorage.setItem('translationEnabled', 'false');
|
||
}
|
||
|
||
const updateButtonState = () => {
|
||
const isEnabled = localStorage.getItem('translationEnabled') === 'true';
|
||
translationText.textContent = isEnabled
|
||
? (langData[currentLang]?.disable || 'Disable')
|
||
: (langData[currentLang]?.enable || 'Enable');
|
||
|
||
translationIcon.className = isEnabled
|
||
? 'bi bi-toggle-on text-success'
|
||
: 'bi bi-toggle-off text-white';
|
||
};
|
||
|
||
updateButtonState();
|
||
|
||
translationBtn.addEventListener('click', () => {
|
||
const newState = localStorage.getItem('translationEnabled') !== 'true';
|
||
localStorage.setItem('translationEnabled', newState);
|
||
updateButtonState();
|
||
|
||
const spokenMessage = newState
|
||
? (langData[currentLang]?.translation_enabled || 'Translation Enabled')
|
||
: (langData[currentLang]?.translation_disabled || 'Translation Disabled');
|
||
speakMessage(spokenMessage);
|
||
showLogMessage(spokenMessage);
|
||
translationBtn.style.transform = "scale(0.95)";
|
||
setTimeout(() => {
|
||
translationBtn.style.transform = "scale(1)";
|
||
}, 100);
|
||
});
|
||
});
|
||
|
||
async function translateText(text, targetLang = null) {
|
||
if (!text?.trim()) return text;
|
||
|
||
if (!targetLang) {
|
||
try {
|
||
const response = await fetch('./lib/language.txt?t=' + new Date().getTime());
|
||
if (response.ok) {
|
||
const lang = await response.text();
|
||
targetLang = lang.trim().toLowerCase() || 'zh';
|
||
} else {
|
||
targetLang = 'zh';
|
||
}
|
||
} catch (error) {
|
||
targetLang = 'zh';
|
||
}
|
||
}
|
||
|
||
const apiLangMap = {
|
||
'zh':'zh-CN','zh-tw':'zh-TW','zh-CN':'zh-CN','hk':'zh-TW', 'zh-mo': 'zh-TW',
|
||
'en':'en','ja':'ja','ko':'ko','fr':'fr','de':'de','es':'es',
|
||
'it':'it','pt':'pt','ru':'ru','ar':'ar','hi':'hi','bn':'bn',
|
||
'ms':'ms','id':'id','vi':'vi','th':'th','nl':'nl','pl':'pl',
|
||
'tr':'tr','sv':'sv','no':'no','fi':'fi','da':'da','cs':'cs',
|
||
'he':'he','el':'el','hu':'hu','ro':'ro','sk':'sk','bg':'bg','uk':'uk'
|
||
};
|
||
|
||
const apiTargetLang = apiLangMap[targetLang.toLowerCase()] || 'zh-CN';
|
||
|
||
const detectJP = (text) => /[\u3040-\u309F\u30A0-\u30FF\u4E00-\u9FAF]/.test(text);
|
||
const sourceLang = detectJP(text) ? 'ja' : 'en';
|
||
|
||
const isSameLang = () => {
|
||
const sourceBase = sourceLang.split('-')[0];
|
||
const targetBase = apiTargetLang.split('-')[0];
|
||
return sourceBase === targetBase;
|
||
};
|
||
|
||
if (isSameLang()) return text;
|
||
|
||
if (localStorage.getItem('translationEnabled') !== 'true') return text;
|
||
|
||
const cacheKey = `trans_${sourceLang}_${apiTargetLang}_${text}`;
|
||
const cached = localStorage.getItem(cacheKey);
|
||
if (cached) return cached;
|
||
|
||
const apis = [
|
||
{
|
||
url: `https://api.mymemory.translated.net/get?q=${encodeURIComponent(text)}&langpair=${sourceLang}|${apiTargetLang}&de=example@yourdomain.com`,
|
||
method: 'GET',
|
||
parse: data => data.responseData?.translatedText
|
||
}
|
||
];
|
||
|
||
const controller = new AbortController();
|
||
try {
|
||
const promises = apis.map(async (api) => {
|
||
const timeoutPromise = new Promise((_, reject) =>
|
||
setTimeout(() => reject('timeout'), api.timeout || 3000)
|
||
);
|
||
|
||
try {
|
||
const fetchPromise = fetch(api.url, {
|
||
method: api.method,
|
||
headers: api.headers || {},
|
||
body: api.method === 'POST' ? api.body : undefined,
|
||
signal: controller.signal
|
||
});
|
||
|
||
const response = await Promise.race([fetchPromise, timeoutPromise]);
|
||
if (!response.ok) throw new Error('API response not OK');
|
||
const data = await response.json();
|
||
return api.parse ? api.parse(data) : data?.translatedText;
|
||
} catch (e) {
|
||
return null;
|
||
}
|
||
});
|
||
|
||
for (const promise of promises) {
|
||
const result = await promise;
|
||
if (result) {
|
||
localStorage.setItem(cacheKey, result);
|
||
clearOldCache();
|
||
return result;
|
||
}
|
||
}
|
||
} finally {
|
||
controller.abort();
|
||
}
|
||
|
||
return text;
|
||
}
|
||
|
||
function clearOldCache() {
|
||
const cachePrefix = 'trans_';
|
||
const cacheKeys = Object.keys(localStorage).filter(key =>
|
||
key.startsWith(cachePrefix)
|
||
);
|
||
|
||
if (cacheKeys.length > 1000) {
|
||
const itemsToRemove = cacheKeys.slice(0, cacheKeys.length - 1000);
|
||
itemsToRemove.forEach(key => localStorage.removeItem(key));
|
||
}
|
||
}
|
||
|
||
let IP = {
|
||
isRefreshing: false,
|
||
lastGeoData: null,
|
||
ipApis: [
|
||
{url: 'https://api.ipify.org?format=json', type: 'json', key: 'ip'},
|
||
{url: 'https://api-ipv4.ip.sb/geoip', type: 'json', key: 'ip'},
|
||
{url: 'https://myip.ipip.net', type: 'text'},
|
||
{url: 'http://pv.sohu.com/cityjson', type: 'text'},
|
||
{url: 'https://ipinfo.io/json', type: 'json', key: 'ip'},
|
||
{url: 'https://ipapi.co/json/', type: 'json'},
|
||
{url: 'https://freegeoip.app/json/', type: 'json'}
|
||
],
|
||
|
||
fetchIP: async () => {
|
||
let error;
|
||
for(let api of IP.ipApis) {
|
||
try {
|
||
const response = await IP.get(api.url, api.type);
|
||
if(api.type === 'json') {
|
||
const ipData = api.key ? response.data[api.key] : response.data;
|
||
cachedIP = ipData;
|
||
document.getElementById('d-ip').innerHTML = ipData;
|
||
return ipData;
|
||
} else {
|
||
const ipData = response.data.match(/\d+\.\d+\.\d+\.\d+/)?.[0];
|
||
if(ipData) {
|
||
cachedIP = ipData;
|
||
document.getElementById('d-ip').innerHTML = ipData;
|
||
return ipData;
|
||
}
|
||
}
|
||
} catch(e) {
|
||
error = e;
|
||
console.error(`Error with ${api.url}:`, e);
|
||
continue;
|
||
}
|
||
}
|
||
throw error || new Error("All IP APIs failed");
|
||
},
|
||
|
||
get: (url, type) =>
|
||
fetch(url, {
|
||
method: 'GET',
|
||
cache: 'no-store'
|
||
}).then((resp) => {
|
||
if (type === 'text')
|
||
return Promise.all([resp.ok, resp.status, resp.text(), resp.headers]);
|
||
else
|
||
return Promise.all([resp.ok, resp.status, resp.json(), resp.headers]);
|
||
}).then(([ok, status, data, headers]) => {
|
||
if (ok) {
|
||
return { ok, status, data, headers };
|
||
} else {
|
||
throw new Error(JSON.stringify(data.error));
|
||
}
|
||
}).catch(error => {
|
||
console.error("Error fetching data:", error);
|
||
throw error;
|
||
}),
|
||
|
||
Ipip: async (ip, elID) => {
|
||
const geoApis = [
|
||
{url: `https://api.ip.sb/geoip/${ip}`, type: 'json'},
|
||
{url: 'https://myip.ipip.net', type: 'text'},
|
||
{url: `http://ip-api.com/json/${ip}`, type: 'json'},
|
||
{url: `https://ipinfo.io/${ip}/json`, type: 'json'},
|
||
{url: `https://ipapi.co/${ip}/json/`, type: 'json'},
|
||
{url: `https://freegeoip.app/json/${ip}`, type: 'json'}
|
||
];
|
||
|
||
let geoData = null;
|
||
let error;
|
||
|
||
for(let api of geoApis) {
|
||
try {
|
||
const response = await IP.get(api.url, api.type);
|
||
geoData = response.data;
|
||
break;
|
||
} catch(e) {
|
||
error = e;
|
||
console.error(`Error with ${api.url}:`, e);
|
||
continue;
|
||
}
|
||
}
|
||
|
||
if(!geoData) {
|
||
throw error || new Error("All Geo APIs failed");
|
||
}
|
||
|
||
cachedIP = ip;
|
||
IP.lastGeoData = geoData;
|
||
|
||
IP.updateUI(geoData, elID);
|
||
},
|
||
|
||
updateUI: async (data, elID) => {
|
||
try {
|
||
const country = await translateText(data.country || translations['unknown']);
|
||
const region = await translateText(data.region || "");
|
||
const city = await translateText(data.city || "");
|
||
const isp = await translateText(data.isp || "");
|
||
const asnOrganization = await translateText(data.asn_organization || "");
|
||
|
||
let location = `${region && city && region !== city ? `${region} ${city}` : region || city || ''}`;
|
||
|
||
let displayISP = isp;
|
||
let displayASN = asnOrganization;
|
||
|
||
if (isp && asnOrganization && asnOrganization.includes(isp)) {
|
||
displayISP = '';
|
||
} else if (isp && asnOrganization && isp.includes(asnOrganization)) {
|
||
displayASN = '';
|
||
}
|
||
|
||
const isSmallScreen = window.innerWidth < 768;
|
||
|
||
let locationInfo;
|
||
if (isSmallScreen) {
|
||
locationInfo = `<span style="margin-left: 8px; position: relative; top: -4px;">${location} ${data.asn || ''} ${displayASN}</span>`;
|
||
} else {
|
||
locationInfo = `<span style="margin-left: 8px; position: relative; top: -4px;">${location} ${displayISP} ${data.asn || ''} ${displayASN}</span>`;
|
||
}
|
||
|
||
const isHidden = localStorage.getItem("ipHidden") === "true";
|
||
|
||
let simpleDisplay = `
|
||
<div class="ip-row" style="display: flex; align-items: center; gap: 8px; flex-wrap: nowrap;">
|
||
<div class="ip-main" style="cursor: pointer;" onclick="handleIPClick()" title="${translations['show_ip']}">
|
||
<div style="display: flex; align-items: center; gap: 5px;">
|
||
<span id="ip-address" style="margin-left: 0.3ch">${isHidden ? '**.**.**.**.**' : cachedIP}</span>
|
||
<span class="badge badge-primary" style="color: #333;">${country}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<span id="toggle-ip" style="cursor: pointer; text-indent: 0.3ch; padding-top: 2px;" title="${translations['hide_ip']}">
|
||
<i class="bi ${isHidden ? 'bi-eye-slash' : 'bi-eye'}" style="font-size: 1.4rem; vertical-align: middle;"></i>
|
||
</span>
|
||
`;
|
||
|
||
document.getElementById('d-ip').innerHTML = simpleDisplay;
|
||
document.getElementById('ipip').innerHTML = locationInfo;
|
||
|
||
const countryCode = data.country_code || 'unknown';
|
||
const flagSrc = (countryCode !== 'unknown') ? _IMG + "flags/" + countryCode.toLowerCase() + ".png" : './assets/neko/flags/cn.png';
|
||
$("#flag").attr("src", flagSrc);
|
||
|
||
const ipContainer = document.querySelector('.ip-row');
|
||
const toggleIcon = document.getElementById('toggle-ip');
|
||
|
||
if (ipContainer && toggleIcon) {
|
||
|
||
toggleIcon.style.opacity = '0';
|
||
|
||
ipContainer.addEventListener('mouseenter', function() {
|
||
toggleIcon.style.opacity = '1';
|
||
});
|
||
|
||
ipContainer.addEventListener('mouseleave', function() {
|
||
toggleIcon.style.opacity = '0';
|
||
});
|
||
}
|
||
|
||
document.getElementById('toggle-ip').addEventListener('click', () => {
|
||
const ipElement = document.getElementById('ip-address');
|
||
const iconElement = document.getElementById('toggle-ip').querySelector('i');
|
||
|
||
if (ipElement.textContent === cachedIP) {
|
||
ipElement.textContent = '***.***.***.***.***';
|
||
iconElement.classList.remove('bi-eye');
|
||
iconElement.classList.add('bi-eye-slash');
|
||
localStorage.setItem("ipHidden", "true");
|
||
} else {
|
||
ipElement.textContent = cachedIP;
|
||
iconElement.classList.remove('bi-eye-slash');
|
||
iconElement.classList.add('bi-eye');
|
||
localStorage.setItem("ipHidden", "false");
|
||
}
|
||
});
|
||
|
||
} catch (error) {
|
||
console.error("Error in updateUI:", error);
|
||
document.getElementById('d-ip').innerHTML = langData[currentLang]['connection_timeout'];
|
||
$("#flag").attr("src", "./assets/neko/flags/mo.png");
|
||
}
|
||
},
|
||
|
||
showDetailModal: async () => {
|
||
const data = IP.lastGeoData;
|
||
if (!data) return;
|
||
|
||
const translatedCountry = await translateText(data.country, currentLang) || data.country || langData[currentLang]['unknown'];
|
||
const translatedRegion = await translateText(data.region, currentLang) || data.region || "";
|
||
const translatedCity = await translateText(data.city, currentLang) || data.city || "";
|
||
const translatedIsp = await translateText(data.isp, currentLang) || data.isp || "";
|
||
const translatedAsnOrganization = await translateText(data.asn_organization, currentLang) || data.asn_organization || "";
|
||
|
||
let country = translatedCountry;
|
||
let region = translatedRegion;
|
||
let city = translatedCity;
|
||
let isp = translatedIsp;
|
||
let asnOrganization = translatedAsnOrganization;
|
||
let timezone = data.timezone || "";
|
||
let asn = data.asn || "";
|
||
|
||
let areaParts = [country, region, city].filter(Boolean);
|
||
|
||
if (region && city && region === city) {
|
||
areaParts = [country, region];
|
||
}
|
||
|
||
if (country && region && country === region) {
|
||
areaParts = [country];
|
||
}
|
||
|
||
if (country && city && country === city) {
|
||
areaParts = [country];
|
||
}
|
||
let areaDisplay = areaParts.join(" ");
|
||
|
||
const pingResults = await checkAllPings();
|
||
const delayInfoHTML = Object.entries(pingResults).map(([key, { name, pingTime }]) => {
|
||
let color = '#ff6b6b';
|
||
if (typeof pingTime === 'number') {
|
||
color = pingTime <= 300 ? '#09B63F' : pingTime <= 700 ? '#FFA500' : '#ff6b6b';
|
||
}
|
||
return `<span style="margin-right: 20px; font-size: 18px; color: ${color};">${name}: ${pingTime === '超时' ? langData[currentLang]['timeout'] : `${pingTime}ms`}</span>`;
|
||
}).join('');
|
||
|
||
let lat = data.latitude || null;
|
||
let lon = data.longitude || null;
|
||
|
||
if (!lat || !lon) {
|
||
try {
|
||
const response = await fetch(`https://ipapi.co/${cachedIP}/json/`);
|
||
const geoData = await response.json();
|
||
lat = geoData.latitude;
|
||
lon = geoData.longitude;
|
||
} catch (error) {
|
||
console.error(langData[currentLang]['geo_location_error'], error);
|
||
}
|
||
}
|
||
|
||
const modalHTML = `
|
||
<div class="modal fade custom-modal" id="ipDetailModal" tabindex="-1" role="dialog" aria-labelledby="ipDetailModalLabel" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
|
||
<div class="modal-dialog modal-xl draggable" role="document">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h5 class="modal-title" id="ipDetailModalLabel">${translations['ip_info']}</h5>
|
||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||
<span aria-hidden="true">×</span>
|
||
</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['ip_address']}:</span>
|
||
<span class="detail-value">${cachedIP}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['location']}:</span>
|
||
<span class="detail-value">${areaDisplay}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['isp']}:</span>
|
||
<span class="detail-value">${isp}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['asn']}:</span>
|
||
<span class="detail-value">${asn} ${asnOrganization}</span>
|
||
</div>
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['timezone']}:</span>
|
||
<span class="detail-value">${timezone}</span>
|
||
</div>
|
||
${lat && lon ? `
|
||
<div class="detail-row">
|
||
<span class="detail-label">${translations['latitude_longitude']}:</span>
|
||
<span class="detail-value">${lat}, ${lon}</span>
|
||
</div>
|
||
<div class="detail-row" style="height: 400px; margin-top: 20px;">
|
||
<div id="leafletMap" style="width: 100%; height: 100%;"></div>
|
||
</div>` : ''}
|
||
<h5 style="margin-top: 15px;">${translations['latency_info']}:</h5>
|
||
<div class="detail-row" style="display: flex; flex-wrap: wrap;">
|
||
${delayInfoHTML}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
|
||
$('#ipDetailModal').remove();
|
||
$('body').append(modalHTML);
|
||
$('#ipDetailModal').modal('show');
|
||
|
||
if (lat && lon) {
|
||
setTimeout(() => {
|
||
if (window._leafletMap) {
|
||
window._leafletMap.remove();
|
||
}
|
||
window._leafletMap = L.map('leafletMap').setView([lat, lon], 10);
|
||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||
attribution: ''
|
||
}).addTo(window._leafletMap);
|
||
const areaDisplayForPopup = [country, region].filter(Boolean).join(" ");
|
||
L.marker([lat, lon])
|
||
.addTo(window._leafletMap)
|
||
.bindPopup(areaDisplay)
|
||
.openPopup();
|
||
try {
|
||
if (typeof L.Control.FullScreen !== 'undefined') {
|
||
const fullscreenControl = new L.Control.FullScreen({
|
||
position: 'topright',
|
||
title: 'Full Screen',
|
||
titleCancel: 'Exit Full Screen',
|
||
content: null
|
||
});
|
||
window._leafletMap.addControl(fullscreenControl);
|
||
console.log('FullScreen control added:', fullscreenControl);
|
||
} else {
|
||
console.error('FullScreen control plugin is not loaded.');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error adding FullScreen control:', error);
|
||
}
|
||
}, 200);
|
||
}
|
||
},
|
||
|
||
getIpipnetIP: async () => {
|
||
if (IP.isRefreshing) return;
|
||
|
||
try {
|
||
IP.isRefreshing = true;
|
||
document.getElementById('d-ip').innerHTML = `
|
||
<div class="ip-main">
|
||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||
${langData[currentLang]['checking']}
|
||
</div>
|
||
`;
|
||
document.getElementById('ipip').innerHTML = "";
|
||
$("#flag").attr("src", _IMG + "img/loading.svg");
|
||
|
||
const ip = await IP.fetchIP();
|
||
await IP.Ipip(ip, 'ipip');
|
||
} catch (error) {
|
||
console.error("Error in getIpipnetIP function:", error);
|
||
document.getElementById('ipip').innerHTML = langData[currentLang]['ip_info_fail'];
|
||
} finally {
|
||
IP.isRefreshing = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const style = document.createElement('style');
|
||
style.textContent = `
|
||
.ip-main {
|
||
font-size: 15px;
|
||
padding: 5px;
|
||
transition: all 0.3s;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.badge-primary {
|
||
color: #ff69b4 !important;
|
||
background-color: #f8f9fa !important;
|
||
border: 1px solid #dee2e6;
|
||
}
|
||
|
||
#ipip {
|
||
margin-left: -3px;
|
||
font-size: 1rem;
|
||
color: rgb(255, 0, 255) !important;
|
||
}
|
||
|
||
.ip-main:hover {
|
||
background: #f0f0f0;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.ip-details {
|
||
font-size: 18px !important;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.detail-row {
|
||
display: flex;
|
||
margin-bottom: 10px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.detail-label {
|
||
flex: 0 0 200px;
|
||
text-align: left;
|
||
font-weight: 500;
|
||
padding-right: 18px;
|
||
}
|
||
|
||
.detail-value {
|
||
flex: 1;
|
||
text-align: left;
|
||
word-break: break-all;
|
||
margin-left: 0;
|
||
}
|
||
`;
|
||
document.head.appendChild(style);
|
||
IP.getIpipnetIP();
|
||
if(typeof checkSiteStatus !== 'undefined') {
|
||
checkSiteStatus.check();
|
||
setInterval(() => checkSiteStatus.check(), 30000);
|
||
}
|
||
|
||
setInterval(IP.getIpipnetIP, 180000);
|
||
</script>
|
||
|
||
<script>
|
||
function handleIPClick() {
|
||
if (window.innerWidth <= 768) {
|
||
return;
|
||
}
|
||
IP.showDetailModal();
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
function toggleControlPanel() {
|
||
const overlay = document.getElementById('controlPanelOverlay');
|
||
if (overlay.style.display === 'none' || overlay.style.display === '') {
|
||
overlay.style.display = 'flex';
|
||
} else {
|
||
overlay.style.display = 'none';
|
||
}
|
||
}
|
||
|
||
const colorPicker = document.getElementById('colorPicker');
|
||
const controlPanel = document.querySelector('.control-panel');
|
||
|
||
colorPicker.addEventListener('input', function() {
|
||
const color = this.value;
|
||
controlPanel.style.background = `linear-gradient(135deg, ${color}, #1a508b, #2d8bc0)`;
|
||
});
|
||
|
||
document.querySelectorAll('.panel-btn, .panel-close-btn, .close-icon').forEach(button => {
|
||
button.addEventListener('mousedown', function() {
|
||
this.style.transform = 'translateY(0)';
|
||
});
|
||
|
||
button.addEventListener('mouseup', function() {
|
||
this.style.transform = this.classList.contains('panel-close-btn') || this.classList.contains('close-icon') ?
|
||
'translateY(-3px)' : 'translateY(0)';
|
||
});
|
||
|
||
button.addEventListener('mouseleave', function() {
|
||
this.style.transform = 'translateY(0)';
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.ctrlKey && event.shiftKey && event.code === 'KeyC') {
|
||
clearCache();
|
||
event.preventDefault();
|
||
}
|
||
});
|
||
|
||
document.getElementById('clear-cache-btn').addEventListener('click', function() {
|
||
clearCache();
|
||
});
|
||
|
||
const notificationMessage = translations['cache_cleared_notification'];
|
||
const speechMessage = translations['cache_cleared_speech'];
|
||
|
||
function clearCache() {
|
||
location.reload(true);
|
||
|
||
localStorage.clear();
|
||
sessionStorage.clear();
|
||
|
||
sessionStorage.setItem('cacheCleared', 'true');
|
||
|
||
showLogMessage(translations['notificationMessage']);
|
||
speakMessage(translations['notificationMessage']);
|
||
}
|
||
|
||
window.addEventListener('load', function() {
|
||
if (sessionStorage.getItem('cacheCleared') === 'true') {
|
||
showLogMessage(translations['notificationMessage']);
|
||
speakMessage(translations['notificationMessage']);
|
||
sessionStorage.removeItem('cacheCleared');
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
const bars = document.querySelectorAll('.bar');
|
||
function synthWave() {
|
||
const time = Date.now() / 1000;
|
||
|
||
bars.forEach((bar, i) => {
|
||
const frequency = 0.5 + i * 0.2;
|
||
const amplitude = Math.sin(time * frequency) * 50 + 50;
|
||
const height = amplitude * (0.8 + Math.random() * 0.2);
|
||
|
||
bar.style.height = `${Math.max(5, height)}%`;
|
||
bar.style.backgroundColor = `hsl(${(time * 50 + i * 30) % 360}, 80%, 60%)`;
|
||
});
|
||
|
||
requestAnimationFrame(synthWave);
|
||
}
|
||
synthWave();
|
||
</script>
|
||
|
||
<script>
|
||
function toggleFloatingLyrics() {
|
||
const lyrics = document.getElementById('floatingLyrics');
|
||
const isHidden = lyrics.style.display === 'none';
|
||
lyrics.style.display = isHidden ? '' : 'none';
|
||
localStorage.setItem('floatingLyricsVisible', isHidden ? 'true' : 'false');
|
||
|
||
document.querySelectorAll('.toggleFloatingLyricsBtn i').forEach(icon => {
|
||
icon.className = isHidden ? 'bi bi-display-fill floatingIcon' : 'bi bi-display floatingIcon';
|
||
});
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const lyrics = document.getElementById('floatingLyrics');
|
||
const isVisible = localStorage.getItem('floatingLyricsVisible') === 'true';
|
||
|
||
lyrics.style.display = isVisible ? '' : 'none';
|
||
|
||
document.querySelectorAll('.toggleFloatingLyricsBtn i').forEach(icon => {
|
||
icon.className = isVisible ? 'bi bi-display-fill floatingIcon' : 'bi bi-display floatingIcon';
|
||
});
|
||
|
||
document.querySelectorAll('.toggleFloatingLyricsBtn').forEach(btn => {
|
||
btn.addEventListener('click', toggleFloatingLyrics);
|
||
});
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
const audioPlayer = new Audio();
|
||
let songs = JSON.parse(localStorage.getItem('cachedPlaylist') || '[]');
|
||
let currentTrackIndex = JSON.parse(localStorage.getItem('currentTrackIndex') || '0');
|
||
let isPlaying = JSON.parse(localStorage.getItem('isPlaying') || 'false');
|
||
let repeatMode = JSON.parse(localStorage.getItem('repeatMode') || '0');
|
||
let isHovering = false;
|
||
let isManualScroll = false;
|
||
let isSmallScreen = window.innerWidth < 768;
|
||
|
||
const showLogMessage = (function() {
|
||
let currentIndex = 0;
|
||
const activeLogs = new Set();
|
||
const BASE_OFFSET = 20;
|
||
const MARGIN = 10;
|
||
|
||
function createIcon(type) {
|
||
const icons = {
|
||
error: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z',
|
||
warning: 'M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z',
|
||
info: 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z'
|
||
};
|
||
|
||
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="#fff" d="${icons[type] || icons.info}"/></svg>`;
|
||
|
||
return `data:image/svg+xml;base64,${btoa(svg)}`;
|
||
}
|
||
|
||
function updatePositions() {
|
||
let verticalPos = BASE_OFFSET;
|
||
activeLogs.forEach(log => {
|
||
log.style.transform = `translateY(${verticalPos}px)`;
|
||
verticalPos += log.offsetHeight + MARGIN;
|
||
});
|
||
}
|
||
|
||
return function(message, type = '') {
|
||
const logBox = document.createElement('div');
|
||
logBox.className = `log-box ${type}`;
|
||
|
||
if (!type) {
|
||
logBox.dataset.dynamicBg = 'true';
|
||
}
|
||
|
||
logBox.innerHTML = `
|
||
<div class="log-content">
|
||
<span class="log-icon" style="background-image:url('${createIcon(type)}')"></span>
|
||
${decodeURIComponent(message)}
|
||
<button class="log-close-btn">×</button>
|
||
</div>
|
||
`;
|
||
|
||
logBox.querySelector('.log-close-btn').onclick = () => {
|
||
logBox.classList.add('exiting');
|
||
setTimeout(() => {
|
||
logBox.remove();
|
||
activeLogs.delete(logBox);
|
||
updatePositions();
|
||
}, 300);
|
||
};
|
||
|
||
logBox.addEventListener('mouseenter', () =>
|
||
logBox.style.animationPlayState = 'paused');
|
||
logBox.addEventListener('mouseleave', () =>
|
||
logBox.style.animationPlayState = 'running');
|
||
|
||
document.body.appendChild(logBox);
|
||
activeLogs.add(logBox);
|
||
|
||
requestAnimationFrame(() => {
|
||
logBox.classList.add('active');
|
||
updatePositions();
|
||
});
|
||
|
||
setTimeout(() => {
|
||
logBox.classList.add('exiting');
|
||
setTimeout(() => {
|
||
logBox.remove();
|
||
activeLogs.delete(logBox);
|
||
updatePositions();
|
||
}, 300);
|
||
}, 12000);
|
||
};
|
||
})();
|
||
|
||
function togglePlay() {
|
||
if (isPlaying) {
|
||
audioPlayer.pause();
|
||
const pauseMessage = translations['pause_playing'] || 'Pause_Playing';
|
||
showLogMessage(pauseMessage);
|
||
speakMessage(pauseMessage);
|
||
} else {
|
||
audioPlayer.play();
|
||
const playMessage = translations['start_playing'] || 'Start_Playing';
|
||
showLogMessage(playMessage);
|
||
speakMessage(playMessage);
|
||
}
|
||
isPlaying = !isPlaying;
|
||
updatePlayButton();
|
||
savePlayerState();
|
||
|
||
const btn = event.target.closest('button');
|
||
if(btn) {
|
||
btn.classList.add('clicked');
|
||
setTimeout(() => btn.classList.remove('clicked'), 200);
|
||
}
|
||
}
|
||
|
||
function updatePlayButton() {
|
||
const btn = document.getElementById('playPauseBtn');
|
||
const floatingBtn = document.getElementById('floatingPlayBtn');
|
||
const icon = isPlaying ? 'bi-pause-fill' : 'bi-play-fill';
|
||
|
||
btn.innerHTML = `<i class="bi ${icon}"></i>`;
|
||
floatingBtn.innerHTML = `<i class="bi ${icon}"></i>`;
|
||
}
|
||
|
||
function changeTrack(direction, isManual = false) {
|
||
const oldSong = songs[currentTrackIndex];
|
||
|
||
if (repeatMode === 2 && !isManual) {
|
||
currentTrackIndex = Math.floor(Math.random() * songs.length);
|
||
} else {
|
||
currentTrackIndex = (currentTrackIndex + direction + songs.length) % songs.length;
|
||
}
|
||
|
||
const songName = decodeURIComponent(
|
||
songs[currentTrackIndex].split('/').pop().replace(/\.\w+$/, '')
|
||
);
|
||
|
||
if (isManual) {
|
||
const action = direction === -1
|
||
? translations['previous_track'] || 'Previous Track'
|
||
: translations['next_track'] || 'Next Track';
|
||
showLogMessage(`${translations['manual_switch'] || 'Manual Switch'}${action}:${songName}`);
|
||
speakMessage(`${translations['switch_to'] || 'Switch to'}${action}:${songName}`);
|
||
} else {
|
||
showLogMessage(`${translations['auto_switch'] || 'Auto Switch to'}:${songName}`);
|
||
speakMessage(`${translations['auto_play'] || 'Auto Play'}:${songName}`);
|
||
}
|
||
|
||
loadTrack(songs[currentTrackIndex]);
|
||
}
|
||
|
||
function toggleRepeat() {
|
||
repeatMode = (repeatMode + 1) % 3;
|
||
const mainBtn = document.getElementById('repeatBtn');
|
||
const floatingBtn = document.getElementById('floatingRepeatBtn');
|
||
|
||
[mainBtn, floatingBtn].forEach(btn => {
|
||
btn.classList.remove('btn-success', 'btn-warning');
|
||
btn.title = [
|
||
translations['order_play'] || 'Order_Play',
|
||
translations['single_loop'] || 'Single_Loop',
|
||
translations['shuffle_play'] || 'Shuffle_Play'
|
||
][repeatMode];
|
||
|
||
switch (repeatMode) {
|
||
case 0:
|
||
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i>';
|
||
break;
|
||
case 1:
|
||
btn.innerHTML = '<i class="bi bi-repeat-1"></i>';
|
||
btn.classList.add('btn-success');
|
||
break;
|
||
case 2:
|
||
btn.innerHTML = '<i class="bi bi-shuffle"></i>';
|
||
btn.classList.add('btn-warning');
|
||
break;
|
||
}
|
||
});
|
||
|
||
showLogMessage([
|
||
translations['order_play'] || 'Order_Play',
|
||
translations['single_loop'] || 'Single_Loop',
|
||
translations['shuffle_play'] || 'Shuffle_Play'
|
||
][repeatMode]);
|
||
speakMessage([
|
||
translations['order_play'] || 'Order_Play',
|
||
translations['single_loop'] || 'Single_Loop',
|
||
translations['shuffle_play'] || 'Shuffle_Play'
|
||
][repeatMode]);
|
||
savePlayerState();
|
||
}
|
||
|
||
function updatePlaylistUI() {
|
||
const playlist = document.getElementById('playlist');
|
||
playlist.innerHTML = songs.map((url, index) => `
|
||
<div class="playlist-item ${index === currentTrackIndex ? 'active' : ''}"
|
||
onclick="playTrack(${index})">
|
||
${decodeURIComponent(url.split('/').pop().replace(/\.\w+$/, ''))}
|
||
</div>
|
||
`).join('');
|
||
showLogMessage(`Playlist loaded: ${songs.length} songs`);
|
||
setTimeout(() => scrollToCurrentTrack(), 100);
|
||
}
|
||
|
||
function updatePlaylist() {
|
||
const btn = document.getElementById('updatePlaylistBtn');
|
||
const originalHTML = btn.innerHTML;
|
||
btn.innerHTML = '<i class="bi bi-arrow-repeat spinning"></i>';
|
||
|
||
fetch('<?php echo $new_url; ?>')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
const newSongs = data.split('\n')
|
||
.filter(url => url.trim());
|
||
|
||
songs = newSongs;
|
||
localStorage.setItem('cachedPlaylist', JSON.stringify(songs));
|
||
|
||
if (currentTrackIndex >= songs.length) {
|
||
currentTrackIndex = 0;
|
||
}
|
||
|
||
updatePlaylistUI();
|
||
|
||
if (songs.length > 0 && songs[currentTrackIndex]) {
|
||
loadTrack(songs[currentTrackIndex]);
|
||
}
|
||
|
||
btn.innerHTML = originalHTML;
|
||
|
||
const successMsg = translations['playlist_updated']
|
||
? translations['playlist_updated']
|
||
: 'Playlist updated successfully';
|
||
const songCountMsg = translations['song_count']
|
||
? translations['song_count'].replace('{count}', songs.length)
|
||
: `Total ${songs.length} songs`;
|
||
|
||
showLogMessage(`${successMsg},${songCountMsg}`);
|
||
speakMessage(`${successMsg},${songCountMsg}`);
|
||
})
|
||
.catch(error => {
|
||
console.error('Playlist update failed:', error);
|
||
|
||
btn.innerHTML = '<i class="bi bi-x-circle"></i>';
|
||
setTimeout(() => {
|
||
btn.innerHTML = originalHTML;
|
||
}, 1000);
|
||
|
||
const errorMsg = translations['update_failed']
|
||
? translations['update_failed']
|
||
: 'Playlist update failed';
|
||
showLogMessage(errorMsg, 'error');
|
||
speakMessage(errorMsg);
|
||
});
|
||
}
|
||
|
||
function playTrack(index) {
|
||
const songName = decodeURIComponent(songs[index].split('/').pop().replace(/\.\w+$/, ''));
|
||
const message = `${translations['playlist_click'] || 'Playlist Click'}:${translations['index'] || 'Index'}:${index + 1},${translations['song_name'] || 'Song Name'}:${songName}`;
|
||
audioPlayer.pause();
|
||
currentTrackIndex = index;
|
||
loadTrack(songs[index]);
|
||
|
||
isPlaying = true;
|
||
audioPlayer.play().catch((error) => {
|
||
isPlaying = false;
|
||
});
|
||
|
||
updatePlayButton();
|
||
savePlayerState();
|
||
showLogMessage(message);
|
||
speakMessage(message);
|
||
|
||
event.target.classList.add('clicked');
|
||
setTimeout(() => event.target.classList.remove('clicked'), 200);
|
||
}
|
||
|
||
function scrollToCurrentTrack() {
|
||
const playlist = document.getElementById('playlist');
|
||
const activeItem = playlist.querySelector('.playlist-item.active');
|
||
if (activeItem) {
|
||
activeItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||
}
|
||
}
|
||
|
||
function loadLyrics(songUrl) {
|
||
const lyricsUrl = songUrl.replace(/\.\w+$/, '.lrc');
|
||
|
||
window.lyrics = {};
|
||
window.lyricTimes = [];
|
||
|
||
const containers = [
|
||
document.getElementById('lyricsContainer'),
|
||
document.querySelector('#floatingLyrics .vertical-lyrics')
|
||
];
|
||
|
||
containers.forEach(container => {
|
||
const message = translations['loading_lyrics'] || 'Loading Lyrics...';
|
||
const verticalText = message.split('').map(char => `<span class="char">${char}</span>`).join('');
|
||
|
||
const statusMsg = container.id === 'lyricsContainer'
|
||
? `<div id="no-lyrics">${message}</div>`
|
||
: `<div id="noLyricsFloating" class="vertical-message">${verticalText}</div>`;
|
||
|
||
container.innerHTML = statusMsg;
|
||
});
|
||
|
||
fetch(lyricsUrl)
|
||
.then(response => {
|
||
if (!response.ok) {
|
||
if (response.status === 404) {
|
||
throw new Error('LYRICS_NOT_FOUND');
|
||
} else {
|
||
throw new Error(`HTTP error! status: ${response.status}`);
|
||
}
|
||
}
|
||
return response.arrayBuffer();
|
||
})
|
||
.then(buffer => {
|
||
const decoder = new TextDecoder('utf-8');
|
||
parseLyrics(decoder.decode(buffer));
|
||
displayLyrics();
|
||
document.dispatchEvent(new Event('lyricsLoaded'));
|
||
})
|
||
.catch(error => {
|
||
if (error.message === 'LYRICS_NOT_FOUND') {
|
||
containers.forEach(container => {
|
||
if (container.id === 'lyricsContainer') {
|
||
container.innerHTML = `<div id="no-lyrics">${translations['no_lyrics'] || 'No Lyrics Available'}</div>`;
|
||
} else {
|
||
const message = translations['no_lyrics'] || 'No Lyrics Available';
|
||
const verticalText = message.split('').map(char => `<span class="char">${char}</span>`).join('');
|
||
container.innerHTML = `<div id="noLyricsFloating" class="vertical-text">${verticalText}</div>`;
|
||
}
|
||
});
|
||
} else {
|
||
console.error(`${translations['lyrics_load_failed'] || 'Lyrics Load Failed'}:`, error);
|
||
containers.forEach(container => {
|
||
if (container.id === 'lyricsContainer') {
|
||
container.innerHTML = `<div id="no-lyrics">${translations['lyrics_load_failed'] || 'Failed to load lyrics'}</div>`;
|
||
} else {
|
||
const message = translations['lyrics_load_failed'] || 'Failed to load lyrics';
|
||
const verticalText = message.split('').map(char => `<span class="char">${char}</span>`).join('');
|
||
container.innerHTML = `<div id="noLyricsFloating" class="vertical-message">${verticalText}</div>`;
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
function parseLyrics(text) {
|
||
window.lyrics = {};
|
||
window.lyricTimes = [];
|
||
const regex = /\[(\d+):(\d+)\.(\d+)\](.+)/g;
|
||
let match;
|
||
while ((match = regex.exec(text)) !== null) {
|
||
const time = parseInt(match[1]) * 60 + parseInt(match[2]) + parseInt(match[3])/1000;
|
||
const content = match[4].replace(/\[\d+:\d+\.\d+\]/g, '').trim();
|
||
lyrics[time] = content;
|
||
lyricTimes.push(time);
|
||
}
|
||
lyricTimes.sort((a, b) => a - b);
|
||
}
|
||
|
||
function tokenize(text) {
|
||
const tokens = [];
|
||
let currentWord = '';
|
||
|
||
for (const char of text) {
|
||
if (/\s/.test(char)) {
|
||
if (currentWord) {
|
||
tokens.push(currentWord);
|
||
currentWord = '';
|
||
}
|
||
tokens.push({ type: 'space', value: char });
|
||
continue;
|
||
}
|
||
|
||
if (/[-–—]/.test(char)) {
|
||
if (currentWord) {
|
||
tokens.push(currentWord);
|
||
currentWord = '';
|
||
}
|
||
tokens.push({ type: 'punctuation', value: char });
|
||
continue;
|
||
}
|
||
|
||
if (/[a-zA-Z0-9]/.test(char)) {
|
||
currentWord += char;
|
||
} else {
|
||
if (currentWord) {
|
||
tokens.push(currentWord);
|
||
currentWord = '';
|
||
}
|
||
tokens.push({ type: 'char', value: char });
|
||
}
|
||
}
|
||
|
||
if (currentWord) tokens.push(currentWord);
|
||
return tokens;
|
||
}
|
||
|
||
function createCharSpans(text, startTime, endTime) {
|
||
const tokens = tokenize(text);
|
||
const totalDuration = endTime - startTime;
|
||
const charCount = text.replace(/\s/g, '').length;
|
||
const durationPerChar = totalDuration / charCount;
|
||
|
||
let charIndex = 0;
|
||
const spans = [];
|
||
|
||
tokens.forEach(token => {
|
||
if (typeof token === 'string') {
|
||
const wordSpan = document.createElement('span');
|
||
wordSpan.className = 'word';
|
||
const letters = token.split('');
|
||
|
||
letters.forEach(letter => {
|
||
const span = document.createElement('span');
|
||
span.className = 'char';
|
||
span.textContent = letter;
|
||
span.dataset.start = startTime + charIndex * durationPerChar;
|
||
span.dataset.end = startTime + (charIndex + 1) * durationPerChar;
|
||
wordSpan.appendChild(span);
|
||
charIndex++;
|
||
});
|
||
|
||
spans.push(wordSpan);
|
||
} else if (token.type === 'space') {
|
||
const spaceSpan = document.createElement('span');
|
||
spaceSpan.className = 'char space';
|
||
spaceSpan.innerHTML = ' ';
|
||
spans.push(spaceSpan);
|
||
} else if (token.type === 'punctuation') {
|
||
const punctSpan = document.createElement('span');
|
||
punctSpan.className = 'char punctuation';
|
||
punctSpan.textContent = token.value;
|
||
punctSpan.dataset.start = startTime + charIndex * durationPerChar;
|
||
punctSpan.dataset.end = startTime + (charIndex + 1) * durationPerChar;
|
||
spans.push(punctSpan);
|
||
charIndex++;
|
||
} else {
|
||
const span = document.createElement('span');
|
||
span.className = 'char';
|
||
span.textContent = token.value;
|
||
span.dataset.start = startTime + charIndex * durationPerChar;
|
||
span.dataset.end = startTime + (charIndex + 1) * durationPerChar;
|
||
spans.push(span);
|
||
charIndex++;
|
||
}
|
||
});
|
||
|
||
return spans;
|
||
}
|
||
|
||
function isEnglishWord(text) {
|
||
return /^[a-zA-Z']+$/.test(text);
|
||
}
|
||
|
||
function createCharSpans(text, startTime, endTime) {
|
||
const elements = [];
|
||
const words = text.split(/(\s+)/);
|
||
|
||
let currentTime = startTime;
|
||
const totalDuration = endTime - startTime;
|
||
const validWords = words.filter(word => word.trim().length > 0);
|
||
const durationPerWord = totalDuration / validWords.length;
|
||
|
||
words.forEach(word => {
|
||
if (word.trim().length === 0) return;
|
||
|
||
const isEnglish = isEnglishWord(word.replace(/[^a-zA-Z']/g, ''));
|
||
const span = document.createElement('span');
|
||
span.className = 'char';
|
||
|
||
if (isEnglish) {
|
||
span.textContent = word;
|
||
span.dataset.start = currentTime;
|
||
span.dataset.end = currentTime + durationPerWord;
|
||
currentTime += durationPerWord;
|
||
} else {
|
||
const chars = word.split('');
|
||
const charDuration = durationPerWord / chars.length;
|
||
chars.forEach((char, index) => {
|
||
const charSpan = document.createElement('span');
|
||
charSpan.className = 'char';
|
||
charSpan.textContent = char;
|
||
charSpan.dataset.start = currentTime + index * charDuration;
|
||
charSpan.dataset.end = currentTime + (index + 1) * charDuration;
|
||
elements.push(charSpan);
|
||
});
|
||
currentTime += durationPerWord;
|
||
return;
|
||
}
|
||
|
||
elements.push(span);
|
||
});
|
||
|
||
return elements;
|
||
}
|
||
|
||
function displayLyrics() {
|
||
const lyricsContainer = document.getElementById('lyricsContainer');
|
||
const floatingLyrics = document.querySelector('#floatingLyrics .vertical-lyrics');
|
||
|
||
lyricsContainer.innerHTML = '';
|
||
floatingLyrics.innerHTML = '';
|
||
|
||
if (Object.keys(window.lyrics).length === 0) {
|
||
const message = translations['no_lyrics'] || 'No Lyrics Available';
|
||
const verticalText = message.split('').map(char => `<span class="char">${char}</span>`).join('');
|
||
|
||
lyricsContainer.innerHTML = `<div id="no-lyrics">${message}</div>`;
|
||
floatingLyrics.innerHTML = `<div id="noLyricsFloating" class="vertical-message">${verticalText}</div>`;
|
||
return;
|
||
}
|
||
|
||
lyricTimes.forEach((time, index) => {
|
||
const line = document.createElement('div');
|
||
line.className = 'lyric-line';
|
||
line.dataset.time = time;
|
||
|
||
const endTime = index < lyricTimes.length - 1
|
||
? lyricTimes[index + 1]
|
||
: time + 3;
|
||
|
||
const elements = createCharSpans(lyrics[time], time, endTime);
|
||
elements.forEach(element => line.appendChild(element));
|
||
lyricsContainer.appendChild(line);
|
||
});
|
||
|
||
audioPlayer.addEventListener('timeupdate', syncLyrics);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const lyricsContainer = document.getElementById('lyricsContainer');
|
||
|
||
lyricsContainer.addEventListener('mouseenter', () => {
|
||
isHovering = true;
|
||
});
|
||
|
||
lyricsContainer.addEventListener('mouseleave', () => {
|
||
isHovering = false;
|
||
isManualScroll = false;
|
||
});
|
||
|
||
lyricsContainer.addEventListener('scroll', () => {
|
||
if (isHovering) {
|
||
isManualScroll = true;
|
||
setTimeout(() => {
|
||
isManualScroll = false;
|
||
}, 3000);
|
||
}
|
||
});
|
||
|
||
loadPlayerState();
|
||
updatePlaylistUI();
|
||
});
|
||
|
||
function syncLyrics() {
|
||
const currentTime = audioPlayer.currentTime;
|
||
const lyricsContainer = document.getElementById('lyricsContainer');
|
||
const lines = lyricsContainer.querySelectorAll('.lyric-line');
|
||
let currentLine = null;
|
||
let hasActiveLine = false;
|
||
|
||
lines.forEach(line => {
|
||
line.classList.remove('highlight', 'played');
|
||
line.style.color = 'white';
|
||
});
|
||
|
||
for (let i = lines.length - 1; i >= 0; i--) {
|
||
const line = lines[i];
|
||
const lineTime = parseFloat(line.dataset.time);
|
||
if (currentTime >= lineTime) {
|
||
line.classList.add('highlight');
|
||
currentLine = line;
|
||
hasActiveLine = true;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (currentLine) {
|
||
const chars = currentLine.querySelectorAll('.char');
|
||
chars.forEach(char => {
|
||
const start = parseFloat(char.dataset.start);
|
||
const end = parseFloat(char.dataset.end);
|
||
|
||
if (currentTime >= start && currentTime <= end) {
|
||
char.classList.add('active');
|
||
} else if (currentTime > end && !char.classList.contains('played')) {
|
||
char.classList.add('played');
|
||
spawnHeartAbove(char);
|
||
}
|
||
});
|
||
|
||
const floatingContainer = document.getElementById('floatingLyrics');
|
||
const floatingLyrics = floatingContainer.querySelector('.vertical-lyrics');
|
||
if (!floatingLyrics.innerHTML || currentLine.dataset.time !== floatingLyrics.dataset.time) {
|
||
floatingLyrics.innerHTML = currentLine.innerHTML;
|
||
floatingLyrics.dataset.time = currentLine.dataset.time;
|
||
floatingLyrics.classList.add('enter-active');
|
||
setTimeout(() => floatingLyrics.classList.remove('enter-active'), 500);
|
||
}
|
||
|
||
const floatingChars = floatingLyrics.querySelectorAll('.char');
|
||
chars.forEach((char, index) => {
|
||
const floatingChar = floatingChars[index];
|
||
if (!floatingChar) return;
|
||
|
||
const start = parseFloat(char.dataset.start);
|
||
const end = parseFloat(char.dataset.end);
|
||
|
||
if (currentTime >= start && currentTime <= end) {
|
||
floatingChar.classList.add('active');
|
||
const progress = (currentTime - start) / (end - start);
|
||
floatingChar.style.transform = `scale(${1 + progress * 0.2})`;
|
||
} else {
|
||
floatingChar.classList.remove('active');
|
||
floatingChar.style.transform = '';
|
||
}
|
||
});
|
||
|
||
if (!isSmallScreen && !isHovering && !isManualScroll) {
|
||
const lineRect = currentLine.getBoundingClientRect();
|
||
const containerRect = lyricsContainer.getBoundingClientRect();
|
||
const targetPosition = lineRect.top - containerRect.top + lyricsContainer.scrollTop - (lyricsContainer.clientHeight / 2) + (lineRect.height / 2);
|
||
|
||
const buffer = 50;
|
||
if (lineRect.top < containerRect.top + buffer ||
|
||
lineRect.bottom > containerRect.bottom - buffer) {
|
||
lyricsContainer.scrollTo({ top: targetPosition, behavior: 'smooth' });
|
||
}
|
||
}
|
||
|
||
if (!hasActiveLine && lyricsContainer.scrollTop !== 0) {
|
||
lyricsContainer.scrollTo({ top: 0, behavior: 'smooth' });
|
||
}
|
||
}
|
||
}
|
||
|
||
function spawnHeartAbove(char) {
|
||
const heart = document.createElement('span');
|
||
heart.className = 'heart';
|
||
heart.textContent = '💖';
|
||
|
||
const rect = char.getBoundingClientRect();
|
||
const offsetTop = rect.top + window.scrollY;
|
||
const offsetLeft = rect.left + window.scrollX;
|
||
|
||
heart.style.left = `${offsetLeft + char.offsetWidth / 2}px`;
|
||
heart.style.top = `${offsetTop - 30}px`;
|
||
|
||
document.body.appendChild(heart);
|
||
|
||
requestAnimationFrame(() => {
|
||
heart.classList.add('pop');
|
||
});
|
||
|
||
setTimeout(() => {
|
||
heart.remove();
|
||
}, 1000);
|
||
}
|
||
|
||
function loadTrack(url) {
|
||
window.lyrics = {};
|
||
window.lyricTimes = [];
|
||
|
||
const lyricsContainers = [
|
||
document.getElementById('lyricsContainer'),
|
||
document.querySelector('#floatingLyrics .vertical-lyrics')
|
||
];
|
||
|
||
lyricsContainers.forEach(container => {
|
||
container.innerHTML = `<div class="no-lyrics">${translations['loading_lyrics'] || 'Loading Lyrics...'}</div>`;
|
||
});
|
||
|
||
audioPlayer.pause();
|
||
audioPlayer.src = url;
|
||
|
||
audioPlayer.load();
|
||
audioPlayer.addEventListener('canplaythrough', () => {
|
||
audioPlayer.play().catch((error) => {
|
||
console.log(`${translations['autoplay_blocked'] || 'Autoplay Blocked'}:`, error);
|
||
showLogMessage(translations['click_to_play'] || 'Click play button to start');
|
||
});
|
||
isPlaying = true;
|
||
updatePlayButton();
|
||
}, { once: true });
|
||
|
||
updatePlayButton();
|
||
updatePlaylistUI();
|
||
loadLyrics(url);
|
||
updateCurrentSong(url);
|
||
updateTimeDisplay();
|
||
|
||
savePlayerState();
|
||
}
|
||
|
||
function initializePlayer() {
|
||
audioPlayer.src = songs[currentTrackIndex] || '';
|
||
audioPlayer.currentTime = JSON.parse(localStorage.getItem('currentTime') || '0');
|
||
|
||
audioPlayer.addEventListener('loadedmetadata', () => {
|
||
loadLyrics(songs[currentTrackIndex]);
|
||
updateCurrentSong(songs[currentTrackIndex]);
|
||
});
|
||
|
||
updatePlayButton();
|
||
setRepeatButtonState();
|
||
updateTimeDisplay(true);
|
||
|
||
if (isPlaying) {
|
||
audioPlayer.play().catch((error) => {
|
||
console.log(`${translations['autoplay_blocked'] || 'Autoplay Blocked'}:`, error);
|
||
isPlaying = false;
|
||
saveCoreState();
|
||
updatePlayButton();
|
||
});
|
||
}
|
||
}
|
||
|
||
function saveCoreState() {
|
||
localStorage.setItem('cachedPlaylist', JSON.stringify(songs));
|
||
localStorage.setItem('currentTrackIndex', currentTrackIndex);
|
||
localStorage.setItem('isPlaying', isPlaying);
|
||
localStorage.setItem('repeatMode', repeatMode);
|
||
localStorage.setItem('currentTime', audioPlayer.currentTime);
|
||
}
|
||
|
||
function updateCurrentSong(url) {
|
||
const songName = decodeURIComponent(url.split('/').pop().replace(/\.\w+$/, ''));
|
||
document.getElementById('currentSong').textContent = songName;
|
||
|
||
const floatingTitle = document.querySelector('#floatingLyrics #floatingCurrentSong');
|
||
if (floatingTitle) floatingTitle.textContent = songName;
|
||
|
||
const modalTitle = document.querySelector('#musicModal #currentSong');
|
||
if (modalTitle) modalTitle.textContent = songName;
|
||
}
|
||
|
||
function updateTimeDisplay() {
|
||
const currentTimeElement = document.getElementById('currentTime');
|
||
const durationElement = document.getElementById('duration');
|
||
const progressBar = document.getElementById('progressBar');
|
||
|
||
audioPlayer.addEventListener('timeupdate', () => {
|
||
const currentTime = audioPlayer.currentTime;
|
||
const duration = audioPlayer.duration || 0;
|
||
const progress = (currentTime / duration) * 100 || 0;
|
||
|
||
currentTimeElement.textContent = formatTime(currentTime);
|
||
durationElement.textContent = formatTime(duration);
|
||
progressBar.style.width = `${progress}%`;
|
||
progressBar.setAttribute('aria-valuenow', progress);
|
||
});
|
||
|
||
progressBar.parentElement.addEventListener('click', (e) => {
|
||
const rect = progressBar.parentElement.getBoundingClientRect();
|
||
const x = e.clientX - rect.left;
|
||
const clickedPercent = (x / rect.width) * 100;
|
||
const newTime = (clickedPercent / 100) * audioPlayer.duration;
|
||
audioPlayer.currentTime = newTime;
|
||
});
|
||
}
|
||
|
||
function formatTime(seconds) {
|
||
const minutes = Math.floor(seconds / 60);
|
||
const remainingSeconds = Math.floor(seconds % 60);
|
||
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
|
||
}
|
||
|
||
audioPlayer.addEventListener('ended', () => {
|
||
if (repeatMode === 1) {
|
||
audioPlayer.play();
|
||
} else {
|
||
changeTrack(1);
|
||
}
|
||
});
|
||
|
||
function savePlayerState() {
|
||
localStorage.setItem('playerState', JSON.stringify({
|
||
isPlaying: isPlaying,
|
||
repeatMode: repeatMode,
|
||
currentTrackIndex: currentTrackIndex,
|
||
currentTime: audioPlayer.currentTime,
|
||
currentTrack: songs[currentTrackIndex]
|
||
}));
|
||
}
|
||
|
||
function loadPlayerState() {
|
||
const savedState = localStorage.getItem('playerState');
|
||
if (savedState) {
|
||
const state = JSON.parse(savedState);
|
||
isPlaying = state.isPlaying;
|
||
repeatMode = state.repeatMode;
|
||
currentTrackIndex = state.currentTrackIndex || 0;
|
||
|
||
if (state.currentTrack) {
|
||
audioPlayer.currentTime = state.currentTime;
|
||
setRepeatButtonState();
|
||
}
|
||
}
|
||
}
|
||
|
||
function setRepeatButtonState() {
|
||
const mainBtn = document.getElementById('repeatBtn');
|
||
const floatingBtn = document.getElementById('floatingRepeatBtn');
|
||
|
||
[mainBtn, floatingBtn].forEach(btn => {
|
||
btn.classList.remove('btn-success', 'btn-warning');
|
||
btn.title = [
|
||
translations['order_play'] || 'Order_Play',
|
||
translations['single_loop'] || 'Single_Loop',
|
||
translations['shuffle_play'] || 'Shuffle_Play'
|
||
][repeatMode];
|
||
|
||
switch(repeatMode) {
|
||
case 1:
|
||
btn.classList.add('btn-success');
|
||
btn.innerHTML = '<i class="bi bi-repeat-1"></i>';
|
||
break;
|
||
case 2:
|
||
btn.classList.add('btn-warning');
|
||
btn.innerHTML = '<i class="bi bi-shuffle"></i>';
|
||
break;
|
||
default:
|
||
btn.innerHTML = '<i class="bi bi-arrow-repeat"></i>';
|
||
}
|
||
});
|
||
}
|
||
|
||
function loadDefaultPlaylist() {
|
||
fetch('<?php echo $new_url; ?>')
|
||
.then(response => response.text())
|
||
.then(data => {
|
||
const newSongs = data.split('\n').filter(url => url.trim());
|
||
|
||
if (JSON.stringify(songs) !== JSON.stringify(newSongs)) {
|
||
songs = [...new Set([...songs, ...newSongs])];
|
||
currentTrackIndex = songs.findIndex(url => url === localStorage.getItem('currentTrack'));
|
||
if (currentTrackIndex === -1) currentTrackIndex = 0;
|
||
localStorage.setItem('cachedPlaylist', JSON.stringify(songs));
|
||
}
|
||
|
||
updatePlaylistUI();
|
||
initializePlayer();
|
||
|
||
if (songs[currentTrackIndex]) {
|
||
loadLyrics(songs[currentTrackIndex]);
|
||
updateCurrentSong(songs[currentTrackIndex]);
|
||
}
|
||
})
|
||
.catch(error => console.error('Playlist loading failed:', error));
|
||
}
|
||
|
||
function updatePlaylistUI() {
|
||
const playlist = document.getElementById('playlist');
|
||
playlist.innerHTML = songs.map((url, index) => `
|
||
<div class="playlist-item
|
||
${index === currentTrackIndex ? 'active' : ''}
|
||
${!isPlaying && index === currentTrackIndex ? 'paused' : ''}"
|
||
onclick="playTrack(${index})">
|
||
${decodeURIComponent(url.split('/').pop().replace(/\.\w+$/, ''))}
|
||
</div>
|
||
`).join('');
|
||
setTimeout(() => {
|
||
const activeItem = playlist.querySelector('.active');
|
||
if (activeItem) {
|
||
const itemTop = activeItem.offsetTop;
|
||
const itemHeight = activeItem.offsetHeight;
|
||
const containerHeight = playlist.offsetHeight;
|
||
|
||
if (itemTop < playlist.scrollTop ||
|
||
itemTop + itemHeight > playlist.scrollTop + containerHeight) {
|
||
activeItem.scrollIntoView({
|
||
behavior: 'smooth',
|
||
block: 'center',
|
||
inline: 'nearest'
|
||
});
|
||
}
|
||
|
||
activeItem.style.animation = 'none';
|
||
}
|
||
}, 300);
|
||
}
|
||
|
||
loadDefaultPlaylist();
|
||
window.addEventListener('resize', () => {
|
||
isSmallScreen = window.innerWidth < 768;
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
const muteToggle = document.getElementById('muteToggle');
|
||
const volumeToggle = document.getElementById('volumeToggle');
|
||
const volumePanel = document.getElementById('volumePanel');
|
||
const volumeSlider = document.getElementById('volumeSlider');
|
||
const muteIconEl = muteToggle.querySelector('i');
|
||
const volumeIconEl = volumeToggle.querySelector('i');
|
||
|
||
let lastVolume = 1;
|
||
|
||
const savedVolume = localStorage.getItem('audioVolume');
|
||
const savedMuted = localStorage.getItem('audioMuted');
|
||
if (savedVolume !== null) {
|
||
lastVolume = parseFloat(savedVolume);
|
||
}
|
||
audioPlayer.volume = lastVolume;
|
||
volumeSlider.value = lastVolume;
|
||
audioPlayer.muted = (savedMuted === 'true');
|
||
|
||
updateVolumeIcon();
|
||
|
||
function togglePanel() {
|
||
const isVisible = volumePanel.classList.contains('show');
|
||
if (isVisible) {
|
||
volumePanel.classList.remove('show');
|
||
setTimeout(() => (volumePanel.style.display = 'none'), 200);
|
||
} else {
|
||
volumePanel.style.display = 'block';
|
||
setTimeout(() => volumePanel.classList.add('show'), 10);
|
||
}
|
||
}
|
||
|
||
function toggleMute() {
|
||
audioPlayer.muted = !audioPlayer.muted;
|
||
if (!audioPlayer.muted && audioPlayer.volume === 0) {
|
||
audioPlayer.volume = lastVolume;
|
||
volumeSlider.value = lastVolume;
|
||
}
|
||
localStorage.setItem('audioMuted', audioPlayer.muted);
|
||
updateVolumeIcon();
|
||
|
||
const muteMessage = audioPlayer.muted
|
||
? (translations['mute_on'] || 'Audio muted')
|
||
: (translations['mute_off'] || 'Audio unmuted');
|
||
showLogMessage(muteMessage);
|
||
speakMessage(muteMessage);
|
||
}
|
||
|
||
function updateVolumeIcon() {
|
||
let cls;
|
||
if (audioPlayer.muted || audioPlayer.volume === 0) {
|
||
cls = 'bi bi-volume-mute-fill';
|
||
} else if (audioPlayer.volume < 0.5) {
|
||
cls = 'bi bi-volume-down-fill';
|
||
} else {
|
||
cls = 'bi bi-volume-up-fill';
|
||
}
|
||
muteIconEl.className = cls;
|
||
volumeIconEl.className = cls;
|
||
|
||
if (!audioPlayer.muted) {
|
||
lastVolume = audioPlayer.volume;
|
||
localStorage.setItem('audioVolume', lastVolume);
|
||
}
|
||
}
|
||
|
||
muteToggle.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
toggleMute();
|
||
});
|
||
|
||
volumeToggle.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
if (e.target === volumeIconEl) {
|
||
toggleMute();
|
||
} else {
|
||
togglePanel();
|
||
}
|
||
});
|
||
|
||
document.addEventListener('click', () => {
|
||
if (volumePanel.classList.contains('show')) {
|
||
volumePanel.classList.remove('show');
|
||
setTimeout(() => (volumePanel.style.display = 'none'), 200);
|
||
}
|
||
});
|
||
|
||
volumeSlider.addEventListener('input', e => {
|
||
const vol = Math.round(parseFloat(e.target.value) * 100);
|
||
audioPlayer.volume = e.target.value;
|
||
if (audioPlayer.muted) {
|
||
audioPlayer.muted = false;
|
||
localStorage.setItem('audioMuted', 'false');
|
||
}
|
||
updateVolumeIcon();
|
||
|
||
const volumeMessage = translations['volume_change']
|
||
? translations['volume_change'].replace('{vol}', vol)
|
||
: `Volume adjusted to ${vol}%`;
|
||
showLogMessage(volumeMessage);
|
||
speakMessage(volumeMessage);
|
||
});
|
||
|
||
const speedToggle = document.getElementById('speedToggle');
|
||
const speedLabel = document.getElementById('speedLabel');
|
||
const speeds = [0.75, 1, 1.25, 1.5, 1.75, 2];
|
||
let speedIndex = 1;
|
||
|
||
const savedSpeed = localStorage.getItem('audioSpeed');
|
||
if (savedSpeed !== null) {
|
||
const idx = speeds.indexOf(parseFloat(savedSpeed));
|
||
if (idx !== -1) {
|
||
speedIndex = idx;
|
||
}
|
||
}
|
||
|
||
audioPlayer.playbackRate = speeds[speedIndex];
|
||
speedLabel.textContent = speeds[speedIndex] + '×';
|
||
|
||
function toggleSpeed() {
|
||
speedIndex = (speedIndex + 1) % speeds.length;
|
||
const rate = speeds[speedIndex];
|
||
audioPlayer.playbackRate = rate;
|
||
speedLabel.textContent = rate + '×';
|
||
localStorage.setItem('audioSpeed', rate);
|
||
|
||
const speedMessage = translations['speed_change']
|
||
? translations['speed_change'].replace('{rate}', rate)
|
||
: `Playback speed changed to ${rate}x`;
|
||
showLogMessage(speedMessage);
|
||
speakMessage(speedMessage);
|
||
}
|
||
|
||
speedToggle.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
toggleSpeed();
|
||
});
|
||
|
||
speedToggle.addEventListener('click', e => e.stopPropagation());
|
||
</script>
|
||
|
||
<script>
|
||
(function() {
|
||
const toggleBtns = document.querySelectorAll('.toggleFloatingLyricsBtn');
|
||
const box = document.getElementById('floatingLyrics');
|
||
|
||
const savedState = localStorage.getItem('floatingLyricsVisible') === 'true';
|
||
const savedLeft = localStorage.getItem('floatingLyricsLeft');
|
||
const savedTop = localStorage.getItem('floatingLyricsTop');
|
||
|
||
box.classList.toggle('visible', savedState);
|
||
box.style.resize = 'none';
|
||
box.style.overflow = 'auto';
|
||
box.style.position = 'absolute';
|
||
|
||
if (savedLeft && savedTop) {
|
||
box.style.left = savedLeft;
|
||
box.style.top = savedTop;
|
||
} else {
|
||
box.style.left = '20px';
|
||
box.style.top = '20px';
|
||
}
|
||
|
||
toggleBtns.forEach(btn => {
|
||
btn.addEventListener('click', e => {
|
||
e.stopPropagation();
|
||
const isNowVisible = box.classList.toggle('visible');
|
||
localStorage.setItem('floatingLyricsVisible', isNowVisible);
|
||
|
||
const msgKey = isNowVisible
|
||
? 'floating_lyrics_enabled'
|
||
: 'floating_lyrics_disabled';
|
||
const message = translations[msgKey] ||
|
||
(isNowVisible
|
||
? "Floating lyrics enabled"
|
||
: "Floating lyrics disabled");
|
||
showLogMessage(message);
|
||
speakMessage(message);
|
||
});
|
||
});
|
||
|
||
let isDragging = false, offsetX = 0, offsetY = 0;
|
||
|
||
box.addEventListener('mousedown', e => {
|
||
if (e.target.closest('.ctrl-btn')) return;
|
||
e.preventDefault();
|
||
isDragging = true;
|
||
offsetX = e.clientX - box.offsetLeft;
|
||
offsetY = e.clientY - box.offsetTop;
|
||
});
|
||
|
||
document.addEventListener('mousemove', e => {
|
||
if (!isDragging) return;
|
||
updatePosition(e.clientX, e.clientY);
|
||
});
|
||
|
||
document.addEventListener('mouseup', () => {
|
||
if (isDragging) {
|
||
isDragging = false;
|
||
savePosition();
|
||
}
|
||
});
|
||
|
||
let touchId = null;
|
||
|
||
box.addEventListener('touchstart', e => {
|
||
if (e.target.closest('.ctrl-btn')) return;
|
||
e.preventDefault();
|
||
const touch = e.touches[0];
|
||
touchId = touch.identifier;
|
||
offsetX = touch.clientX - box.offsetLeft;
|
||
offsetY = touch.clientY - box.offsetTop;
|
||
}, { passive: false });
|
||
|
||
document.addEventListener('touchmove', e => {
|
||
if (!touchId) return;
|
||
const touch = Array.from(e.touches).find(t => t.identifier === touchId);
|
||
if (!touch) return;
|
||
updatePosition(touch.clientX, touch.clientY);
|
||
}, { passive: false });
|
||
|
||
document.addEventListener('touchend', () => {
|
||
if (touchId) {
|
||
touchId = null;
|
||
savePosition();
|
||
}
|
||
});
|
||
|
||
function updatePosition(clientX, clientY) {
|
||
const newLeft = (clientX - offsetX) + 'px';
|
||
const newTop = (clientY - offsetY) + 'px';
|
||
box.style.left = newLeft;
|
||
box.style.top = newTop;
|
||
}
|
||
|
||
function savePosition() {
|
||
localStorage.setItem('floatingLyricsLeft', box.style.left);
|
||
localStorage.setItem('floatingLyricsTop', box.style.top);
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('keydown', function (event) {
|
||
const target = event.target;
|
||
const isTyping = target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
|
||
|
||
if (isTyping) return;
|
||
|
||
switch (event.code) {
|
||
case 'ArrowUp':
|
||
event.preventDefault();
|
||
document.querySelector('.toggleFloatingLyricsBtn')?.click();
|
||
break;
|
||
case 'Delete':
|
||
if (currentTrackIndex !== 0) {
|
||
currentTrackIndex = 0;
|
||
loadTrack(songs[0]);
|
||
}
|
||
const message = translations['back_to_first'] || 'Returned to the first song in the playlist';
|
||
showLogMessage(message);
|
||
speakMessage(message);
|
||
break;
|
||
case 'Insert':
|
||
document.getElementById('repeatBtn')?.click();
|
||
break;
|
||
case 'Home':
|
||
event.preventDefault();
|
||
document.querySelector('.panel-btn[data-bs-target="#musicModal"]')?.click();
|
||
break;
|
||
case 'Escape':
|
||
event.preventDefault();
|
||
const confirmText = document.getElementById('clearConfirmText')?.textContent.trim() || 'Are you sure you want to clear the config?';
|
||
showConfirmation(confirmText, () => {
|
||
document.getElementById('clear-cache-btn')?.click();
|
||
});
|
||
speakMessage(translations['clear_confirm'] || 'Are you sure you want to clear the configuration?');
|
||
break;
|
||
}
|
||
});
|
||
|
||
document.getElementById('goFirstBtn')?.addEventListener('click', function() {
|
||
if (currentTrackIndex !== 0) {
|
||
currentTrackIndex = 0;
|
||
loadTrack(songs[0]);
|
||
}
|
||
const message = translations['back_to_first'] || 'Returned to the first song in the playlist';
|
||
showLogMessage(message);
|
||
speakMessage(message);
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
const websites = [
|
||
'https://www.baidu.com/',
|
||
'https://www.cloudflare.com/',
|
||
'https://openai.com/',
|
||
'https://www.youtube.com/',
|
||
'https://www.google.com/',
|
||
'https://www.facebook.com/',
|
||
'https://www.twitter.com/',
|
||
'https://www.github.com/'
|
||
];
|
||
|
||
function getWebsiteStatusMessage(url, status) {
|
||
const statusMessages = translations['statusMessages'][url] || {};
|
||
|
||
const message = status
|
||
? statusMessages['accessible'] || `${url} 网站访问正常。`
|
||
: statusMessages['notAccessible'] || `无法访问 ${url} 网站,请检查网络连接。`;
|
||
|
||
return message;
|
||
}
|
||
|
||
function checkWebsiteAccess(urls) {
|
||
const statusMessages = [];
|
||
let requestsCompleted = 0;
|
||
|
||
urls.forEach(url => {
|
||
fetch(url, { mode: 'no-cors' })
|
||
.then(response => {
|
||
const isAccessible = response.type === 'opaque';
|
||
statusMessages.push(getWebsiteStatusMessage(url, isAccessible));
|
||
})
|
||
.catch(() => {
|
||
statusMessages.push(getWebsiteStatusMessage(url, false));
|
||
})
|
||
.finally(() => {
|
||
requestsCompleted++;
|
||
if (requestsCompleted === urls.length) {
|
||
speakMessage(statusMessages.join(' '));
|
||
speakMessage(translations['websiteChecked']);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
let autoCheckInterval;
|
||
let isAutoCheckEnabled = localStorage.getItem('autoCheckEnabled') === 'true';
|
||
let userInteracted = localStorage.getItem('userInteracted') === 'true';
|
||
|
||
function updateToggleIcon() {
|
||
const icon = document.getElementById('autoCheckIcon');
|
||
if (isAutoCheckEnabled) {
|
||
icon.classList.replace('bi-toggle-off', 'bi-toggle-on');
|
||
} else {
|
||
icon.classList.replace('bi-toggle-on', 'bi-toggle-off');
|
||
}
|
||
}
|
||
|
||
function startAutoCheck(shouldAnnounce = false) {
|
||
if (autoCheckInterval) clearInterval(autoCheckInterval);
|
||
autoCheckInterval = setInterval(() => {
|
||
if (userInteracted) {
|
||
speakMessage(translations['startCheck']);
|
||
showLogMessage(translations['startCheck']);
|
||
}
|
||
checkWebsiteAccess(websites);
|
||
}, 3600000);
|
||
|
||
isAutoCheckEnabled = true;
|
||
localStorage.setItem('autoCheckEnabled', 'true');
|
||
updateToggleIcon();
|
||
|
||
if (shouldAnnounce && userInteracted) {
|
||
const msg = translations['autoCheckEnabled'] || "Auto check enabled";
|
||
speakMessage(msg);
|
||
showLogMessage(msg);
|
||
}
|
||
}
|
||
|
||
function stopAutoCheck(shouldAnnounce = false) {
|
||
if (autoCheckInterval) {
|
||
clearInterval(autoCheckInterval);
|
||
autoCheckInterval = null;
|
||
}
|
||
|
||
isAutoCheckEnabled = false;
|
||
localStorage.setItem('autoCheckEnabled', 'false');
|
||
updateToggleIcon();
|
||
|
||
if (shouldAnnounce && userInteracted) {
|
||
const msg = translations['autoCheckDisabled'] || "Auto check disabled";
|
||
speakMessage(msg);
|
||
showLogMessage(msg);
|
||
}
|
||
}
|
||
|
||
document.addEventListener('click', function() {
|
||
if (!userInteracted) {
|
||
userInteracted = true;
|
||
localStorage.setItem('userInteracted', 'true');
|
||
}
|
||
});
|
||
|
||
document.getElementById('autoCheckIcon').addEventListener('click', function(event) {
|
||
event.stopPropagation();
|
||
if (isAutoCheckEnabled) {
|
||
stopAutoCheck(true);
|
||
} else {
|
||
startAutoCheck(true);
|
||
}
|
||
});
|
||
|
||
updateToggleIcon();
|
||
if (isAutoCheckEnabled) {
|
||
startAutoCheck(false);
|
||
}
|
||
|
||
let isDetectionStarted = false;
|
||
|
||
document.addEventListener('keydown', function(event) {
|
||
if (event.key === 'F8' && !isDetectionStarted) {
|
||
event.preventDefault();
|
||
speakMessage(translations['checkStarted']);
|
||
checkWebsiteAccess(websites);
|
||
isDetectionStarted = true;
|
||
}
|
||
});
|
||
|
||
document.getElementById('startCheckBtn').addEventListener('click', function() {
|
||
speakMessage(translations['checkStarted']);
|
||
checkWebsiteAccess(websites);
|
||
});
|
||
|
||
function speakTimeNow() {
|
||
const now = new Date();
|
||
let hour = now.getHours();
|
||
const minute = now.getMinutes();
|
||
const second = now.getSeconds();
|
||
|
||
if (minute !== 0 || second !== 0) return;
|
||
|
||
let lang = currentLang.split('-')[0];
|
||
let message = '';
|
||
|
||
if (lang === 'zh' || lang === 'zh-tw' || lang === 'hk') {
|
||
let period = '';
|
||
if (hour >= 0 && hour < 6) period = translations['periods']?.['earlyMorning'] || '凌晨';
|
||
else if (hour >= 6 && hour < 12) period = translations['periods']?.['morning'] || '早上';
|
||
else if (hour >= 12 && hour < 18) period = translations['periods']?.['afternoon'] || '下午';
|
||
else period = translations['periods']?.['evening'] || '晚上';
|
||
|
||
const template = translations['timeReport'] || "整点报时,现在是北京时间{period}{hour}点整";
|
||
message = template.replace("{period}", period).replace("{hour}", hour);
|
||
} else {
|
||
let period = 'AM';
|
||
if (hour >= 12) period = 'PM';
|
||
let displayHour = hour % 12;
|
||
displayHour = displayHour === 0 ? 12 : displayHour;
|
||
|
||
const template = translations['timeReport']?.[lang] || translations['timeReport']?.['en'] || "It is now {hour} {period}.";
|
||
message = template.replace("{hour}", displayHour).replace("{period}", period);
|
||
}
|
||
|
||
speakMessage(message);
|
||
}
|
||
|
||
setInterval(speakTimeNow, 1000);
|
||
</script>
|
||
<style>
|
||
.animated-box {
|
||
width: 50px;
|
||
height: 50px;
|
||
margin: 10px;
|
||
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
|
||
border-radius: 10px;
|
||
position: absolute;
|
||
animation: complex-animation 5s infinite alternate ease-in-out;
|
||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
@keyframes complex-animation {
|
||
0% {
|
||
transform: rotate(0deg) scale(1);
|
||
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
|
||
}
|
||
|
||
25% {
|
||
transform: rotate(45deg) scale(1.2);
|
||
background: linear-gradient(135deg, #42a5f5, #66bb6a);
|
||
}
|
||
|
||
50% {
|
||
transform: rotate(90deg) scale(0.8);
|
||
background: linear-gradient(225deg, #ab47bc, #ff7043);
|
||
}
|
||
|
||
75% {
|
||
transform: rotate(135deg) scale(1.5);
|
||
background: linear-gradient(315deg, #29b6f6, #8e24aa);
|
||
}
|
||
|
||
100% {
|
||
transform: rotate(180deg) scale(1);
|
||
background: linear-gradient(45deg, #ff6b6b, #ffd93d);
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
(function() {
|
||
let isAnimationActive = localStorage.getItem('animationActive') === 'true';
|
||
let intervalId;
|
||
|
||
function createAnimatedBox() {
|
||
const box = document.createElement('div');
|
||
box.className = 'animated-box';
|
||
document.body.appendChild(box);
|
||
const randomX = Math.random() * window.innerWidth;
|
||
const randomY = Math.random() * window.innerHeight;
|
||
box.style.left = randomX + 'px';
|
||
box.style.top = randomY + 'px';
|
||
const randomDuration = Math.random() * 3 + 3;
|
||
box.style.animationDuration = randomDuration + 's';
|
||
setTimeout(() => {
|
||
box.remove();
|
||
}, randomDuration * 1000);
|
||
}
|
||
|
||
function startAnimation() {
|
||
intervalId = setInterval(() => {
|
||
createAnimatedBox();
|
||
}, 1000);
|
||
localStorage.setItem('animationActive', 'true');
|
||
isAnimationActive = true;
|
||
updateButtonText();
|
||
}
|
||
|
||
function stopAnimation() {
|
||
clearInterval(intervalId);
|
||
localStorage.setItem('animationActive', 'false');
|
||
isAnimationActive = false;
|
||
updateButtonText();
|
||
}
|
||
|
||
function updateButtonText() {
|
||
document.getElementById('toggleAnimationBtn').innerText = isAnimationActive
|
||
? translations['toggleButton']['stop']
|
||
: translations['toggleButton']['start'];
|
||
}
|
||
|
||
window.addEventListener('keydown', function(event) {
|
||
if (event.ctrlKey && event.key === 'F10') {
|
||
isAnimationActive = !isAnimationActive;
|
||
if (isAnimationActive) {
|
||
startAnimation();
|
||
showLogMessage(translations['startAnimation']);
|
||
speakMessage(translations['startAnimation']);
|
||
} else {
|
||
stopAnimation();
|
||
showLogMessage(translations['stopAnimation']);
|
||
speakMessage(translations['stopAnimation']);
|
||
}
|
||
}
|
||
});
|
||
|
||
document.getElementById('toggleAnimationBtn').addEventListener('click', function() {
|
||
if (isAnimationActive) {
|
||
stopAnimation();
|
||
showLogMessage(translations['stopAnimation']);
|
||
speakMessage(translations['stopAnimation']);
|
||
} else {
|
||
startAnimation();
|
||
showLogMessage(translations['startAnimation']);
|
||
speakMessage(translations['startAnimation']);
|
||
}
|
||
});
|
||
|
||
if (isAnimationActive) {
|
||
startAnimation();
|
||
}
|
||
updateButtonText();
|
||
})();
|
||
</script>
|
||
|
||
<style>
|
||
.snowflake {
|
||
position: absolute;
|
||
top: -10px;
|
||
width: 10px;
|
||
height: 10px;
|
||
background-color: white;
|
||
border-radius: 50%;
|
||
animation: fall linear infinite;
|
||
}
|
||
|
||
@keyframes fall {
|
||
0% {
|
||
transform: translateY(0) rotate(0deg);
|
||
}
|
||
|
||
100% {
|
||
transform: translateY(100vh) rotate(360deg);
|
||
}
|
||
}
|
||
|
||
.snowflake:nth-child(1) {
|
||
animation-duration: 8s;
|
||
animation-delay: -2s;
|
||
left: 10%;
|
||
width: 12px;
|
||
height: 12px;
|
||
}
|
||
|
||
.snowflake:nth-child(2) {
|
||
animation-duration: 10s;
|
||
animation-delay: -3s;
|
||
left: 20%;
|
||
width: 8px;
|
||
height: 8px;
|
||
}
|
||
|
||
.snowflake:nth-child(3) {
|
||
animation-duration: 12s;
|
||
animation-delay: -1s;
|
||
left: 30%;
|
||
width: 15px;
|
||
height: 15px;
|
||
}
|
||
|
||
.snowflake:nth-child(4) {
|
||
animation-duration: 9s;
|
||
animation-delay: -5s;
|
||
left: 40%;
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
|
||
.snowflake:nth-child(5) {
|
||
animation-duration: 11s;
|
||
animation-delay: -4s;
|
||
left: 50%;
|
||
width: 14px;
|
||
height: 14px;
|
||
}
|
||
|
||
.snowflake:nth-child(6) {
|
||
animation-duration: 7s;
|
||
animation-delay: -6s;
|
||
left: 60%;
|
||
width: 9px;
|
||
height: 9px;
|
||
}
|
||
|
||
.snowflake:nth-child(7) {
|
||
animation-duration: 8s;
|
||
animation-delay: -7s;
|
||
left: 70%;
|
||
width: 11px;
|
||
height: 11px;
|
||
}
|
||
|
||
.snowflake:nth-child(8) {
|
||
animation-duration: 10s;
|
||
animation-delay: -8s;
|
||
left: 80%;
|
||
width: 13px;
|
||
height: 13px;
|
||
}
|
||
|
||
.snowflake:nth-child(9) {
|
||
animation-duration: 6s;
|
||
animation-delay: -9s;
|
||
left: 90%;
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
function createSnowflakes() {
|
||
for (let i = 0; i < 80; i++) {
|
||
let snowflake = document.createElement('div');
|
||
snowflake.classList.add('snowflake');
|
||
|
||
let size = Math.random() * 10 + 5 + 'px';
|
||
snowflake.style.width = size;
|
||
snowflake.style.height = size;
|
||
|
||
let speed = Math.random() * 3 + 2 + 's';
|
||
snowflake.style.animationDuration = speed;
|
||
|
||
let rotate = Math.random() * 360 + 'deg';
|
||
let rotateSpeed = Math.random() * 5 + 2 + 's';
|
||
snowflake.style.animationName = 'fall';
|
||
snowflake.style.animationDuration = speed;
|
||
snowflake.style.animationTimingFunction = 'linear';
|
||
snowflake.style.animationIterationCount = 'infinite';
|
||
|
||
let leftPosition = Math.random() * 100 + 'vw';
|
||
snowflake.style.left = leftPosition;
|
||
|
||
snowflake.style.animationDelay = Math.random() * 5 + 's';
|
||
|
||
document.body.appendChild(snowflake);
|
||
}
|
||
}
|
||
|
||
function stopSnowflakes() {
|
||
let snowflakes = document.querySelectorAll('.snowflake');
|
||
snowflakes.forEach(snowflake => snowflake.remove());
|
||
}
|
||
|
||
function getSnowingState() {
|
||
return localStorage.getItem('isSnowing') === 'true';
|
||
}
|
||
|
||
function saveSnowingState(state) {
|
||
localStorage.setItem('isSnowing', state);
|
||
}
|
||
|
||
let isSnowing = getSnowingState();
|
||
|
||
if (isSnowing) {
|
||
createSnowflakes();
|
||
}
|
||
|
||
function toggleSnowflakes() {
|
||
isSnowing = !isSnowing;
|
||
saveSnowingState(isSnowing);
|
||
if (isSnowing) {
|
||
createSnowflakes();
|
||
showLogMessage(translations['startSnowflakes']);
|
||
speakMessage(translations['startSnowflakes']);
|
||
document.getElementById('toggleSnowBtn').innerText = translations['toggleSnowButton']['stop'];
|
||
} else {
|
||
stopSnowflakes();
|
||
showLogMessage(translations['stopSnowflakes']);
|
||
speakMessage(translations['stopSnowflakes']);
|
||
document.getElementById('toggleSnowBtn').innerText = translations['toggleSnowButton']['start'];
|
||
}
|
||
}
|
||
|
||
window.addEventListener('keydown', function(event) {
|
||
if (event.ctrlKey && event.key === 'F6') {
|
||
toggleSnowflakes();
|
||
}
|
||
});
|
||
|
||
document.getElementById('toggleSnowBtn').addEventListener('click', toggleSnowflakes);
|
||
|
||
if (isSnowing) {
|
||
document.getElementById('toggleSnowBtn').innerText = translations['toggleSnowButton']['stop'];
|
||
}
|
||
|
||
</script>
|
||
|
||
<style>
|
||
@keyframes lightPulse {
|
||
0% {
|
||
transform: scale(0.5);
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.5);
|
||
opacity: 0.7;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(3);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.light-point {
|
||
position: fixed;
|
||
width: 10px;
|
||
height: 10px;
|
||
background: radial-gradient(circle, rgba(255, 255, 255, 1), rgba(255, 255, 255, 0.2));
|
||
border-radius: 50%;
|
||
pointer-events: none;
|
||
z-index: 9999;
|
||
animation: lightPulse 3s linear infinite;
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
(function () {
|
||
let isLightEffectActive = localStorage.getItem('lightEffectAnimation') === 'true';
|
||
let lightInterval;
|
||
|
||
function createLightPoint() {
|
||
const lightPoint = document.createElement('div');
|
||
lightPoint.className = 'light-point';
|
||
|
||
const posX = Math.random() * window.innerWidth;
|
||
const posY = Math.random() * window.innerHeight;
|
||
|
||
lightPoint.style.left = `${posX}px`;
|
||
lightPoint.style.top = `${posY}px`;
|
||
|
||
const colors = ['#ffcc00', '#00ccff', '#ff6699', '#99ff66', '#cc99ff'];
|
||
const randomColor = colors[Math.floor(Math.random() * colors.length)];
|
||
lightPoint.style.background = `radial-gradient(circle, ${randomColor}, rgba(255, 255, 255, 0.1))`;
|
||
|
||
document.body.appendChild(lightPoint);
|
||
setTimeout(() => {
|
||
lightPoint.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
function startLightEffect(showLog = true) {
|
||
if (lightInterval) clearInterval(lightInterval);
|
||
lightInterval = setInterval(createLightPoint, 200);
|
||
localStorage.setItem('lightEffectAnimation', 'true');
|
||
if (showLog) {
|
||
showLogMessage(translations['startLightEffect']);
|
||
speakMessage(translations['startLightEffect']);
|
||
}
|
||
updateButtonText();
|
||
}
|
||
|
||
function stopLightEffect(showLog = true) {
|
||
clearInterval(lightInterval);
|
||
document.querySelectorAll('.light-point').forEach((light) => light.remove());
|
||
localStorage.setItem('lightEffectAnimation', 'false');
|
||
if (showLog) {
|
||
showLogMessage(translations['stopLightEffect']);
|
||
speakMessage(translations['stopLightEffect']);
|
||
}
|
||
updateButtonText();
|
||
}
|
||
|
||
function showNotification(message) {
|
||
const notification = document.createElement('div');
|
||
notification.style.position = 'fixed';
|
||
notification.style.top = '10px';
|
||
notification.style.right = '10px';
|
||
notification.style.padding = '10px';
|
||
notification.style.backgroundColor = '#4CAF50';
|
||
notification.style.color = '#fff';
|
||
notification.style.borderRadius = '5px';
|
||
notification.style.zIndex = 9999;
|
||
notification.textContent = message;
|
||
|
||
document.body.appendChild(notification);
|
||
setTimeout(() => {
|
||
notification.remove();
|
||
}, 3000);
|
||
}
|
||
|
||
function toggleLightEffect() {
|
||
isLightEffectActive = !isLightEffectActive;
|
||
if (isLightEffectActive) {
|
||
startLightEffect();
|
||
} else {
|
||
stopLightEffect();
|
||
}
|
||
}
|
||
|
||
window.addEventListener('keydown', function (event) {
|
||
if (event.ctrlKey && event.key === 'F11') {
|
||
toggleLightEffect();
|
||
}
|
||
});
|
||
|
||
document.getElementById('toggleLightEffectBtn').addEventListener('click', toggleLightEffect);
|
||
|
||
if (isLightEffectActive) {
|
||
document.getElementById('toggleLightEffectBtn').innerText = translations['toggleLightEffectButton']['stop'];
|
||
} else {
|
||
document.getElementById('toggleLightEffectBtn').innerText = translations['toggleLightEffectButton']['start'];
|
||
}
|
||
})();
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", function() {
|
||
feather.replace();
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
|
||
@media (max-width: 767.98px) {
|
||
#musicModal .modal-dialog {
|
||
margin: 0.5rem;
|
||
max-width: calc(100% - 1rem);
|
||
max-height: 90vh;
|
||
}
|
||
|
||
#musicModal .modal-content {
|
||
max-height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#musicModal .modal-body {
|
||
overflow-y: auto;
|
||
max-height: calc(90vh - 120px);
|
||
padding: 15px;
|
||
}
|
||
|
||
#musicModal #floatingLyrics {
|
||
font-size: 1.1rem;
|
||
padding: 8px 12px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
#musicModal #currentSong {
|
||
font-size: 1.1rem;
|
||
margin-bottom: 8px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
#musicModal .lyrics-container {
|
||
height: 200px;
|
||
font-size: 0.95rem;
|
||
line-height: 1.6;
|
||
padding: 10px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#musicModal .progress-container {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
#musicModal .d-flex.justify-content-between {
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
#musicModal .controls {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
justify-content: space-between;
|
||
gap: 4px;
|
||
margin-top: 15px;
|
||
overflow-x: auto;
|
||
padding-bottom: 5px;
|
||
}
|
||
|
||
#musicModal .control-btn,
|
||
#musicModal .btn-volume {
|
||
width: 35px;
|
||
height: 35px;
|
||
min-width: 35px;
|
||
padding: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
#musicModal #playPauseBtn {
|
||
width: 35px;
|
||
height: 35px;
|
||
min-width: 35px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
#musicModal .btn-volume {
|
||
position: relative;
|
||
}
|
||
|
||
#musicModal #volumePanel {
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 100px;
|
||
z-index: 10;
|
||
background: rgba(0, 0, 0, 0.8);
|
||
border-radius: 5px;
|
||
display: none;
|
||
padding: 8px;
|
||
}
|
||
|
||
#musicModal .btn-volume:hover #volumePanel,
|
||
#musicModal .btn-volume:focus-within #volumePanel {
|
||
display: block;
|
||
}
|
||
|
||
#musicModal .playlist {
|
||
margin-top: 15px;
|
||
max-height: 150px;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
#musicModal .playlist-item {
|
||
padding: 6px 10px;
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
#musicModal .modal-footer {
|
||
padding: 10px 15px;
|
||
}
|
||
|
||
#musicModal .modal-footer .btn {
|
||
padding: 5px 10px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
#musicModal .modal-body::-webkit-scrollbar,
|
||
#musicModal .lyrics-container::-webkit-scrollbar,
|
||
#musicModal .playlist::-webkit-scrollbar {
|
||
width: 6px;
|
||
}
|
||
|
||
#musicModal .modal-body::-webkit-scrollbar-track,
|
||
#musicModal .lyrics-container::-webkit-scrollbar-track,
|
||
#musicModal .playlist::-webkit-scrollbar-track {
|
||
background: rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
#musicModal .modal-body::-webkit-scrollbar-thumb,
|
||
#musicModal .lyrics-container::-webkit-scrollbar-thumb,
|
||
#musicModal .playlist::-webkit-scrollbar-thumb {
|
||
background: #4ecca3;
|
||
border-radius: 3px;
|
||
}
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
function initializeStyles() {
|
||
const containerWidth = localStorage.getItem('containerWidth') || 1600;
|
||
const modalMaxWidth = localStorage.getItem('modalMaxWidth') || 1100;
|
||
document.documentElement.style.setProperty('--container-width', `${containerWidth}px`);
|
||
document.documentElement.style.setProperty('--modal-max-width', `${modalMaxWidth}px`);
|
||
}
|
||
|
||
initializeStyles();
|
||
|
||
const slider = document.getElementById("containerWidth");
|
||
const widthValue = document.getElementById("widthValue");
|
||
const modalSlider = document.getElementById("modalMaxWidth");
|
||
const modalWidthValue = document.getElementById("modalWidthValue");
|
||
|
||
slider.value = localStorage.getItem('containerWidth') || 1600;
|
||
modalSlider.value = localStorage.getItem('modalMaxWidth') || 1100;
|
||
|
||
function updateSliderColor(value, slider, valueElement) {
|
||
let red = Math.min(Math.max((value - 800) / (5400 - 800) * 255, 0), 255);
|
||
let green = 255 - red;
|
||
slider.style.background = `linear-gradient(to right, rgb(${red}, ${green}, 255), rgb(${255 - red}, ${green}, ${255 - red}))`;
|
||
slider.style.setProperty('--thumb-color', `rgb(${red}, ${green}, 255)`);
|
||
const currentWidthText = translations && translations['current_width']
|
||
? translations['current_width']
|
||
: 'Current Width';
|
||
valueElement.textContent = `${currentWidthText}: ${value}px`;
|
||
valueElement.style.color = `rgb(${red}, ${green}, 255)`;
|
||
}
|
||
|
||
updateSliderColor(slider.value, slider, widthValue);
|
||
updateSliderColor(modalSlider.value, modalSlider, modalWidthValue);
|
||
|
||
slider.oninput = function () {
|
||
updateSliderColor(slider.value, slider, widthValue);
|
||
localStorage.setItem('containerWidth', slider.value);
|
||
document.documentElement.style.setProperty('--container-width', `${slider.value}px`);
|
||
const msg = translations['page_width_updated'].replace('%s', slider.value);
|
||
showLogMessage(msg);
|
||
if (typeof speakMessage === 'function') speakMessage(msg);
|
||
};
|
||
|
||
modalSlider.oninput = function () {
|
||
updateSliderColor(modalSlider.value, modalSlider, modalWidthValue);
|
||
localStorage.setItem('modalMaxWidth', modalSlider.value);
|
||
document.documentElement.style.setProperty('--modal-max-width', `${modalSlider.value}px`);
|
||
const msg = translations['modal_width_updated'].replace('%s', modalSlider.value);
|
||
showLogMessage(msg);
|
||
if (typeof speakMessage === 'function') speakMessage(msg);
|
||
};
|
||
|
||
const savedColor = localStorage.getItem('themeBackgroundColor') || '';
|
||
const preview = document.getElementById('color-preview');
|
||
const selector = document.getElementById('color-selector');
|
||
const input = document.getElementById('color-input');
|
||
const colorBlock = document.getElementById('current-color-block');
|
||
|
||
preview.style.background = savedColor;
|
||
selector.value = savedColor;
|
||
input.value = savedColor;
|
||
colorBlock.style.background = savedColor;
|
||
applyCustomBackgroundColor(savedColor);
|
||
|
||
selector.addEventListener('input', e => {
|
||
const color = e.target.value;
|
||
preview.style.background = color;
|
||
input.value = color;
|
||
colorBlock.style.background = color;
|
||
});
|
||
|
||
input.addEventListener('input', e => {
|
||
const color = e.target.value;
|
||
if (isValidColor(color)) {
|
||
preview.style.background = color;
|
||
selector.value = color;
|
||
colorBlock.style.background = color;
|
||
}
|
||
});
|
||
|
||
colorBlock.addEventListener('click', () => {
|
||
const color = selector.value;
|
||
preview.style.background = color;
|
||
input.value = color;
|
||
applyCustomBackgroundColor(color);
|
||
});
|
||
|
||
document.getElementById('apply-color').addEventListener('click', () => {
|
||
const color = document.getElementById('color-input').value;
|
||
if (isValidColor(color)) {
|
||
applyCustomBackgroundColor(color);
|
||
const successMsgTemplate = translations['apply_color_success'] || 'Background color %s has been applied successfully.';
|
||
const successMsg = successMsgTemplate.replace('%s', color);
|
||
if (typeof showLogMessage === 'function') showLogMessage(successMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(successMsg);
|
||
} else {
|
||
const invalidMsg = translations['invalid_color'] || 'The color entered is invalid.';
|
||
if (typeof showLogMessage === 'function') showLogMessage(invalidMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(invalidMsg);
|
||
}
|
||
});
|
||
|
||
document.getElementById('reset-color').addEventListener('click', () => {
|
||
resetCustomBackgroundColor();
|
||
const defaultColor = '';
|
||
preview.style.background = defaultColor;
|
||
selector.value = defaultColor;
|
||
input.value = defaultColor;
|
||
colorBlock.style.background = defaultColor;
|
||
const successMsg = translations['reset_color_success'] || 'Background color has been reset to default.';
|
||
if (typeof showLogMessage === 'function') showLogMessage(successMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(successMsg);
|
||
});
|
||
|
||
generateColorPresets();
|
||
window.addEventListener('resize', generateColorPresets);
|
||
|
||
document.querySelectorAll('input[type=range]').forEach(range => {
|
||
updateRangeBackground(range);
|
||
range.addEventListener('input', () => updateRangeBackground(range));
|
||
});
|
||
});
|
||
|
||
function isValidColor(str) {
|
||
const s = new Option().style;
|
||
s.color = str;
|
||
return s.color !== '';
|
||
}
|
||
|
||
function applyCustomBackgroundColor(color) {
|
||
document.body.style.background = color;
|
||
localStorage.setItem('themeBackgroundColor', color);
|
||
}
|
||
|
||
function resetCustomBackgroundColor() {
|
||
document.body.style.background = '';
|
||
localStorage.removeItem('themeBackgroundColor');
|
||
}
|
||
|
||
function generateColorPresets() {
|
||
const presets = [
|
||
'#0f3460', '#0f172a', '#1e293b', '#1e3a8a', '#1d4ed8', '#2563eb',
|
||
'#3b82f6', '#1e40af', '#3730a3', '#4c1d95', '#5b21b6', '#6d28d9',
|
||
'#7c3aed', '#0369a1', '#0284c7', '#0ea5e9', '#38bdf8', '#7dd3fc',
|
||
'#bae6fd', '#1d4ed8', '#60a5fa', '#93c5fd', '#bfdbfe',
|
||
'#064e3b', '#047857', '#059669', '#10b981', '#34d399', '#6ee7b7',
|
||
'#a7f3d0', '#d1fae5', '#166534', '#22c55e',
|
||
'#854d0e', '#a16207', '#ca8a04', '#eab308', '#facc15', '#fde047',
|
||
'#fef08a', '#fef9c3', '#ea580c', '#f97316', '#fb923c', '#fdba74',
|
||
'#7f1d1d', '#b91c1c', '#dc2626', '#ef4444', '#f87171', '#fca5a5',
|
||
'#fecaca', '#fee2e2', '#9d174d', '#be185d', '#db2777', '#ec4899',
|
||
'#581c87', '#6b21a8', '#7e22ce', '#9333ea', '#a855f7', '#c084fc',
|
||
'#d8b4fe', '#e9d5ff', '#5b21b6', '#7c3aed',
|
||
'#111827', '#1f2937', '#374151', '#4b5563', '#6b7280', '#9ca3af',
|
||
'#d1d5db', '#e5e7eb', '#f3f4f6', '#f9fafb', '#ffffff',
|
||
'#134e4a', '#0d9488', '#14b8a6', '#2dd4bf', '#5eead4', '#99f6e4',
|
||
'#ccfbf1', '#ecfdf5', '#0891b2', '#06b6d4',
|
||
'#4338ca', '#4f46e5', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef',
|
||
'#ec4899', '#f43f5e', '#ef4444', '#f97316', '#f59e0b', '#eab308'
|
||
];
|
||
|
||
const container = document.getElementById('preset-colors');
|
||
container.innerHTML = '';
|
||
container.style.gridTemplateColumns = window.innerWidth < 768 ? 'repeat(10, 1fr)' : 'repeat(15, 1fr)';
|
||
|
||
presets.forEach(color => {
|
||
const div = document.createElement('div');
|
||
div.className = 'color-preset rounded';
|
||
div.style.background = color;
|
||
div.style.height = '30px';
|
||
div.style.cursor = 'pointer';
|
||
div.title = color;
|
||
div.style.border = '2px solid transparent';
|
||
div.addEventListener('click', () => {
|
||
document.getElementById('color-preview').style.background = color;
|
||
document.getElementById('color-selector').value = color;
|
||
document.getElementById('color-input').value = color;
|
||
document.getElementById('current-color-block').style.background = color;
|
||
applyCustomBackgroundColor(color);
|
||
const msgTemplate = translations['apply_color_success'] || 'Background color %s has been applied successfully.';
|
||
const msg = msgTemplate.replace('%s', color);
|
||
if (typeof showLogMessage === 'function') showLogMessage(msg);
|
||
if (typeof speakMessage === 'function') speakMessage(msg);
|
||
});
|
||
container.appendChild(div);
|
||
});
|
||
}
|
||
|
||
function updateRangeBackground(range) {
|
||
const val = range.value;
|
||
const min = range.min ? range.min : 0;
|
||
const max = range.max ? range.max : 100;
|
||
const percent = (val - min) / (max - min) * 100;
|
||
range.style.background = `linear-gradient(to right, #00ff00 0%, #00ff00 ${percent}%, #ffffff ${percent}%, #ffffff 100%)`;
|
||
}
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const colorModal = new bootstrap.Modal(document.getElementById('colorModal'));
|
||
let currentHue = 260, currentChroma = 0.10, currentLightness = 30;
|
||
let recentColors = [];
|
||
|
||
function hexToRgb(hex) {
|
||
const fullHex = hex.replace(/^#?([a-f\d])([a-f\d])([a-f\d])$/i,
|
||
(_, r, g, b) => `#${r}${r}${g}${g}${b}${b}`);
|
||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(fullHex);
|
||
return result ? {
|
||
r: parseInt(result[1], 16),
|
||
g: parseInt(result[2], 16),
|
||
b: parseInt(result[3], 16)
|
||
} : { r: 0, g: 0, b: 0 };
|
||
}
|
||
|
||
function rgbToLinear(c) {
|
||
const normalized = c / 255;
|
||
return normalized <= 0.04045
|
||
? normalized / 12.92
|
||
: Math.pow((normalized + 0.055) / 1.055, 2.4);
|
||
}
|
||
|
||
function rgbToOklch(r, g, b) {
|
||
const [lr, lg, lb] = [r, g, b].map(rgbToLinear);
|
||
const l = 0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb;
|
||
const m = 0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb;
|
||
const s = 0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb;
|
||
const l_ = Math.cbrt(l);
|
||
const m_ = Math.cbrt(m);
|
||
const s_ = Math.cbrt(s);
|
||
const L = 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_;
|
||
const a = 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_;
|
||
const b_ = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_;
|
||
const c = Math.sqrt(a ** 2 + b_ ** 2);
|
||
let h = Math.atan2(b_, a) * 180 / Math.PI;
|
||
h = h >= 0 ? h : h + 360;
|
||
return { l: L * 100, c: c, h: h };
|
||
}
|
||
|
||
function hexToOklch(hex) {
|
||
const { r, g, b } = hexToRgb(hex);
|
||
return rgbToOklch(r, g, b);
|
||
}
|
||
|
||
function oklchToHex(h, c, l = 50) {
|
||
const L = l / 100;
|
||
const a = c * Math.cos(h * Math.PI / 180);
|
||
const b = c * Math.sin(h * Math.PI / 180);
|
||
const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
|
||
const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
|
||
const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
|
||
const [lr, lg, lb] = [l_, m_, s_].map(v => v ** 3);
|
||
const r = 4.0767416621 * lr - 3.3077115913 * lg + 0.2309699292 * lb;
|
||
const g = -1.2684380046 * lr + 2.6097574011 * lg - 0.3413193965 * lb;
|
||
const bLinear = -0.0041960863 * lr - 0.7034186147 * lg + 1.7076147010 * lb;
|
||
const toSRGB = (v) => {
|
||
v = Math.min(Math.max(v, 0), 1);
|
||
return v > 0.0031308
|
||
? 1.055 * (v ** (1/2.4)) - 0.055
|
||
: 12.92 * v;
|
||
};
|
||
const [R, G, B] = [r, g, bLinear].map(v => Math.round(toSRGB(v) * 255));
|
||
return `#${[R, G, B].map(x => x.toString(16).padStart(2, '0')).join('')}`.toUpperCase();
|
||
}
|
||
|
||
function updateTextPrimary(currentL) {
|
||
const textL = currentL > 60 ? 20 : 95;
|
||
document.documentElement.style.setProperty('--text-primary', `oklch(${textL}% 0 0)`);
|
||
document.body.offsetHeight;
|
||
}
|
||
|
||
function updateColorPreview() {
|
||
const hexColor = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
document.getElementById('colorPreview').style.backgroundColor = hexColor;
|
||
const contrastRatio = calculateContrast(hexColor);
|
||
document.getElementById('contrastRatio').textContent = contrastRatio;
|
||
updateContrastRating(parseFloat(contrastRatio.split(':')[0]));
|
||
document.getElementById('oklchValue').textContent = `OKLCH(${currentLightness.toFixed(0)}%, ${currentChroma.toFixed(2)}, ${Math.round(currentHue)}°)`;
|
||
}
|
||
|
||
function calculateContrast(hexColor) {
|
||
const rgb = hexToRgb(hexColor);
|
||
const luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
|
||
const textLuminance = luminance > 0.5 ? 0 : 1;
|
||
const contrast = (Math.max(luminance, textLuminance) + 0.05) / (Math.min(luminance, textLuminance) + 0.05);
|
||
return contrast.toFixed(2) + ":1";
|
||
}
|
||
|
||
function updateContrastRating(contrast) {
|
||
const container = document.getElementById('contrastRating');
|
||
let icon = '';
|
||
if (contrast >= 7) {
|
||
icon = '<i class="bi bi-check-circle-fill"></i> ';
|
||
container.className = "mt-1 text-success fw-bold";
|
||
container.innerHTML = `${icon}${translations['excellent_aaa'] || 'Excellent (AAA)'}`;
|
||
} else if (contrast >= 4.5) {
|
||
icon = '<i class="bi bi-check-circle"></i> ';
|
||
container.className = "mt-1 text-primary fw-bold";
|
||
container.innerHTML = `${icon}${translations['good_aa'] || 'Good (AA)'}`;
|
||
} else {
|
||
icon = '<i class="bi bi-exclamation-triangle-fill"></i> ';
|
||
container.className = "mt-1 text-danger fw-bold";
|
||
container.innerHTML = `${icon}${translations['poor_needs_improvement'] || 'Needs Improvement'}`;
|
||
}
|
||
}
|
||
|
||
function adjustHue(amount) {
|
||
currentHue = (currentHue + amount + 360) % 360;
|
||
document.getElementById('hueSlider').value = currentHue;
|
||
document.getElementById('hueValue').textContent = Math.round(currentHue) + '°';
|
||
updateColorPreview();
|
||
}
|
||
|
||
function addToRecentColors(color) {
|
||
recentColors = recentColors.filter(c => c !== color);
|
||
recentColors.unshift(color);
|
||
|
||
if (recentColors.length > 10) {
|
||
recentColors.pop();
|
||
}
|
||
|
||
updateRecentColors();
|
||
localStorage.setItem('appColorSettings', JSON.stringify({
|
||
recentColors,
|
||
hue: currentHue,
|
||
chroma: currentChroma,
|
||
lightness: currentLightness
|
||
}));
|
||
}
|
||
|
||
function updateRecentColors() {
|
||
const container = document.getElementById('recentColors');
|
||
container.innerHTML = '';
|
||
recentColors.forEach(color => {
|
||
const swatch = document.createElement('button');
|
||
swatch.className = 'btn btn-sm p-2';
|
||
swatch.style.backgroundColor = color;
|
||
swatch.style.width = '30px';
|
||
swatch.style.height = '30px';
|
||
swatch.title = color;
|
||
swatch.addEventListener('click', function() {
|
||
const { h, c, l } = hexToOklch(color);
|
||
document.getElementById('hueSlider').value = h;
|
||
document.getElementById('chromaSlider').value = c;
|
||
document.getElementById('lightnessSlider').value = l;
|
||
document.getElementById('hueValue').textContent = Math.round(h) + '°';
|
||
document.getElementById('chromaValue').textContent = c.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = l + '%';
|
||
currentHue = h;
|
||
currentChroma = c;
|
||
currentLightness = l;
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = color;
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
updateTextPrimary(currentLightness);
|
||
addToRecentColors(color);
|
||
});
|
||
container.appendChild(swatch);
|
||
});
|
||
}
|
||
|
||
function initRecentColors() {
|
||
const savedSettings = localStorage.getItem('appColorSettings');
|
||
let initialTheme = 'dark';
|
||
if (savedSettings) {
|
||
const settings = JSON.parse(savedSettings);
|
||
recentColors = settings.recentColors || [];
|
||
if (recentColors.length > 10) {
|
||
recentColors = recentColors.slice(0, 10);
|
||
}
|
||
currentHue = settings.hue || 260;
|
||
currentChroma = settings.chroma || 0.10;
|
||
currentLightness = settings.lightness || 30;
|
||
initialTheme = currentLightness > 60 ? 'light' : 'dark';
|
||
} else {
|
||
recentColors = [
|
||
'#4d79ff', '#ff4d94', '#4dff88', '#ffb84d',
|
||
'#bf4dff', '#ff6b6b', '#4eca9e', '#ff9ff3',
|
||
'#6c757d', '#ffc107'
|
||
];
|
||
}
|
||
document.documentElement.setAttribute('data-theme', initialTheme);
|
||
document.getElementById('lightnessSlider').value = currentLightness;
|
||
document.getElementById('lightnessValue').textContent = currentLightness + '%';
|
||
updateColorPreview();
|
||
updateTextPrimary(currentLightness);
|
||
updateRecentColors();
|
||
}
|
||
|
||
const picker = document.getElementById("colorPicker");
|
||
picker.addEventListener('input', (event) => {
|
||
const color = event.target.value;
|
||
const { h, c, l } = hexToOklch(color);
|
||
currentHue = h;
|
||
currentChroma = c;
|
||
currentLightness = l;
|
||
|
||
document.getElementById('hueSlider').value = h;
|
||
document.getElementById('chromaSlider').value = c;
|
||
document.getElementById('lightnessSlider').value = l;
|
||
document.getElementById('hueValue').textContent = Math.round(h) + '°';
|
||
document.getElementById('chromaValue').textContent = c.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = l + '%';
|
||
|
||
const theme = l > 60 ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
|
||
updateColorPreview();
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
updateTextPrimary(currentLightness);
|
||
addToRecentColors(color);
|
||
});
|
||
|
||
document.getElementById('advancedColorBtn').addEventListener('click', () => {
|
||
document.getElementById('hueSlider').value = currentHue;
|
||
document.getElementById('chromaSlider').value = currentChroma;
|
||
document.getElementById('lightnessSlider').value = currentLightness;
|
||
document.getElementById('hueValue').textContent = Math.round(currentHue) + '°';
|
||
document.getElementById('chromaValue').textContent = currentChroma.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = currentLightness + '%';
|
||
updateColorPreview();
|
||
colorModal.show();
|
||
});
|
||
|
||
document.getElementById('hueSlider').addEventListener('input', function() {
|
||
currentHue = parseFloat(this.value);
|
||
document.getElementById('hueValue').textContent = Math.round(currentHue) + '°';
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
updateTextPrimary(currentLightness);
|
||
});
|
||
|
||
document.getElementById('chromaSlider').addEventListener('input', function() {
|
||
currentChroma = parseFloat(this.value);
|
||
document.getElementById('chromaValue').textContent = currentChroma.toFixed(2);
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
updateTextPrimary(currentLightness);
|
||
});
|
||
|
||
document.getElementById('lightnessSlider').addEventListener('input', function() {
|
||
currentLightness = parseFloat(this.value);
|
||
document.getElementById('lightnessValue').textContent = currentLightness + '%';
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
updateTextPrimary(currentLightness);
|
||
|
||
const theme = currentLightness > 60 ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
});
|
||
|
||
document.querySelectorAll('[data-h]').forEach(button => {
|
||
button.addEventListener('click', function() {
|
||
currentHue = parseFloat(this.dataset.h);
|
||
currentChroma = parseFloat(this.dataset.c);
|
||
currentLightness = parseFloat(this.dataset.l);
|
||
document.getElementById('hueSlider').value = currentHue;
|
||
document.getElementById('chromaSlider').value = currentChroma;
|
||
document.getElementById('lightnessSlider').value = currentLightness;
|
||
document.getElementById('hueValue').textContent = Math.round(currentHue) + '°';
|
||
document.getElementById('chromaValue').textContent = currentChroma.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = currentLightness + '%';
|
||
addToRecentColors(oklchToHex(currentHue, currentChroma, currentLightness));
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
updateTextPrimary(currentLightness);
|
||
});
|
||
});
|
||
|
||
document.getElementById('applyColorBtn').addEventListener('click', function() {
|
||
const settings = {
|
||
hue: currentHue,
|
||
chroma: currentChroma,
|
||
lightness: currentLightness,
|
||
recentColors: recentColors || []
|
||
};
|
||
localStorage.setItem('appColorSettings', JSON.stringify(settings));
|
||
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
updateTextPrimary(currentLightness);
|
||
|
||
const hexColor = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
const successMsg = (translations['apply_color_success'] || 'Background color %s has been applied successfully.').replace('%s', hexColor);
|
||
if (typeof showLogMessage === 'function') showLogMessage(successMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(successMsg);
|
||
|
||
colorModal.hide();
|
||
});
|
||
|
||
document.getElementById('resetColorBtn').addEventListener('click', function() {
|
||
currentHue = 260;
|
||
currentChroma = 0.10;
|
||
currentLightness = 30;
|
||
document.getElementById('hueSlider').value = currentHue;
|
||
document.getElementById('chromaSlider').value = currentChroma;
|
||
document.getElementById('lightnessSlider').value = currentLightness;
|
||
document.getElementById('hueValue').textContent = currentHue + '°';
|
||
document.getElementById('chromaValue').textContent = currentChroma.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = currentLightness + '%';
|
||
updateColorPreview();
|
||
document.getElementById('colorPicker').value = oklchToHex(currentHue, currentChroma, currentLightness);
|
||
document.documentElement.style.setProperty('--base-hue', currentHue);
|
||
document.documentElement.style.setProperty('--base-chroma', currentChroma);
|
||
updateTextPrimary(currentLightness);
|
||
|
||
const resetMsg = translations['reset_color_success'] || 'Application color settings have been reset to default.';
|
||
if (typeof showLogMessage === 'function') showLogMessage(resetMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(resetMsg);
|
||
});
|
||
|
||
initRecentColors();
|
||
const savedSettings = localStorage.getItem('appColorSettings');
|
||
if (savedSettings) {
|
||
const settings = JSON.parse(savedSettings);
|
||
document.documentElement.style.setProperty('--base-hue', settings.hue || 260);
|
||
document.documentElement.style.setProperty('--base-chroma', settings.chroma || 0.10);
|
||
document.getElementById('colorPicker').value = oklchToHex(settings.hue || 260, settings.chroma || 0.10, settings.lightness || 30);
|
||
updateTextPrimary(settings.lightness || 30);
|
||
}
|
||
});
|
||
|
||
document.getElementById('removeAppColorBtn').addEventListener('click', function() {
|
||
const confirmMsg = translations['confirm_reset_color'] || 'Are you sure you want to reset color settings to default?';
|
||
|
||
showConfirmation(confirmMsg, function() {
|
||
localStorage.removeItem('appColorSettings');
|
||
|
||
let defaultHue = 260;
|
||
let defaultChroma = 0.10;
|
||
let defaultLightness = 30;
|
||
document.getElementById('hueSlider').value = defaultHue;
|
||
document.getElementById('chromaSlider').value = defaultChroma;
|
||
document.getElementById('lightnessSlider').value = defaultLightness;
|
||
document.getElementById('hueValue').textContent = defaultHue + '°';
|
||
document.getElementById('chromaValue').textContent = defaultChroma.toFixed(2);
|
||
document.getElementById('lightnessValue').textContent = defaultLightness + '%';
|
||
|
||
if (typeof oklchToHex === 'function') {
|
||
const hex = oklchToHex(defaultHue, defaultChroma, defaultLightness);
|
||
document.getElementById('colorPreview').style.backgroundColor = hex;
|
||
document.getElementById('colorPicker').value = hex;
|
||
} else {
|
||
document.getElementById('colorPreview').style.backgroundColor = '';
|
||
document.getElementById('colorPicker').value = '';
|
||
}
|
||
|
||
let recentColorsDiv = document.getElementById('recentColors');
|
||
if (recentColorsDiv) recentColorsDiv.innerHTML = '';
|
||
|
||
document.documentElement.style.setProperty('--base-hue', defaultHue);
|
||
document.documentElement.style.setProperty('--base-chroma', defaultChroma);
|
||
|
||
document.documentElement.setAttribute('data-theme', defaultLightness > 60 ? 'light' : 'dark');
|
||
|
||
const successMsg = translations['reset_color_success'] || 'Application color settings have been reset to default.';
|
||
if (typeof showLogMessage === 'function') showLogMessage(successMsg);
|
||
if (typeof speakMessage === 'function') speakMessage(successMsg);
|
||
});
|
||
});
|
||
|
||
function checkColorChange() {
|
||
const saved = localStorage.getItem('appColorSettings');
|
||
|
||
if (saved) {
|
||
const settings = JSON.parse(saved);
|
||
|
||
const rootStyles = getComputedStyle(document.documentElement);
|
||
const currentHueValue = parseFloat(rootStyles.getPropertyValue('--base-hue').trim()) || 0;
|
||
const currentChromaValue = parseFloat(rootStyles.getPropertyValue('--base-chroma').trim()) || 0;
|
||
|
||
if (Math.abs(settings.hue - currentHueValue) > 1 || Math.abs(settings.chroma - currentChromaValue) > 0.01) {
|
||
document.documentElement.style.setProperty('--base-hue', settings.hue);
|
||
document.documentElement.style.setProperty('--base-chroma', settings.chroma);
|
||
|
||
const textL = settings.lightness > 60 ? 20 : 95;
|
||
document.documentElement.style.setProperty('--text-primary', `oklch(${textL}% 0 0)`);
|
||
|
||
const theme = settings.lightness > 60 ? 'light' : 'dark';
|
||
document.documentElement.setAttribute('data-theme', theme);
|
||
}
|
||
}
|
||
}
|
||
|
||
setInterval(checkColorChange, 1000);
|
||
</script>
|
||
|
||
<script>
|
||
window.showConfirmation = function(message, onConfirm) {
|
||
const decodedMessage = decodeURIComponent(message);
|
||
const modalElement = document.getElementById('confirmModal');
|
||
document.getElementById('confirmModalMessage').innerText = decodedMessage;
|
||
|
||
const oldBtn = document.getElementById('confirmModalYes');
|
||
const newBtn = oldBtn.cloneNode(true);
|
||
oldBtn.parentNode.replaceChild(newBtn, oldBtn);
|
||
|
||
newBtn.addEventListener('click', () => {
|
||
const modal = bootstrap.Modal.getInstance(modalElement);
|
||
modal.hide();
|
||
if (typeof onConfirm === 'function') onConfirm();
|
||
});
|
||
|
||
let modal = bootstrap.Modal.getInstance(modalElement);
|
||
if (!modal) {
|
||
modal = new bootstrap.Modal(modalElement);
|
||
}
|
||
modal.show();
|
||
};
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const currentSong = document.querySelector('#currentSong');
|
||
const floatingCurrentSong = document.getElementById('floatingCurrentSong');
|
||
const dynamicTitle = document.getElementById('dynamicTitle');
|
||
|
||
let usedColors = [];
|
||
let usedLogBoxColors = [];
|
||
|
||
function getColorListFromTheme() {
|
||
const styles = getComputedStyle(document.documentElement);
|
||
const lightness = styles.getPropertyValue('--l').trim();
|
||
const chroma = styles.getPropertyValue('--c').trim();
|
||
|
||
const colors = [];
|
||
for (let i = 1; i <= 7; i++) {
|
||
const hue = styles.getPropertyValue(`--base-hue-${i}`).trim();
|
||
const color = `oklch(${lightness} ${chroma} ${hue})`;
|
||
colors.push(color);
|
||
}
|
||
return colors;
|
||
}
|
||
|
||
function getNextColor(colorList) {
|
||
if (usedColors.length === colorList.length) {
|
||
usedColors = [];
|
||
}
|
||
const remaining = colorList.filter(c => !usedColors.includes(c));
|
||
const next = remaining[Math.floor(Math.random() * remaining.length)];
|
||
usedColors.push(next);
|
||
return next;
|
||
}
|
||
|
||
function rotateColors() {
|
||
const colorList = getColorListFromTheme();
|
||
|
||
if (currentSong) {
|
||
currentSong.style.color = getNextColor(colorList);
|
||
}
|
||
if (floatingCurrentSong) {
|
||
floatingCurrentSong.style.color = getNextColor(colorList);
|
||
}
|
||
if (dynamicTitle) {
|
||
dynamicTitle.style.color = getNextColor(colorList);
|
||
}
|
||
}
|
||
|
||
function getLogBoxColorListFromTheme() {
|
||
const styles = getComputedStyle(document.documentElement);
|
||
const lightness = '35%';
|
||
const chroma = styles.getPropertyValue('--c').trim();
|
||
|
||
const colors = new Set();
|
||
for (let i = 1; i <= 7; i++) {
|
||
let hue = styles.getPropertyValue(`--base-hue-${i}`).trim();
|
||
if (!hue) continue;
|
||
let color = `oklch(${lightness} ${chroma} ${hue})`;
|
||
colors.add(color);
|
||
}
|
||
return Array.from(colors);
|
||
}
|
||
|
||
function getNextLogBoxColor(colorList) {
|
||
if (usedLogBoxColors.length >= colorList.length) {
|
||
usedLogBoxColors = [];
|
||
}
|
||
const remaining = colorList.filter(c => !usedLogBoxColors.includes(c));
|
||
const next = remaining[Math.floor(Math.random() * remaining.length)];
|
||
usedLogBoxColors.push(next);
|
||
return next;
|
||
}
|
||
|
||
function rotateLogBoxColors() {
|
||
const colorList = getLogBoxColorListFromTheme();
|
||
if (colorList.length === 0) return;
|
||
|
||
document.querySelectorAll('.log-box[data-dynamic-bg="true"]').forEach(box => {
|
||
const color = getNextLogBoxColor(colorList);
|
||
box.style.background = color;
|
||
});
|
||
}
|
||
|
||
rotateColors();
|
||
rotateLogBoxColors();
|
||
setInterval(rotateColors, 4000);
|
||
setInterval(rotateLogBoxColors, 4000);
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.lyrics-mode {
|
||
.non-lyrics-content {
|
||
display: none !important;
|
||
}
|
||
|
||
#lyricsContainer {
|
||
height: calc(70vh - 150px) !important;
|
||
position: relative;
|
||
z-index: 1000;
|
||
}
|
||
|
||
#currentSong {
|
||
font-size: 1.5rem;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
#floatingLyrics {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
#lyricsToggle .bi {
|
||
transition: transform 0.3s;
|
||
}
|
||
.lyrics-mode #lyricsToggle .bi {
|
||
transform: rotate(180deg);
|
||
}
|
||
</style>
|
||
|
||
<script>
|
||
let isLyricsMode = localStorage.getItem('lyricsMode') === 'true';
|
||
|
||
function toggleLyricsMode() {
|
||
const modal = document.getElementById('musicModal');
|
||
const icon = document.getElementById('lyricsIcon');
|
||
|
||
isLyricsMode = !isLyricsMode;
|
||
modal.classList.toggle('lyrics-mode', isLyricsMode);
|
||
localStorage.setItem('lyricsMode', isLyricsMode);
|
||
|
||
icon.className = isLyricsMode ? 'bi bi-chevron-up' : 'bi bi-chevron-down';
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const icon = document.getElementById('lyricsIcon');
|
||
document.getElementById('lyricsToggle').addEventListener('click', toggleLyricsMode);
|
||
|
||
if (isLyricsMode) {
|
||
document.getElementById('musicModal').classList.add('lyrics-mode');
|
||
icon.className = 'bi bi-chevron-up';
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
window.toggleIpStatusBar = function() {
|
||
const ipStatusBar = document.getElementById('status-bar-component');
|
||
if (!ipStatusBar) return;
|
||
|
||
const isHidden = ipStatusBar.style.display === 'none';
|
||
|
||
if (isHidden) {
|
||
ipStatusBar.style.display = '';
|
||
localStorage.setItem('neko_ip_status_hidden', 'false');
|
||
if (typeof speakMessage === 'function') {
|
||
speakMessage(translations['ip_info_shown'] || 'IP Information Shown');
|
||
}
|
||
if (typeof showLogMessage === 'function') {
|
||
showLogMessage(translations['ip_info_shown'] || 'IP Information Shown');
|
||
}
|
||
} else {
|
||
ipStatusBar.style.display = 'none';
|
||
localStorage.setItem('neko_ip_status_hidden', 'true');
|
||
if (typeof speakMessage === 'function') {
|
||
speakMessage(translations['ip_info_hidden'] || 'IP Information Hidden');
|
||
}
|
||
if (typeof showLogMessage === 'function') {
|
||
showLogMessage(translations['ip_info_hidden'] || 'IP Information Hidden');
|
||
}
|
||
}
|
||
};
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const storedValue = localStorage.getItem('neko_ip_status_hidden');
|
||
const ipStatusHidden = storedValue === null || storedValue === 'true';
|
||
const ipStatusBar = document.getElementById('status-bar-component');
|
||
|
||
if (ipStatusBar) {
|
||
ipStatusBar.style.display = ipStatusHidden ? 'none' : '';
|
||
}
|
||
localStorage.setItem('neko_ip_status_hidden', ipStatusHidden.toString());
|
||
});
|
||
</script>
|
||
|
||
<script>
|
||
document.addEventListener("DOMContentLoaded", () => {
|
||
const btn = document.getElementById("fontSwitchBtn");
|
||
const icon = document.getElementById("fontSwitchIcon");
|
||
const body = document.body;
|
||
const storageKey = "fontSwitch";
|
||
|
||
const fonts = [
|
||
{ class: "default-font", key: "font_default", icon: "fa-font" },
|
||
{ class: "fredoka-font", key: "font_fredoka", icon: "fa-child-reaching" },
|
||
{ class: "cinzel-font", key: "font_cinzel", icon: "fa-crown" },
|
||
{ class: "system-nofo-font", key: "font_noto", icon: "fa-language" },
|
||
{ class: "noto-sans-font", key: "font_noto_sans", icon: "fa-globe" },
|
||
{ class: "system-mono-font", key: "font_mono", icon: "fa-code" },
|
||
{ class: "dm-serif-font", key: "font_dm_serif", icon: "fa-feather-pointed" }
|
||
];
|
||
|
||
const savedFont = localStorage.getItem(storageKey);
|
||
if (savedFont) {
|
||
const fontObj = fonts.find(f => f.class === savedFont);
|
||
body.classList.add(savedFont);
|
||
if (fontObj) updateIcon(fontObj.icon);
|
||
} else {
|
||
updateIcon("fa-font");
|
||
}
|
||
|
||
btn.addEventListener("click", () => {
|
||
const currentIndex = fonts.findIndex(f => body.classList.contains(f.class));
|
||
const nextIndex = (currentIndex + 1) % fonts.length;
|
||
const nextFont = fonts[nextIndex];
|
||
|
||
fonts.forEach(f => body.classList.remove(f.class));
|
||
body.classList.add(nextFont.class);
|
||
|
||
body.classList.add("font-transition");
|
||
|
||
setTimeout(() => {
|
||
body.classList.remove("font-transition");
|
||
}, 400);
|
||
|
||
localStorage.setItem(storageKey, nextFont.class);
|
||
|
||
updateIcon(nextFont.icon);
|
||
|
||
const message = translations[nextFont.key] || "Switched font";
|
||
if (typeof speakMessage === "function") speakMessage(message);
|
||
if (typeof showLogMessage === "function") showLogMessage(message);
|
||
});
|
||
|
||
function updateIcon(iconName) {
|
||
icon.className = `fa-solid ${iconName}`;
|
||
icon.style.color = "white";
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
:root {
|
||
--base-hue: 260;
|
||
--base-chroma: 0.03;
|
||
--danger-base: 15;
|
||
--base-hue-1: 20;
|
||
--base-hue-2: 200;
|
||
--base-hue-3: 135;
|
||
--base-hue-4: 80;
|
||
--base-hue-5: 270;
|
||
--base-hue-6: 170;
|
||
--base-hue-7: 340;
|
||
--l: 85%;
|
||
--c: 0.18;
|
||
--glass-blur: blur(20px);
|
||
--radius: 20px;
|
||
--glass-opacity: 0.85;
|
||
--glass-border: 1px solid rgba(255, 255, 255, 0.1);
|
||
--shadow-intensity: 0.25;
|
||
--text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||
--transition-speed: 0.3s;
|
||
--highlight-intensity: 0.8;
|
||
--border-strong: 1px solid oklch(70% 0.1 var(--base-hue) / 0.6);
|
||
--bg-body: oklch(40% var(--base-chroma) var(--base-hue) / 90%);
|
||
--bg-container: oklch(30% var(--base-chroma) var(--base-hue));
|
||
--text-primary: oklch(95% 0 0);
|
||
--accent-color: oklch(70% 0.2 calc(var(--base-hue) + 0));
|
||
--card-bg: oklch(25% var(--base-chroma) var(--base-hue));
|
||
--header-bg: oklch(35% var(--base-chroma) var(--base-hue));
|
||
--border-color: oklch(40% var(--base-chroma) var(--base-hue));
|
||
--btn-primary-bg: oklch(50% 0.18 var(--base-hue));
|
||
--btn-primary-hover: color-mix(in oklch, var(--btn-primary-bg), white 12%);
|
||
--btn-success-bg: oklch(55% 0.22 140);
|
||
--btn-info-bg: oklch(55% 0.22 220);
|
||
--btn-warning-bg: oklch(65% 0.18 80);
|
||
--btn-danger-bg: oklch(55% 0.25 var(--danger-base));
|
||
--item-hover-bg: color-mix(in oklch, var(--btn-primary-bg), white 15%);
|
||
--item-hover-shadow: 0 4px 12px oklch(0 0 0 / 0.4);
|
||
--drag-over-bg: oklch(35% 0.05 var(--base-hue) / 0.3);
|
||
--drag-over-shadow: 0 0 25px oklch(var(--base-hue) 0.2 0.4 / 0.3);
|
||
--text-secondary: oklch(75% 0.03 0);
|
||
--success-text: oklch(75% 0.22 140);
|
||
--info-text: oklch(75% 0.22 220);
|
||
--warning-text: oklch(80% 0.22 80);
|
||
--danger-text: oklch(70% 0.28 30);
|
||
--ocean-bg: oklch(48% 0.32 calc(var(--base-hue) + 220));
|
||
--forest-bg: oklch(42% 0.32 calc(var(--base-hue) + 140));
|
||
--rose-bg: oklch(48% 0.32 calc(var(--base-hue) + 350));
|
||
--lavender-bg: oklch(46% 0.32 calc(var(--base-hue) + 270));
|
||
--sand-bg: oklch(45% 0.32 calc(var(--base-hue) + 60));
|
||
--glass-blur: blur(20px);
|
||
--radius: 16px;
|
||
--transition: all 0.25s ease;
|
||
--primary-color: var(--accent-color);
|
||
--secondary-color: var(--btn-primary-bg);
|
||
--background: var(--bg-body);
|
||
--text-color: var(--text-primary);
|
||
}
|
||
|
||
[data-theme="light"] {
|
||
--base-hue: 200;
|
||
--base-chroma: 0.01;
|
||
--l: 60%;
|
||
--c: 0.25;
|
||
--bg-body: oklch(95% var(--base-chroma) var(--base-hue) / 90%);
|
||
--bg-container: oklch(99% var(--base-chroma) var(--base-hue));
|
||
--text-primary: oklch(25% var(--base-chroma) var(--base-hue));
|
||
--accent-color: oklch(60% 0.2 calc(var(--base-hue) + 60));
|
||
--card-bg: oklch(96% var(--base-chroma) var(--base-hue));
|
||
--header-bg: oklch(88% var(--base-chroma) var(--base-hue));
|
||
--border-color: oklch(85% 0.03 var(--base-hue));
|
||
--border-strong: 1px solid oklch(70% 0.1 var(--base-hue) / 0.6);
|
||
--btn-primary-bg: oklch(60% 0.35 var(--base-hue));
|
||
--btn-success-bg: oklch(75% 0.25 140);
|
||
--btn-info-bg: oklch(65% 0.3 220);
|
||
--btn-warning-bg: oklch(65% 0.25 80);
|
||
--btn-danger-bg: oklch(55% 0.35 var(--danger-base));
|
||
--item-hover-bg: color-mix(in oklch, var(--accent-color), white 25%);
|
||
--item-hover-shadow: 0 2px 15px oklch(0 0 0 / 0.15);
|
||
--drag-over-bg: oklch(92% 0.01 var(--base-hue) / 0.3);
|
||
--drag-over-shadow: 0 0 20px oklch(0 0 0 / 0.1);
|
||
--text-secondary: oklch(45% 0.03 0);
|
||
--success-text: oklch(40% 0.25 140);
|
||
--info-text: oklch(40% 0.25 220);
|
||
--warning-text: oklch(45% 0.25 80);
|
||
--danger-text: oklch(45% 0.35 30);
|
||
--ocean-bg: oklch(85% 0.18 calc(var(--base-hue) + 220));
|
||
--highlight-color: oklch(90% 0.15 calc(var(--base-hue) + 90));
|
||
--soft-highlight: oklch(85% 0.1 calc(var(--base-hue) + 90));
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Cinzel Decorative';
|
||
font-style: normal;
|
||
font-weight: 700;
|
||
src: url('./assets/webfonts/cinzel-decorative-v17-latin-700.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-family: 'Cinzel Decorative';
|
||
font-style: normal;
|
||
font-weight: 900;
|
||
src: url('./assets/webfonts/cinzel-decorative-v17-latin-900.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'Fredoka One';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
src: url('./assets/webfonts/fredoka-v16-latin-regular.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'Noto Serif SC';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
src: url('./assets/webfonts/noto-serif-sc-v31-latin-regular.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'Comic Neue';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
src: url('./assets/webfonts/comic-neue-v8-latin-regular.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'DM Serif Display';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
src: url('./assets/webfonts/dm-serif-display-v15-latin-regular.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'Noto Sans';
|
||
font-style: normal;
|
||
font-weight: 400;
|
||
src: url('./assets/webfonts/noto-sans-v39-cyrillic_cyrillic-ext_devanagari_greek_greek-ext_latin_latin-ext_vietnamese-regular.woff2') format('woff2');
|
||
}
|
||
|
||
@font-face {
|
||
font-display: swap;
|
||
font-family: 'Noto Sans';
|
||
font-style: normal;
|
||
font-weight: 500;
|
||
src: url('./assets/webfonts/noto-sans-v39-cyrillic_cyrillic-ext_devanagari_greek_greek-ext_latin_latin-ext_vietnamese-500.woff2') format('woff2');
|
||
}
|
||
|
||
body {
|
||
margin: 0;
|
||
color: var(--text-color);
|
||
background-attachment: fixed;
|
||
}
|
||
|
||
#playerModal.active {
|
||
display: flex;
|
||
animation: slideIn 0.3s ease forwards;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from {
|
||
transform: translateY(40px);
|
||
opacity: 0;
|
||
}
|
||
|
||
to {
|
||
transform: translateY(0);
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
.lyrics-container {
|
||
flex: 1;
|
||
overflow-y: auto;
|
||
padding: 12px;
|
||
margin-bottom: 15px;
|
||
border-radius: var(--radius);
|
||
background: var(--card-bg);
|
||
border: var(--border-strong);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
}
|
||
|
||
.lyric-line {
|
||
opacity: 1 !important;
|
||
color: var(--text-primary) !important;
|
||
font-size: 1.1rem;
|
||
transition: all 0.3s ease;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.lyric-line .char {
|
||
display: inline-block;
|
||
white-space: nowrap;
|
||
margin-right: 0.1rem;
|
||
}
|
||
|
||
.lyric-line .char.played {
|
||
background: linear-gradient(...);
|
||
}
|
||
|
||
.lyric-line.highlight {
|
||
color: var(--text-primary) !important;
|
||
font-size: 1.3rem;
|
||
}
|
||
|
||
.lyric-line.highlight .char {
|
||
transition: all 0.1s ease;
|
||
}
|
||
|
||
.lyric-line.highlight .char.active {
|
||
opacity: 1;
|
||
transform: scale(1.3);
|
||
background: linear-gradient(90deg,
|
||
oklch(65% 0.25 15) 0%,
|
||
oklch(70% 0.25 50) 25%,
|
||
oklch(75% 0.25 85) 50%,
|
||
oklch(70% 0.25 135) 75%,
|
||
oklch(65% 0.25 240) 100%);
|
||
background-size: 200% auto;
|
||
background-clip: text;
|
||
-webkit-background-clip: text;
|
||
color: transparent !important;
|
||
animation: color-flow 1s linear infinite;
|
||
0 0 10px rgba(255, 51, 102, 0.5),
|
||
0 0 15px rgba(102, 255, 51, 0.5),
|
||
0 0 20px rgba(51, 204, 255, 0.5);
|
||
}
|
||
|
||
.lyric-line.enter-active {
|
||
animation: textPop 0.5s ease;
|
||
}
|
||
|
||
@keyframes textPop {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(10px);
|
||
}
|
||
|
||
to {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
@keyframes color-flow {
|
||
0% {
|
||
background-position: 0% 50%;
|
||
}
|
||
|
||
50% {
|
||
background-position: 100% 50%;
|
||
}
|
||
|
||
100% {
|
||
background-position: 0% 50%;
|
||
}
|
||
}
|
||
|
||
.char.space {
|
||
display: inline;
|
||
min-width: 0.5em;
|
||
}
|
||
|
||
.progress-container {
|
||
width: 100%;
|
||
height: 6px;
|
||
background: var(--border-color);
|
||
border-radius: 4px;
|
||
margin: 16px 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: var(--accent-color);
|
||
transition: width 0.2s ease;
|
||
}
|
||
|
||
.controls {
|
||
display: flex;
|
||
justify-content: center;
|
||
gap: 20px;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.control-btn,
|
||
#volumeToggle {
|
||
background: var(--card-bg);
|
||
border: var(--border-strong);
|
||
color: var(--text-color);
|
||
width: 48px;
|
||
height: 48px;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
font-size: 1.2rem;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.control-btn:hover,
|
||
#volumeToggle:hover {
|
||
background: var(--item-hover-bg);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
#playPauseBtn {
|
||
width: 60px;
|
||
height: 60px;
|
||
font-size: 1.5rem;
|
||
background: var(--accent-color);
|
||
color: var(--text-primary);
|
||
box-shadow: 0 4px 20px rgba(var(--accent-color), 0.3);
|
||
}
|
||
|
||
.playlist {
|
||
margin-top: 20px;
|
||
max-height: 380px;
|
||
overflow-y: auto;
|
||
padding: 10px;
|
||
border-radius: var(--radius);
|
||
background: var(--card-bg);
|
||
box-shadow: inset 1px 1px 2px rgba(255, 255, 255, 0.1),
|
||
inset -1px -1px 2px rgba(0, 0, 0, 0.15),
|
||
inset 1px -1px 1px rgba(0, 0, 0, 0.08),
|
||
inset -1px 1px 1px rgba(0, 0, 0, 0.08);
|
||
border: var(--border-strong);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.lyrics-container:hover,
|
||
.playlist:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: inset 1px 1px 2px rgba(255, 255, 255, 0.1),
|
||
inset -1px -1px 2px rgba(0, 0, 0, 0.15),
|
||
inset 1px -1px 1px rgba(0, 0, 0, 0.08),
|
||
inset -1px 1px 1px rgba(0, 0, 0, 0.08),
|
||
0 4px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.playlist-item {
|
||
padding: 10px 14px;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease;
|
||
font-size: 0.95rem;
|
||
}
|
||
|
||
.playlist-item:hover {
|
||
background: var(--item-hover-bg);
|
||
color: #fff;
|
||
font-weight: bold;
|
||
}
|
||
|
||
.playlist-item.active {
|
||
background: var(--rose-bg);
|
||
color: white;
|
||
font-weight: bold;
|
||
}
|
||
|
||
#floatingLyrics {
|
||
position: fixed;
|
||
top: 1%;
|
||
left: 4.5%;
|
||
background: var(--bg-container);
|
||
padding: 15px 10px;
|
||
border-radius: 20px;
|
||
backdrop-filter: var(--glass-blur);
|
||
display: none;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
cursor: grab;
|
||
transition: opacity 0.3s ease;
|
||
writing-mode: vertical-rl;
|
||
text-orientation: mixed;
|
||
line-height: 2;
|
||
z-index: 1060;
|
||
flex-direction: column;
|
||
gap: 0.5em;
|
||
width: 200px;
|
||
resize: none;
|
||
overflow: auto;
|
||
user-select: none;
|
||
border: var(--border-strong);
|
||
}
|
||
|
||
@keyframes float {
|
||
from {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
to {
|
||
transform: translateY(-10px);
|
||
}
|
||
}
|
||
|
||
@keyframes breath {
|
||
|
||
0%,
|
||
100% {
|
||
--glow-primary: oklch(85% 0.32 var(--glow-base));
|
||
box-shadow: 0 0 10px 2px color-mix(in oklch, var(--glow-primary), transparent 50%),
|
||
inset 0 -10px 20px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
}
|
||
|
||
50% {
|
||
--glow-primary: oklch(92% 0.38 var(--glow-base));
|
||
box-shadow: 0 0 20px 4px color-mix(in oklch, var(--glow-primary), transparent 30%),
|
||
inset 0 -15px 30px color-mix(in oklch, var(--glow-primary), transparent 50%);
|
||
}
|
||
}
|
||
|
||
[data-theme="dark"] #floatingLyrics {
|
||
--glow-base: var(--base-hue);
|
||
--glow-primary: oklch(88% 0.35 var(--glow-base));
|
||
--glow-secondary: oklch(85% 0.3 calc(var(--glow-base) + 15));
|
||
border: 1px solid color-mix(in oklch, var(--glow-primary), transparent 10%);
|
||
box-shadow: 0 0 12px 2px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
inset 0 -12px 24px color-mix(in oklch, var(--glow-primary), transparent 60%);
|
||
animation: float 3s ease-in-out infinite alternate,
|
||
breath 4s ease-in-out infinite;
|
||
}
|
||
|
||
[data-theme="dark"] #floatingLyrics:hover {
|
||
animation-play-state: paused;
|
||
--glow-primary: oklch(92% 0.38 var(--glow-base));
|
||
box-shadow: 0 0 20px 4px color-mix(in oklch, var(--glow-primary), transparent 30%),
|
||
inset 0 -15px 30px color-mix(in oklch, var(--glow-primary), transparent 50%);
|
||
}
|
||
|
||
#floatingLyrics.visible {
|
||
display: flex;
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
cursor: move;
|
||
}
|
||
|
||
#floatingLyrics #floatingCurrentSong.vertical-title {
|
||
font-size: 1.8rem;
|
||
font-weight: 700;
|
||
writing-mode: vertical-rl;
|
||
padding-right: 0.5em;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3),
|
||
0px -1px 2px rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
#floatingLyrics .vertical-lyrics {
|
||
writing-mode: vertical-rl;
|
||
text-combine-upright: all;
|
||
}
|
||
|
||
#floatingLyrics .char {
|
||
font-size: 1.6rem;
|
||
transition: transform 0.3s ease;
|
||
display: inline-block;
|
||
position: relative;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.floating-controls {
|
||
display: flex;
|
||
flex-direction: row;
|
||
gap: 0.8em;
|
||
margin-bottom: 1em;
|
||
order: -1;
|
||
}
|
||
|
||
.ctrl-btn {
|
||
background: var(--bg-body);
|
||
border: var(--border-strong);
|
||
color: var(--text-primary) !important;
|
||
border-radius: 50%;
|
||
width: 36px;
|
||
height: 36px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
backdrop-filter: blur(5px);
|
||
}
|
||
|
||
.ctrl-btn:hover {
|
||
background: var(--item-hover-bg);
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
.ctrl-btn.clicked {
|
||
transform: scale(0.9);
|
||
background: rgba(50, 205, 50, 0.5);
|
||
}
|
||
|
||
.ctrl-btn i {
|
||
font-size: 1.6rem;
|
||
display: block;
|
||
vertical-align: middle;
|
||
line-height: 1;
|
||
margin: 0;
|
||
padding: 0;
|
||
}
|
||
|
||
#currentSong.vertical-title {
|
||
margin-top: 0.5em;
|
||
border-right: none;
|
||
padding-right: 0;
|
||
padding-bottom: 0.8em;
|
||
margin-right: 0;
|
||
writing-mode: horizontal-tb;
|
||
}
|
||
|
||
.vertical-lyrics {
|
||
margin-top: 0.5em;
|
||
overflow-x: hidden;
|
||
max-width: 100%;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.char {
|
||
transition: all 0.3s ease;
|
||
display: inline-block;
|
||
margin-right: 2px;
|
||
}
|
||
|
||
#floatingLyrics .char.active {
|
||
color: var(--accent-color);
|
||
animation: bounce-scale 0.6s ease-out;
|
||
transform: scale(1.3);
|
||
position: relative;
|
||
text-shadow: none !important;
|
||
}
|
||
|
||
@keyframes bounce-scale {
|
||
0% {
|
||
transform: scale(1);
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.3);
|
||
}
|
||
|
||
70% {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
100% {
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.char.played {
|
||
transform: scale(1) !important;
|
||
}
|
||
|
||
.playlist {
|
||
counter-reset: list-item;
|
||
}
|
||
|
||
.playlist-item::before {
|
||
content: counter(list-item) ".";
|
||
counter-increment: list-item;
|
||
margin-right: 8px;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.time-display {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
margin-top: 8px;
|
||
font-size: 0.9em;
|
||
color: var(--text-secondary);
|
||
}
|
||
|
||
.progress-container {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.char.active {
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
.char[data-start]+.char[data-start] {
|
||
margin-left: 12px;
|
||
}
|
||
|
||
.lyrics-loading {
|
||
position: relative;
|
||
min-height: 100px;
|
||
}
|
||
|
||
#no-lyrics {
|
||
text-align: center;
|
||
color: var(--text-secondary);
|
||
padding: 2rem;
|
||
font-size: 1.8em;
|
||
}
|
||
|
||
#noLyricsFloating {
|
||
width: min-content;
|
||
max-width: 4em;
|
||
text-align: center;
|
||
color: var(--text-primary) !important;
|
||
line-height: 1.2;
|
||
font-size: 1.5rem;
|
||
padding: 10px 2px;
|
||
letter-spacing: 0.2em;
|
||
writing-mode: vertical-rl;
|
||
text-orientation: upright;
|
||
}
|
||
|
||
@keyframes glow {
|
||
0% {
|
||
opacity: 0.8;
|
||
}
|
||
|
||
50% {
|
||
opacity: 1;
|
||
}
|
||
|
||
100% {
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
.progress-bar {
|
||
height: 100%;
|
||
background: var(--btn-success-bg);
|
||
border-radius: 4px;
|
||
transition: width 0.1s linear;
|
||
}
|
||
|
||
.progress-overlay {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
z-index: 2;
|
||
}
|
||
|
||
#currentSong {
|
||
font-weight: bold !important;
|
||
color: var(--accent-color);
|
||
text-shadow: 0px 1px 2px rgba(0, 0, 0, 0.3),
|
||
0px -1px 2px rgba(255, 255, 255, 0.4);
|
||
}
|
||
|
||
#volumePanel {
|
||
position: absolute;
|
||
bottom: 100%;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
margin-bottom: 10px;
|
||
background-color: var(--card-bg);
|
||
border: var(--border-strong);
|
||
border-radius: 0.5rem;
|
||
padding: 10px;
|
||
width: 160px;
|
||
box-shadow: 0 4px 10px var(--border-color);
|
||
z-index: 1000;
|
||
display: none;
|
||
}
|
||
|
||
#volumeSlider {
|
||
width: 100%;
|
||
accent-color: var(--text-color);
|
||
}
|
||
|
||
#volumeLabel {
|
||
color: var(--text-color);
|
||
font-size: 0.9rem;
|
||
text-align: right;
|
||
margin-top: 5px;
|
||
}
|
||
|
||
.heart {
|
||
position: absolute;
|
||
font-size: 2rem;
|
||
color: #ff69b4;
|
||
pointer-events: none;
|
||
opacity: 0;
|
||
z-index: 9999;
|
||
animation: heartAnimation 1s ease-in-out forwards;
|
||
}
|
||
|
||
@keyframes heartAnimation {
|
||
0% {
|
||
transform: scale(1) translateY(0);
|
||
opacity: 1;
|
||
}
|
||
|
||
50% {
|
||
transform: scale(1.5) translateY(-50px);
|
||
opacity: 1;
|
||
}
|
||
|
||
100% {
|
||
transform: scale(0) translateY(-100px);
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.log-box {
|
||
position: fixed;
|
||
left: 20px;
|
||
padding: 12px 16px;
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border-radius: 8px;
|
||
z-index: 9999;
|
||
max-width: 370px;
|
||
font-size: 15px;
|
||
word-wrap: break-word;
|
||
line-height: 1.5;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.15);
|
||
border: var(--border-strong);
|
||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
||
backdrop-filter: blur(2px);
|
||
transform: translateY(0);
|
||
opacity: 0;
|
||
animation: scrollUp 12s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
||
display: inline-block;
|
||
margin-bottom: 10px;
|
||
transition: transform 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
@keyframes scrollUp {
|
||
0% {
|
||
top: 90%;
|
||
opacity: 0;
|
||
}
|
||
|
||
20% {
|
||
opacity: 1;
|
||
}
|
||
|
||
80% {
|
||
top: 50%;
|
||
opacity: 1;
|
||
}
|
||
|
||
100% {
|
||
top: 45%;
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
.log-box.exiting {
|
||
animation: fadeOut 0.3s forwards;
|
||
}
|
||
|
||
.log-content {
|
||
padding: 6px 20px 6px 8px;
|
||
color: white;
|
||
}
|
||
|
||
.log-close-btn {
|
||
position: absolute;
|
||
top: 6px;
|
||
right: 10px;
|
||
background: transparent;
|
||
border: none;
|
||
color: inherit;
|
||
cursor: pointer;
|
||
font-size: 20px;
|
||
line-height: 1;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.2s;
|
||
}
|
||
|
||
.log-box:hover .log-close-btn {
|
||
opacity: 0.7;
|
||
pointer-events: auto;
|
||
}
|
||
|
||
.log-box:hover .log-close-btn:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
@keyframes fadeOut {
|
||
to {
|
||
opacity: 0;
|
||
transform: translateY(-20px) scale(0.95);
|
||
}
|
||
}
|
||
|
||
.log-icon {
|
||
display: inline-block;
|
||
width: 20px;
|
||
height: 20px;
|
||
margin-right: 3px;
|
||
vertical-align: middle;
|
||
}
|
||
|
||
.log-box.error {
|
||
background: linear-gradient(145deg, #ff4444, #cc0000);
|
||
}
|
||
|
||
.log-box.warning {
|
||
background: linear-gradient(145deg, #ffc107, #ffab00);
|
||
}
|
||
|
||
.log-box.info {
|
||
background: linear-gradient(145deg, #2196F3, #1976D2);
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.log-box {
|
||
left: 10px;
|
||
right: 10px;
|
||
max-width: none;
|
||
font-size: 14px;
|
||
}
|
||
}
|
||
|
||
.list-group-item {
|
||
cursor: pointer;
|
||
color: var(--text-primary);
|
||
background: var(--bg-container);
|
||
border: var(--border-strong);
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
.list-group-item:hover {
|
||
background: var(--item-hover-bg);
|
||
color: white !important;
|
||
}
|
||
|
||
.list-group-item:hover .text-muted,
|
||
.list-group-item:hover .text-truncate {
|
||
color: white !important;
|
||
}
|
||
|
||
.list-group-item.active {
|
||
background: var(--accent-color);
|
||
color: white;
|
||
border: 1px solid var(--accent-color);
|
||
}
|
||
|
||
.list-group-item.active .badge,
|
||
.list-group-item.active .text-truncate,
|
||
.list-group-item.active small,
|
||
.list-group-item.active i {
|
||
color: white !important;
|
||
}
|
||
|
||
.list-group-item .delete-item {
|
||
cursor: pointer;
|
||
}
|
||
|
||
.modal-xl {
|
||
max-width: 60% !important;
|
||
width: 90% !important;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.modal-xl {
|
||
max-width: 95% !important;
|
||
width: 95% !important;
|
||
margin: 1rem auto !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 576px) {
|
||
.modal-xl {
|
||
max-width: 100% !important;
|
||
width: 100% !important;
|
||
margin: 0.5rem auto !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container-sm .btn i {
|
||
margin-right: 0;
|
||
}
|
||
|
||
.container-sm .btn {
|
||
font-size: 9px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.modal-body img {
|
||
display: none;
|
||
}
|
||
|
||
.datetime-container {
|
||
font-size: 17px !important;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
|
||
.section-container .nav-pills {
|
||
justify-content: center !important;
|
||
}
|
||
|
||
.section-container .nav-pills .nav-link {
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
.control-panel-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background: none !important;
|
||
backdrop-filter: none !important;
|
||
display: none;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1050;
|
||
backdrop-filter: blur(3px);
|
||
transition: opacity 0.3s ease;
|
||
}
|
||
|
||
.control-panel {
|
||
--primary-gradient: linear-gradient(135deg, #0f3460, #1a508b, #2d8bc0);
|
||
--button-hover: rgba(255, 255, 255, 0.2);
|
||
--panel-padding: 20px;
|
||
background: var(--primary-gradient);
|
||
border-radius: 20px;
|
||
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
|
||
overflow: hidden;
|
||
padding: var(--panel-padding);
|
||
position: relative;
|
||
z-index: 10;
|
||
max-width: 1000px;
|
||
width: 90%;
|
||
margin: 0;
|
||
display: block;
|
||
}
|
||
|
||
.control-panel::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiPjxkZWZzPjxwYXR0ZXJuIGlkPSJwYXR0ZXJuIiB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiIHBhdHRlcm5UcmFuc2Zvcm09InJvdGF0ZSg0NSkiPjxjaXJjbGUgY3g9IjIwIiBjeT0iMjAiIHI9IjAuNSIgZmlsbD0iIzEzNDI3OSIvPjwvcGF0dGVybj48L2RlZnM+PHJlY3Qgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIgZmlsbD0idXJsKCNwYXR0ZXJuKSIgb3BhY2l0eT0iMC4xIi8+PC9zdmc+');
|
||
opacity: 0.1;
|
||
z-index: -1;
|
||
}
|
||
|
||
.panel-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 25px;
|
||
padding-bottom: 15px;
|
||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.panel-header h3 {
|
||
margin: 0;
|
||
font-weight: 600;
|
||
font-size: 1.8rem;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
color: var(--accent-color);
|
||
}
|
||
|
||
.panel-header h3 i {
|
||
background: rgba(255, 255, 255, 0.15);
|
||
width: 50px;
|
||
height: 50px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.close-icon {
|
||
background: #e74c3c;
|
||
border: none;
|
||
color: white;
|
||
width: 35px;
|
||
height: 35px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||
transform: scale(1);
|
||
}
|
||
|
||
.close-icon:hover {
|
||
transform: scale(1.1);
|
||
box-shadow: 0 6px 15px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.close-icon i,
|
||
.close-icon svg,
|
||
.close-icon span {
|
||
transition: transform 0.3s ease;
|
||
transform: rotate(0deg);
|
||
}
|
||
|
||
.close-icon:hover i,
|
||
.close-icon:hover svg {
|
||
transform: rotate(180deg);
|
||
}
|
||
|
||
.buttons-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.action-row {
|
||
display: flex;
|
||
justify-content: flex-end;
|
||
gap: 15px;
|
||
padding-top: 15px;
|
||
border-top: 1px solid rgba(255, 255, 255, 0.1);
|
||
}
|
||
|
||
.panel-btn {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: none;
|
||
color: white;
|
||
padding: 15px;
|
||
border-radius: 12px;
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
width: 100%;
|
||
text-align: left;
|
||
backdrop-filter: blur(5px);
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.panel-btn:hover {
|
||
background: var(--button-hover);
|
||
transform: translateY(-3px);
|
||
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.panel-btn:active {
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.panel-btn i {
|
||
font-size: 1.4rem;
|
||
min-width: 30px;
|
||
}
|
||
|
||
.btn-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 40px;
|
||
height: 40px;
|
||
background: rgba(255, 255, 255, 0.15);
|
||
border-radius: 10px;
|
||
font-size: 1.2rem;
|
||
}
|
||
|
||
.btn-icon i {
|
||
margin-left: 0.6ch;
|
||
position: relative;
|
||
top: 0.09em;
|
||
}
|
||
|
||
.color-picker {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 15px;
|
||
background: rgba(255, 255, 255, 0.1);
|
||
padding: 10px 15px;
|
||
border-radius: 12px;
|
||
backdrop-filter: blur(5px);
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.color-picker label {
|
||
font-weight: 500;
|
||
white-space: nowrap;
|
||
color: white !important;
|
||
}
|
||
|
||
.color-picker input {
|
||
width: 40px;
|
||
height: 32px;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
padding: 0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.panel-close-btn {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border: none;
|
||
color: white;
|
||
padding: 10px 25px;
|
||
border-radius: 12px;
|
||
font-weight: 500;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
backdrop-filter: blur(5px);
|
||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.panel-close-btn:hover {
|
||
background: rgba(231, 76, 60, 0.2);
|
||
transform: translateY(-3px) scale(1.05);
|
||
box-shadow: 0 7px 20px rgba(0, 0, 0, 0.2);
|
||
}
|
||
|
||
.language-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 25px;
|
||
}
|
||
|
||
.flag-icon {
|
||
width: 50px;
|
||
height: 40px;
|
||
border-radius: 3px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
@media (max-width: 992px) {
|
||
.buttons-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
|
||
@media (max-width: 576px) {
|
||
.buttons-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.action-row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.color-picker {
|
||
width: 100%;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.panel-close-btn {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
.flag-container {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
}
|
||
|
||
.flag-icon {
|
||
width: 30px;
|
||
height: 20px;
|
||
border-radius: 3px;
|
||
object-fit: cover;
|
||
}
|
||
|
||
label[for="newPath"],
|
||
label[for="permissions"],
|
||
.form-text {
|
||
color: white !important;
|
||
}
|
||
|
||
.container-bg {
|
||
border-radius: 12px;
|
||
box-shadow: var(--bs-shadow-medium);
|
||
padding: 2rem;
|
||
margin-top: 2rem;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
#log-message {
|
||
transition: opacity 0.5s ease;
|
||
}
|
||
|
||
@media (max-width: 767.98px) {
|
||
|
||
#colorModal .modal-dialog,
|
||
#colorPickerModal .modal-dialog {
|
||
max-height: 95vh;
|
||
margin: 1rem;
|
||
}
|
||
|
||
#colorModal .modal-content,
|
||
#colorPickerModal .modal-content {
|
||
max-height: 95vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
#colorModal .modal-body,
|
||
#colorPickerModal .modal-body {
|
||
overflow-y: auto;
|
||
max-height: calc(90vh - 120px);
|
||
padding: 15px;
|
||
}
|
||
|
||
#colorModal .row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
#colorModal .col-md-6 {
|
||
width: 100%;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
#colorModal .card {
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
#colorModal .mb-3 {
|
||
margin-bottom: 12px !important;
|
||
}
|
||
|
||
#colorModal .form-label {
|
||
font-size: 0.9rem;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
#colorModal .d-flex {
|
||
gap: 8px;
|
||
}
|
||
|
||
#colorModal .gap-2>button {
|
||
width: 24px;
|
||
height: 24px;
|
||
padding: 0;
|
||
}
|
||
|
||
#colorPickerModal .modal-body>.row {
|
||
flex-direction: column;
|
||
}
|
||
|
||
#colorPickerModal .col-md-6 {
|
||
width: 100%;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
#colorPickerModal .form-label {
|
||
font-size: 0.9rem;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
#colorPickerModal .mb-3 {
|
||
margin-bottom: 12px !important;
|
||
}
|
||
|
||
#colorPickerModal #color-preview {
|
||
height: 80px;
|
||
}
|
||
|
||
#colorPickerModal .d-flex {
|
||
gap: 8px;
|
||
}
|
||
|
||
#colorPickerModal #current-color-block {
|
||
height: 40px;
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
#colorPickerModal #apply-color,
|
||
#colorPickerModal #reset-color {
|
||
height: 42px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
html,
|
||
body {
|
||
overflow-y: scroll !important;
|
||
scrollbar-width: none !important;
|
||
-ms-overflow-style: none !important;
|
||
}
|
||
|
||
html::-webkit-scrollbar,
|
||
body::-webkit-scrollbar {
|
||
display: none !important;
|
||
width: 0 !important;
|
||
background: transparent !important;
|
||
}
|
||
|
||
.link-box a {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
h2#neko-title.neko-title-style {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
.table.custom-table thead th {
|
||
color: var(--purple-text) !important;
|
||
}
|
||
|
||
.container-sm .row a {
|
||
color: var(--accent-color) !important;
|
||
text-decoration: none;
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
.container-sm .row a:hover {
|
||
color: var(--purple-text) !important;
|
||
}
|
||
|
||
.container-sm .row a .bi {
|
||
color: inherit;
|
||
}
|
||
|
||
.form-select option {
|
||
background-color: var(--header-bg) !important;
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.form-select {
|
||
color: var(--accent-color) !important;
|
||
border: var(--border-strong) !important;
|
||
}
|
||
|
||
.form-select option:hover {
|
||
background: var(--border-color) !important;
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
.modal-footer {
|
||
position: relative;
|
||
height: 60px;
|
||
flex-shrink: 0;
|
||
border: none !important;
|
||
box-shadow: none !important;
|
||
background-color: var(--header-bg) !important;
|
||
border-top: 1px solid var(--border-color) !important;
|
||
}
|
||
|
||
.container-bg,
|
||
.modal-content,
|
||
.table {
|
||
--bg-l: oklch(30% 0 0);
|
||
color: oklch(calc(100% - var(--bg-l)) 0 0) !important;
|
||
}
|
||
|
||
.container-bg {
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
background: var(--bg-container);
|
||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.card {
|
||
--card-padding: 1.25rem;
|
||
--card-border-width: 1px;
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-width: 0;
|
||
background: var(--bg-container) !important;
|
||
border: var(--border-strong);
|
||
border-radius: var(--radius);
|
||
box-shadow: 0 2px 8px color-mix(in oklch, var(--border-color), transparent 70%);
|
||
transform: translateY(-2px);
|
||
transition: var(--transition);
|
||
overflow: hidden;
|
||
}
|
||
|
||
.card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 6px 16px color-mix(in oklch, var(--border-color), transparent 60%),
|
||
0 0 0 1px var(--accent-color);
|
||
}
|
||
|
||
.card-header {
|
||
padding: calc(var(--card-padding) * 0.8) var(--card-padding);
|
||
background: var(--header-bg);
|
||
border-bottom: var(--card-border-width) solid var(--border-color);
|
||
font-weight: 600;
|
||
color: var(--text-primary);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.card-body {
|
||
padding: var(--card-padding);
|
||
flex: 1 1 auto;
|
||
color: var(--text-primary);
|
||
background: var(--bg-container);
|
||
}
|
||
|
||
.card-footer {
|
||
padding: calc(var(--card-padding) * 0.8) var(--card-padding);
|
||
background: var(--header-bg);
|
||
border-top: var(--card-border-width) solid var(--border-color);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.75rem;
|
||
}
|
||
|
||
.card-glass {
|
||
background: color-mix(in oklch, var(--card-bg), transparent 15%);
|
||
backdrop-filter: var(--glass-blur);
|
||
border-color: color-mix(in oklch, var(--border-color), white 30%);
|
||
}
|
||
|
||
.card-elevated {
|
||
--card-shadow-color: color-mix(in oklch, var(--border-color), black 30%);
|
||
box-shadow: 0 3px 6px var(--card-shadow-color),
|
||
0 8px 24px color-mix(in oklch, var(--card-shadow-color), transparent 70%);
|
||
}
|
||
|
||
.card-floating {
|
||
position: relative;
|
||
top: 0;
|
||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||
}
|
||
|
||
.card-floating:hover {
|
||
top: -4px;
|
||
box-shadow: 0 12px 28px color-mix(in oklch, var(--border-color), transparent 60%),
|
||
0 0 0 1px var(--accent-color);
|
||
}
|
||
|
||
.card-badge {
|
||
position: absolute;
|
||
top: -0.5rem;
|
||
right: 1rem;
|
||
background: var(--btn-danger-bg);
|
||
color: var(--text-primary);
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: calc(var(--radius) * 2);
|
||
font-size: 0.75rem;
|
||
font-weight: bold;
|
||
box-shadow: 0 2px 4px oklch(0% 0 0 / 0.2);
|
||
z-index: 1;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.card {
|
||
--card-padding: 1rem;
|
||
border-radius: calc(var(--radius) * 0.8);
|
||
}
|
||
|
||
.card-elevated {
|
||
box-shadow: 0 2px 4px color-mix(in oklch, var(--border-color), transparent 70%),
|
||
0 4px 12px color-mix(in oklch, var(--border-color), transparent 80%);
|
||
}
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.card {
|
||
--card-shadow-color: oklch(0% 0 0 / 0.4);
|
||
}
|
||
|
||
.card-glass {
|
||
background: color-mix(in oklch, var(--card-bg), transparent 20%);
|
||
border-color: color-mix(in oklch, var(--border-color), black 30%);
|
||
}
|
||
}
|
||
|
||
[data-theme="dark"] .container-sm .card {
|
||
--glow-base: var(--base-hue);
|
||
--glow-primary: oklch(82% 0.32 var(--glow-base));
|
||
--glow-secondary: oklch(78% 0.28 calc(var(--glow-base) + 10));
|
||
border: 1px solid color-mix(in oklch, var(--glow-primary), transparent 20%);
|
||
box-shadow: 0 2px 8px 1px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
0 4px 24px 2px color-mix(in oklch, var(--glow-secondary), transparent 60%),
|
||
inset 0 0 12px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
transform: translateY(-2px) !important;
|
||
transition: all 0.25s ease-out;
|
||
animation: breath 3s ease-in-out infinite;
|
||
}
|
||
|
||
[data-theme="dark"] .container-sm .card:hover {
|
||
--glow-primary: oklch(88% 0.35 var(--glow-base));
|
||
--glow-secondary: oklch(85% 0.3 calc(var(--glow-base) + 15));
|
||
box-shadow: 0 4px 16px 2px color-mix(in oklch, var(--glow-primary), transparent 30%),
|
||
0 8px 32px 4px color-mix(in oklch, var(--glow-secondary), transparent 50%),
|
||
inset 0 0 24px color-mix(in oklch, var(--glow-primary), transparent 60%);
|
||
transform: translateY(-4px) !important;
|
||
/* animation-play-state: paused; */
|
||
}
|
||
|
||
@keyframes breath {
|
||
|
||
0%,
|
||
100% {
|
||
--glow-primary: oklch(82% 0.32 var(--glow-base));
|
||
--glow-secondary: oklch(78% 0.28 calc(var(--glow-base) + 10));
|
||
box-shadow: 0 2px 8px 1px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
0 4px 24px 2px color-mix(in oklch, var(--glow-secondary), transparent 60%),
|
||
inset 0 0 12px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
}
|
||
|
||
50% {
|
||
--glow-primary: oklch(88% 0.35 var(--glow-base));
|
||
--glow-secondary: oklch(85% 0.3 calc(var(--glow-base) + 15));
|
||
box-shadow: 0 4px 16px 2px color-mix(in oklch, var(--glow-primary), transparent 30%),
|
||
0 8px 32px 4px color-mix(in oklch, var(--glow-secondary), transparent 50%),
|
||
inset 0 0 24px color-mix(in oklch, var(--glow-primary), transparent 60%);
|
||
}
|
||
}
|
||
|
||
[data-theme="dark"] .neko-title-style {
|
||
--glow-base: var(--base-hue);
|
||
--glow-primary: oklch(92% 0.35 var(--glow-base));
|
||
--glow-secondary: oklch(88% 0.3 calc(var(--glow-base) + 10));
|
||
animation: nekoBreath 3s ease-in-out infinite;
|
||
text-shadow: 1px 1px 0 color-mix(in oklch, var(--glow-primary), #999 50%),
|
||
2px 2px 0 color-mix(in oklch, var(--glow-primary), #888 50%),
|
||
3px 3px 0 color-mix(in oklch, var(--glow-primary), #777 50%),
|
||
4px 4px 0 color-mix(in oklch, var(--glow-primary), #666 50%),
|
||
0 0 10px color-mix(in oklch, var(--glow-primary), transparent 60%),
|
||
0 0 20px color-mix(in oklch, var(--glow-secondary), transparent 70%) !important;
|
||
}
|
||
|
||
[data-theme="dark"] .neko-title-style:hover {
|
||
--glow-primary: oklch(96% 0.38 var(--glow-base));
|
||
--glow-secondary: oklch(92% 0.32 calc(var(--glow-base) + 15));
|
||
text-shadow: 1px 1px 1px color-mix(in oklch, var(--glow-primary), #999 50%),
|
||
2px 2px 1px color-mix(in oklch, var(--glow-primary), #888 50%),
|
||
3px 3px 2px color-mix(in oklch, var(--glow-primary), #777 50%),
|
||
4px 4px 2px color-mix(in oklch, var(--glow-primary), #666 50%),
|
||
0 0 15px color-mix(in oklch, var(--glow-primary), transparent 50%),
|
||
0 0 30px color-mix(in oklch, var(--glow-secondary), transparent 60%) !important;
|
||
}
|
||
|
||
@keyframes nekoBreath {
|
||
|
||
0%,
|
||
100% {
|
||
--glow-primary: oklch(88% 0.32 var(--glow-base));
|
||
text-shadow: 1px 1px 0 color-mix(in oklch, var(--glow-primary), #999 50%),
|
||
2px 2px 0 color-mix(in oklch, var(--glow-primary), #888 50%),
|
||
3px 3px 0 color-mix(in oklch, var(--glow-primary), #777 50%),
|
||
4px 4px 0 color-mix(in oklch, var(--glow-primary), #666 50%),
|
||
0 0 8px color-mix(in oklch, var(--glow-primary), transparent 70%),
|
||
0 0 15px color-mix(in oklch, var(--glow-secondary), transparent 80%);
|
||
}
|
||
|
||
50% {
|
||
--glow-primary: oklch(96% 0.38 var(--glow-base));
|
||
text-shadow: 1px 1px 0 color-mix(in oklch, var(--glow-primary), #999 50%),
|
||
2px 2px 0 color-mix(in oklch, var(--glow-primary), #888 50%),
|
||
3px 3px 0 color-mix(in oklch, var(--glow-primary), #777 50%),
|
||
4px 4px 0 color-mix(in oklch, var(--glow-primary), #666 50%),
|
||
0 0 15px color-mix(in oklch, var(--glow-primary), transparent 50%),
|
||
0 0 30px color-mix(in oklch, var(--glow-secondary), transparent 60%);
|
||
}
|
||
}
|
||
|
||
.footer-text {
|
||
color: var(--accent-color) !important;
|
||
font-weight: 700;
|
||
}
|
||
|
||
.footer-text b {
|
||
font-weight: 800;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
|
||
.log-content-container {
|
||
border: var(--border-strong);
|
||
border-radius: var(--radius);
|
||
margin-bottom: 0.5rem;
|
||
overflow: hidden;
|
||
box-shadow: var(--item-hover-shadow);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.log-actions {
|
||
padding: 0.5rem 0;
|
||
background: none !important;
|
||
border-radius: 0 !important;
|
||
border: none !important;
|
||
display: flex;
|
||
justify-content: center;
|
||
}
|
||
|
||
.log-actions.multiple-actions {
|
||
padding: 0.5rem;
|
||
background: none !important;
|
||
border-radius: 0 !important;
|
||
border: none !important;
|
||
justify-content: center;
|
||
}
|
||
|
||
.log-action-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
flex-wrap: wrap;
|
||
width: auto;
|
||
background: none !important;
|
||
border-radius: 0 !important;
|
||
border: none !important;
|
||
}
|
||
|
||
.log-content-container:hover {
|
||
border-color: var(--accent-color);
|
||
box-shadow: 0 4px 16px color-mix(in oklch, var(--accent-color), transparent 85%);
|
||
}
|
||
|
||
.log-content-area {
|
||
display: block;
|
||
width: 100%;
|
||
height: 330px;
|
||
margin-top: 15px;
|
||
margin-left: 15px;
|
||
padding: 0.75rem;
|
||
margin: 0;
|
||
font-family: Consolas, "Courier New", monospace;
|
||
font-size: 0.875rem;
|
||
line-height: 1.5;
|
||
color: var(--text-primary);
|
||
background: var(--bg-container);
|
||
border: 0;
|
||
border-radius: 0;
|
||
resize: vertical;
|
||
white-space: pre-wrap;
|
||
overflow: auto;
|
||
box-shadow: inset 0 1px 3px oklch(0% 0 0 / 0.1);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.log-content-area:focus {
|
||
outline: none;
|
||
box-shadow: inset 0 1px 3px oklch(0% 0 0 / 0.15),
|
||
0 0 0 2px var(--accent-color);
|
||
}
|
||
|
||
.btn-clear-log {
|
||
color: #fff;
|
||
background: var(--btn-danger-bg);
|
||
border: var(--border-strong);
|
||
padding: 0.375rem 0.75rem;
|
||
font-size: 0.875rem;
|
||
border-radius: calc(var(--radius) / 2);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.btn-clear-log:hover {
|
||
color: #fff;
|
||
background: color-mix(in oklch, var(--btn-danger-bg), black 10%);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.btn-schedule {
|
||
color: #fff;
|
||
background: var(--btn-primary-bg);
|
||
border: var(--border-strong);
|
||
padding: 0.375rem 0.75rem;
|
||
font-size: 0.875rem;
|
||
border-radius: calc(var(--radius) / 2);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.btn-schedule:hover {
|
||
color: #fff;
|
||
background: var(--btn-primary-hover);
|
||
box-shadow: 0 2px 8px color-mix(in oklch, var(--accent-color), transparent 80%);
|
||
}
|
||
|
||
.form-check {
|
||
margin-bottom: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.form-check-input {
|
||
width: 1.1em;
|
||
height: 1.1em;
|
||
margin: 0;
|
||
background-color: var(--bg-container);
|
||
border: var(--border-strong);
|
||
appearance: none;
|
||
-webkit-appearance: none;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.form-check-input:checked {
|
||
background-color: var(--accent-color);
|
||
border-color: var(--accent-color);
|
||
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e");
|
||
}
|
||
|
||
.form-check-label {
|
||
color: var(--text-secondary);
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.log-content-container {
|
||
border-radius: calc(var(--radius) * 0.8);
|
||
}
|
||
|
||
.log-action-group {
|
||
flex-direction: column;
|
||
align-items: stretch;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.btn-clear-log,
|
||
.btn-schedule {
|
||
width: 100%;
|
||
justify-content: center;
|
||
}
|
||
|
||
.form-check {
|
||
justify-content: center;
|
||
}
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
.log-content-area {
|
||
box-shadow: inset 0 1px 3px oklch(0% 0 0 / 0.3);
|
||
}
|
||
|
||
.form-check-input {
|
||
background-color: var(--bg-body);
|
||
}
|
||
}
|
||
|
||
.table {
|
||
--bs-table-bg: var(--card-bg);
|
||
--bs-table-color: var(--text-primary);
|
||
--bs-table-border-color: var(--border-color);
|
||
--bs-table-striped-bg: rgba(0, 0, 0, 0.05);
|
||
}
|
||
|
||
.img-thumbnail {
|
||
background: var(--bg-container);
|
||
border: var(--border-strong);
|
||
}
|
||
|
||
#toggleButton {
|
||
background-color: var(--sand-bg);
|
||
}
|
||
|
||
.modal-content {
|
||
background: var(--bg-container);
|
||
border: var(--border-strong);
|
||
}
|
||
|
||
.modal-header {
|
||
background: var(--header-bg);
|
||
border-bottom: 1px solid var(--border-color);
|
||
}
|
||
|
||
.modal-title {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
.modal-body {
|
||
background: var(--card-bg);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
label {
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.btn-primary,
|
||
.btn-success,
|
||
.btn-info,
|
||
.btn-warning,
|
||
.btn btn-danger,
|
||
.btn-pink {
|
||
color: white !important;
|
||
}
|
||
|
||
.container-bg {
|
||
border-radius: 12px !important;
|
||
margin-top: 1rem !important;
|
||
}
|
||
|
||
.neko-title-style {
|
||
font-family: 'Cinzel Decorative', cursive;
|
||
font-weight: 900;
|
||
font-size: 80px;
|
||
letter-spacing: 4px;
|
||
text-align: center;
|
||
margin-top: 20px;
|
||
transition: all var(--bs-transition-speed);
|
||
color: #fff;
|
||
text-shadow: 1px 1px 0 #999,
|
||
2px 2px 0 #888,
|
||
3px 3px 0 #777,
|
||
4px 4px 0 #666,
|
||
5px 5px 10px rgba(0, 0, 0, 0.3);
|
||
transform-style: preserve-3d;
|
||
perspective: 1000px;
|
||
}
|
||
|
||
.neko-title-style:hover {
|
||
transform: rotateX(5deg) rotateY(-5deg) skew(-5deg);
|
||
text-shadow: 1px 1px 1px #999,
|
||
2px 2px 1px #888,
|
||
3px 3px 2px #777,
|
||
4px 4px 2px #666,
|
||
5px 5px 10px rgba(0, 0, 0, 0.4);
|
||
}
|
||
|
||
.form-select {
|
||
background-color: var(--card-bg) !important;
|
||
}
|
||
|
||
.form-control {
|
||
background-color: var(--card-bg) !important;
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.form-control:focus {
|
||
background-color: var(--card-bg) !important;
|
||
border-color: #4eedf9 !important;
|
||
box-shadow: none !important;
|
||
}
|
||
|
||
table td,
|
||
.alert.alert-info {
|
||
background-color: var(--card-bg) !important;
|
||
border: var(--border-strong) !important;
|
||
}
|
||
|
||
.form-control::placeholder {
|
||
color: var(--text-primary) !important;
|
||
opacity: 1;
|
||
}
|
||
|
||
.form-label,
|
||
legend.form-label {
|
||
font-size: 1rem !important;
|
||
font-weight: 500 !important;
|
||
line-height: 1.5 !important;
|
||
}
|
||
|
||
#customTemplateUrl {
|
||
display: none;
|
||
}
|
||
|
||
input[type="text"] {
|
||
background-color: var(--header-bg);
|
||
color: var(--text-primary);
|
||
border: var(--border-strong);
|
||
}
|
||
|
||
#color-input {
|
||
background: transparent !important;
|
||
border: none !important;
|
||
color: white !important;
|
||
font-size: 0.9rem !important;
|
||
width: 100% !important;
|
||
text-align: center !important;
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
box-shadow: none !important;
|
||
outline: none !important;
|
||
}
|
||
|
||
.input-wrapper {
|
||
position: relative;
|
||
display: inline-block;
|
||
width: 100%;
|
||
}
|
||
|
||
.fake-cursor {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(calc(-50% + 3.7ch), -50%);
|
||
color: white;
|
||
animation: blinkCursor 1s step-start infinite;
|
||
pointer-events: none;
|
||
font-size: 0.9rem;
|
||
font-weight: normal;
|
||
}
|
||
|
||
@keyframes blinkCursor {
|
||
50% {
|
||
opacity: 0;
|
||
}
|
||
}
|
||
|
||
footer {
|
||
margin-top: 10px !important;
|
||
padding: 8px 0;
|
||
color: var(--btn-primary-bg);
|
||
font-size: 1.1rem;
|
||
font-weight: 500;
|
||
}
|
||
|
||
footer p {
|
||
margin: 0;
|
||
}
|
||
|
||
h1 {
|
||
color: oklch(var(--l) var(--c) var(--base-hue)) !important;
|
||
font-weight: 900;
|
||
}
|
||
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
color: var(--accent-color) !important;
|
||
font-weight: bold;
|
||
}
|
||
|
||
h1:hover,
|
||
h2:hover {
|
||
opacity: 0.9;
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
a.link-primary:hover {
|
||
color: var(--ocean-bg) !important;
|
||
}
|
||
|
||
a.link-primary:hover svg,
|
||
a.link-primary:hover svg path {
|
||
stroke: var(--ocean-bg) !important;
|
||
fill: var(--ocean-bg) !important;
|
||
}
|
||
|
||
.row>a.btn,
|
||
.row>a.btn i,
|
||
.row>a.btn span {
|
||
transition: color 0.3s ease;
|
||
}
|
||
|
||
.row>a.btn:hover,
|
||
.row>a.btn:hover i,
|
||
.row>a.btn:hover span {
|
||
color: var(--ocean-bg) !important;
|
||
}
|
||
|
||
.triangle-icon {
|
||
border-top: 12px solid var(--accent-color) !important;
|
||
}
|
||
|
||
table {
|
||
border-collapse: separate;
|
||
border-spacing: 0;
|
||
width: 100%;
|
||
}
|
||
|
||
thead.table-light th {
|
||
font-weight: 600;
|
||
padding: 0.85rem 1.25rem;
|
||
border: none !important;
|
||
background: var(--accent-color) !important;
|
||
color: #fff !important;
|
||
text-align: left;
|
||
font-size: 0.925rem;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
thead.table-light th:hover {
|
||
background: color-mix(in oklch, var(--accent-color), black 10%) !important;
|
||
}
|
||
|
||
thead.table-light tr:first-child th:first-child {
|
||
border-top-left-radius: var(--radius) !important;
|
||
}
|
||
|
||
thead.table-light tr:first-child th:last-child {
|
||
border-top-right-radius: var(--radius) !important;
|
||
}
|
||
|
||
thead.table-light th:not(:last-child)::after {
|
||
content: none;
|
||
}
|
||
|
||
.btn-primary,
|
||
.btn-primary svg,
|
||
.btn-primary svg * {
|
||
color: white !important;
|
||
stroke: white !important;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
h1 {
|
||
font-size: 1.8rem;
|
||
}
|
||
|
||
h2 {
|
||
font-size: 1.5rem;
|
||
}
|
||
}
|
||
|
||
.alert,
|
||
.alert.alert-info ul,
|
||
.alert.alert-info ul li {
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
button.btn,
|
||
a.btn,
|
||
input.btn {
|
||
transition: transform 0.2s ease !important;
|
||
}
|
||
|
||
button.btn:hover,
|
||
a.btn:hover,
|
||
input.btn:hover,
|
||
button.btn:focus,
|
||
a.btn:focus,
|
||
input.btn:focus {
|
||
transform: scale(1.05) !important;
|
||
}
|
||
|
||
#logTabs .nav-link:hover {
|
||
color: var(--ocean-bg) !important;
|
||
}
|
||
|
||
svg.feather {
|
||
width: 20px !important;
|
||
height: 20px !important;
|
||
vertical-align: middle !important;
|
||
margin-right: 5px !important;
|
||
stroke: var(--btn-primary-bg) !important;
|
||
fill: none !important;
|
||
}
|
||
|
||
svg.feather:hover,
|
||
.custom-icon:hover {
|
||
color: var(--btn-primary-bg);
|
||
}
|
||
|
||
.custom-icon {
|
||
color: var(--accent-color);
|
||
font-size: 18px;
|
||
vertical-align: middle;
|
||
margin-right: 5px;
|
||
transition: color 0.25s ease;
|
||
}
|
||
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: var(--accent-color) var(--header-bg);
|
||
}
|
||
|
||
*::-webkit-scrollbar {
|
||
width: 6px;
|
||
height: 6px;
|
||
background: transparent;
|
||
}
|
||
|
||
*::-webkit-scrollbar-track {
|
||
background: var(--header-bg);
|
||
border-radius: 3px;
|
||
margin: 4px 0;
|
||
}
|
||
|
||
*::-webkit-scrollbar-thumb {
|
||
background: var(--accent-color);
|
||
border-radius: 3px;
|
||
transition: background 0.3s ease;
|
||
min-height: 40px;
|
||
min-width: 40px;
|
||
}
|
||
|
||
*::-webkit-scrollbar-thumb:hover {
|
||
background: color-mix(in oklch, var(--accent-color), white 20%);
|
||
}
|
||
|
||
body {
|
||
color: var(--text-primary);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
transition: all 0.3s ease;
|
||
font-family: 'Fredoka One', cursive;
|
||
font-weight: 400;
|
||
background: oklch(var(--bg-l) var(--base-chroma) var(--base-hue));
|
||
}
|
||
|
||
body.default-font {
|
||
font-family: system-ui, sans-serif;
|
||
font-weight: 400;
|
||
}
|
||
|
||
body.system-nofo-font {
|
||
font-family: 'Noto Serif SC';
|
||
font-weight: 400;
|
||
}
|
||
|
||
body.system-mono-font {
|
||
font-family: 'Comic Neue';
|
||
font-weight: 400;
|
||
}
|
||
|
||
body.dm-serif-font {
|
||
font-family: 'DM Serif Display';
|
||
font-weight: 400;
|
||
}
|
||
|
||
body.cinzel-font {
|
||
font-family: 'Cinzel Decorative';
|
||
font-weight: 700;
|
||
}
|
||
|
||
body.noto-sans-font {
|
||
font-family: 'Noto Sans';
|
||
font-weight: 400;
|
||
}
|
||
|
||
body.font-transition {
|
||
animation: fontFade 0.4s ease-in-out;
|
||
}
|
||
|
||
@keyframes fontFade {
|
||
0% {
|
||
opacity: 0.3;
|
||
transform: scale(0.98);
|
||
}
|
||
|
||
100% {
|
||
opacity: 1;
|
||
transform: scale(1);
|
||
}
|
||
}
|
||
|
||
.modal-backdrop {
|
||
background: oklch(0 0 0 / 0.4);
|
||
}
|
||
|
||
.color-preset {
|
||
transition: transform 0.2s ease;
|
||
}
|
||
|
||
.color-preset:hover {
|
||
transform: scale(1.1);
|
||
}
|
||
|
||
#color-input {
|
||
font-weight: 700 !important;
|
||
color: white !important;
|
||
}
|
||
|
||
@supports (color: oklch(1 0 0)) {
|
||
#color-input {
|
||
color: oklch(0.95 0 0) !important;
|
||
}
|
||
}
|
||
|
||
input,
|
||
textarea,
|
||
select,
|
||
button {
|
||
font-family: inherit !important;
|
||
}
|
||
|
||
input[type=range] {
|
||
-webkit-appearance: none;
|
||
width: 100%;
|
||
height: 8px;
|
||
border-radius: 5px;
|
||
background: var(--accent-color) !important;
|
||
outline: none;
|
||
transition: background 0.3s ease;
|
||
}
|
||
|
||
input[type=range]:hover {
|
||
background: linear-gradient(to right,
|
||
var(--btn-primary-bg),
|
||
var(--accent-color)) !important;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
appearance: none;
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: #ffffff !important;
|
||
border: var(--border-strong) !important;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease, transform 0.2s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb:hover {
|
||
background: #f0f0f0 !important;
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
input[type=range]::-moz-range-thumb {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: #ffffff !important;
|
||
border: 2px solid var(--accent-color) !important;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease, transform 0.2s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
input[type=range]::-moz-range-thumb:hover {
|
||
background: #f0f0f0 !important;
|
||
transform: scale(1.2);
|
||
}
|
||
|
||
input[type=range]::-ms-thumb {
|
||
width: 18px;
|
||
height: 18px;
|
||
border-radius: 50%;
|
||
background: #ffffff !important;
|
||
border: 2px solid var(--accent-color) !important;
|
||
cursor: pointer;
|
||
transition: background 0.3s ease, transform 0.2s ease, border-color 0.3s ease;
|
||
}
|
||
|
||
.panel-header h3,
|
||
.panel-header button.close-icon,
|
||
.panel-header button.close-icon i {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
#modalWidthValue,
|
||
#widthValue {
|
||
color: var(--text-primary) !important;
|
||
}
|
||
|
||
.btn {
|
||
border: none;
|
||
border-radius: var(--radius);
|
||
transition: var(--transition);
|
||
font-weight: 500;
|
||
position: relative;
|
||
overflow: visible;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: var(--btn-primary-bg);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: var(--btn-primary-hover);
|
||
box-shadow: var(--item-hover-shadow);
|
||
}
|
||
|
||
.btn-success {
|
||
background: var(--btn-success-bg);
|
||
color: var(--success-text);
|
||
}
|
||
|
||
.btn-success:hover {
|
||
background: color-mix(in oklch, var(--btn-success-bg), white 10%);
|
||
box-shadow: var(--item-hover-shadow);
|
||
}
|
||
|
||
.btn-info {
|
||
background: var(--btn-info-bg);
|
||
color: var(--info-text);
|
||
}
|
||
|
||
.btn-info:hover {
|
||
background: color-mix(in oklch, var(--btn-info-bg), white 10%);
|
||
box-shadow: var(--item-hover-shadow);
|
||
}
|
||
|
||
.btn-danger {
|
||
background: oklch(55% 0.25 25);
|
||
color: oklch(95% 0.05 25);
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: oklch(60% 0.25 25);
|
||
}
|
||
|
||
.btn-warning {
|
||
background: oklch(75% 0.22 80);
|
||
color: oklch(25% 0.05 80);
|
||
}
|
||
|
||
.btn-warning:hover {
|
||
background: oklch(80% 0.22 80);
|
||
}
|
||
|
||
[data-theme="light"] {
|
||
--danger-text: oklch(95% 0.05 25);
|
||
--warning-text: oklch(20% 0.15 80);
|
||
|
||
.btn-danger {
|
||
background: oklch(60% 0.3 25);
|
||
color: var(--danger-text);
|
||
}
|
||
|
||
.btn-warning {
|
||
background: oklch(85% 0.25 80);
|
||
color: var(--warning-text);
|
||
}
|
||
}
|
||
|
||
.btn-outline-primary {
|
||
border: 1px solid var(--btn-primary-bg);
|
||
color: var(--btn-primary-bg);
|
||
background: transparent;
|
||
}
|
||
|
||
.btn-outline-primary:hover {
|
||
background: var(--btn-primary-bg);
|
||
color: var(--text-primary);
|
||
}
|
||
|
||
.btn-sm {
|
||
padding: 0.25rem 0.5rem;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.btn-lg {
|
||
padding: 0.5rem 1rem;
|
||
font-size: 1.25rem;
|
||
}
|
||
|
||
.ace_search {
|
||
background: var(--bg-container) !important;
|
||
border: var(--border-strong) !important;
|
||
border-radius: var(--radius);
|
||
box-shadow: var(--item-hover-shadow);
|
||
padding: 8px 12px !important;
|
||
color: var(--text-primary);
|
||
backdrop-filter: var(--glass-blur);
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.ace_search_form,
|
||
.ace_replace_form {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.ace_search_field {
|
||
background: var(--card-bg) !important;
|
||
border: var(--border-strong) !important;
|
||
color: var(--text-primary) !important;
|
||
padding: 6px 12px !important;
|
||
border-radius: calc(var(--radius) - 4px) !important;
|
||
font-size: 14px !important;
|
||
min-width: 200px;
|
||
transition: var(--transition);
|
||
}
|
||
|
||
.ace_search_field:focus {
|
||
border-color: var(--accent-color) !important;
|
||
outline: none;
|
||
box-shadow: 0 0 0 2px color-mix(in oklch, var(--accent-color), transparent 70%);
|
||
}
|
||
|
||
.ace_searchbtn {
|
||
background: var(--btn-primary-bg) !important;
|
||
color: white !important;
|
||
border: none !important;
|
||
border-radius: calc(var(--radius) - 4px) !important;
|
||
background-image: none !important;
|
||
padding: 6px 12px !important;
|
||
font-size: 13px !important;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
min-width: 60px;
|
||
filter: contrast(1.2) brightness(1.1);
|
||
}
|
||
|
||
.ace_searchbtn:hover {
|
||
background: var(--btn-primary-hover) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
.ace_searchbtn.prev,
|
||
.ace_searchbtn.next {
|
||
position: relative;
|
||
}
|
||
|
||
.ace_searchbtn.prev::before,
|
||
.ace_searchbtn.next::before {
|
||
content: "";
|
||
font-size: 14px;
|
||
color: white !important;
|
||
filter: contrast(1.3);
|
||
display: inline-block;
|
||
line-height: 1;
|
||
}
|
||
|
||
.ace_searchbtn.prev::before {
|
||
content: "↑";
|
||
}
|
||
|
||
.ace_searchbtn.next::before {
|
||
content: "↓";
|
||
}
|
||
|
||
.ace_searchbtn .ace_icon,
|
||
.ace_searchbtn::after {
|
||
display: none !important;
|
||
opacity: 0 !important;
|
||
}
|
||
|
||
.ace_searchbtn_close {
|
||
background: transparent !important;
|
||
color: var(--text-secondary) !important;
|
||
position: absolute;
|
||
right: 12px;
|
||
top: 12px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
transition: var(--transition);
|
||
width: 20px;
|
||
height: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 3px;
|
||
}
|
||
|
||
.ace_searchbtn_close:hover {
|
||
color: white !important;
|
||
background: var(--btn-primary-bg) !important;
|
||
}
|
||
|
||
.ace_searchbtn_close::before {
|
||
content: "×";
|
||
}
|
||
|
||
.ace_search_options {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.ace_button {
|
||
background: var(--btn-primary-bg) !important;
|
||
color: white !important;
|
||
border: none !important;
|
||
border-radius: calc(var(--radius) - 4px) !important;
|
||
padding: 4px 8px !important;
|
||
font-size: 12px !important;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
filter: contrast(1.2);
|
||
}
|
||
|
||
.ace_button:hover {
|
||
background: var(--btn-primary-hover) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
.ace_search_counter {
|
||
color: var(--text-secondary);
|
||
font-size: 12px;
|
||
margin-right: auto;
|
||
}
|
||
|
||
[action="toggleRegexpMode"] {
|
||
background: var(--btn-info-bg) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
[action="toggleCaseSensitive"] {
|
||
background: var(--btn-warning-bg) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
[action="toggleWholeWords"] {
|
||
background: var(--btn-success-bg) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
[action="searchInSelection"] {
|
||
background: var(--ocean-bg) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
[action="toggleReplace"] {
|
||
background: var(--lavender-bg) !important;
|
||
color: white !important;
|
||
}
|
||
|
||
#editorStatus {
|
||
font-weight: bold !important;
|
||
font-size: 0.9rem !important;
|
||
color: var(--text-secondary) !important;
|
||
}
|
||
|
||
#editorStatus #lineColumnDisplay,
|
||
#editorStatus #charCountDisplay {
|
||
font-size: 1.1rem !important;
|
||
color: var(--info-text) !important;
|
||
transition: var(--transition) !important;
|
||
}
|
||
|
||
#editorStatus:hover #lineColumnDisplay,
|
||
#editorStatus:hover #charCountDisplay {
|
||
color: var(--accent-color) !important;
|
||
}
|
||
|
||
[data-theme="light"] #editorStatus #lineColumnDisplay,
|
||
[data-theme="light"] #editorStatus #charCountDisplay {
|
||
color: var(--info-text) !important;
|
||
}
|
||
|
||
.btn-close {
|
||
width: 15px !important;
|
||
height: 15px !important;
|
||
background-color: #30e8dc !important;
|
||
border-radius: 6px !important;
|
||
border: none !important;
|
||
position: relative !important;
|
||
display: flex !important;
|
||
align-items: center !important;
|
||
justify-content: center !important;
|
||
cursor: pointer !important;
|
||
transition: background-color 0.2s ease, transform 0.2s ease !important;
|
||
}
|
||
|
||
.btn-close::before,
|
||
.btn-close::after {
|
||
content: '' !important;
|
||
position: absolute !important;
|
||
width: 12px !important;
|
||
height: 2px !important;
|
||
background-color: #ff4d4f !important;
|
||
border-radius: 2px !important;
|
||
transition: background-color 0.2s ease !important;
|
||
}
|
||
|
||
.btn-close::before {
|
||
transform: rotate(45deg) !important;
|
||
}
|
||
|
||
.btn-close::after {
|
||
transform: rotate(-45deg) !important;
|
||
}
|
||
|
||
.btn-close:hover {
|
||
background-color: #30e8dc !important;
|
||
transform: scale(1.1) !important;
|
||
}
|
||
|
||
.btn-close:hover::before,
|
||
.btn-close:hover::after {
|
||
background-color: #d9363e !important;
|
||
}
|
||
|
||
.btn-close:active {
|
||
transform: scale(0.9) !important;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.control-panel {
|
||
display: flex;
|
||
flex-direction: column;
|
||
max-height: 100vh;
|
||
}
|
||
|
||
.buttons-grid {
|
||
overflow-y: auto;
|
||
flex-grow: 1;
|
||
padding-right: 8px;
|
||
-webkit-overflow-scrolling: touch;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
#toggle-ip {
|
||
display: none !important;
|
||
}
|
||
}
|
||
|
||
.scrollable-container:hover {
|
||
overflow: auto;
|
||
}
|
||
|
||
:root {
|
||
--container-width: 2350px;
|
||
--modal-max-width: 1100px;
|
||
}
|
||
|
||
.container-sm {
|
||
width: var(--container-width) !important;
|
||
max-width: 100%;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.modal-xl {
|
||
max-width: var(--modal-max-width) !important;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.modal-xl {
|
||
max-width: 100%;
|
||
}
|
||
}
|
||
|
||
[data-theme="dark"] {
|
||
--glow-base: var(--base-hue);
|
||
--glow-primary: oklch(88% 0.35 var(--glow-base));
|
||
--glow-secondary: oklch(85% 0.3 calc(var(--glow-base) + 15));
|
||
}
|
||
|
||
[data-theme="dark"] .form-select,
|
||
[data-theme="dark"] .form-control:disabled,
|
||
[data-theme="dark"] input[type="text"] {
|
||
border: 1px solid color-mix(in oklch, var(--glow-primary), transparent 30%);
|
||
box-shadow: 0 0 8px 1px color-mix(in oklch, var(--glow-primary), transparent 60%),
|
||
inset 0 -4px 12px color-mix(in oklch, var(--glow-primary), transparent 80%);
|
||
transition: box-shadow 0.3s ease;
|
||
}
|
||
|
||
[data-theme="dark"] .form-select:hover,
|
||
[data-theme="dark"] .form-select:focus,
|
||
[data-theme="dark"] input[type="text"]:hover,
|
||
[data-theme="dark"] input[type="text"]:focus {
|
||
box-shadow: 0 0 12px 2px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
inset 0 -6px 16px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
}
|
||
|
||
[data-theme="dark"] .form-control:disabled {
|
||
opacity: 0.7;
|
||
box-shadow: 0 0 4px 0 color-mix(in oklch, var(--glow-primary), transparent 70%),
|
||
inset 0 -2px 8px color-mix(in oklch, var(--glow-primary), transparent 90%);
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input[type="checkbox"] {
|
||
border: 1px solid color-mix(in oklch, var(--glow-primary), transparent 50%);
|
||
background-color: color-mix(in oklch, var(--bg-body), transparent 20%);
|
||
box-shadow: 0 0 6px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input:checked[type="checkbox"] {
|
||
background-color: var(--glow-primary);
|
||
border-color: var(--glow-primary);
|
||
box-shadow: 0 0 8px var(--glow-primary),
|
||
inset 0 0 4px white;
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input[type="radio"] {
|
||
border: 1px solid color-mix(in oklch, var(--glow-primary), transparent 50%);
|
||
background-color: color-mix(in oklch, var(--bg-body), transparent 20%);
|
||
box-shadow: 0 0 6px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input:checked[type="radio"] {
|
||
background-color: var(--glow-primary);
|
||
border-color: var(--glow-primary);
|
||
box-shadow: 0 0 8px var(--glow-primary),
|
||
inset 0 0 6px color-mix(in oklch, white, transparent 60%);
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input:hover {
|
||
box-shadow: 0 0 10px color-mix(in oklch, var(--glow-primary), transparent 50%);
|
||
}
|
||
|
||
[data-theme="dark"] .form-check-input:disabled {
|
||
opacity: 0.5;
|
||
box-shadow: 0 0 4px color-mix(in oklch, var(--glow-primary), transparent 80%);
|
||
}
|
||
|
||
[data-theme="dark"] table a svg {
|
||
filter: drop-shadow(0 0 2px color-mix(in oklch, var(--glow-primary), transparent 30%));
|
||
transition: filter 0.3s ease;
|
||
}
|
||
|
||
[data-theme="dark"] table a:hover svg {
|
||
filter: drop-shadow(0 0 6px var(--glow-primary));
|
||
}
|
||
|
||
[data-theme="dark"] #dynamicTitle,
|
||
h2,
|
||
h3,
|
||
h4,
|
||
h5,
|
||
h6 {
|
||
position: relative;
|
||
color: color-mix(in oklch, var(--glow-primary), white 20%);
|
||
text-shadow: 0 0 5px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
0 0 15px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
animation: text-glow-pulse 3s ease-in-out infinite alternate;
|
||
}
|
||
|
||
@keyframes text-glow-pulse {
|
||
0% {
|
||
text-shadow: 0 0 5px color-mix(in oklch, var(--glow-primary), transparent 40%),
|
||
0 0 15px color-mix(in oklch, var(--glow-primary), transparent 70%);
|
||
}
|
||
|
||
100% {
|
||
text-shadow: 0 0 10px color-mix(in oklch, var(--glow-primary), transparent 30%),
|
||
0 0 25px color-mix(in oklch, var(--glow-primary), transparent 60%);
|
||
}
|
||
}
|
||
|
||
.rounded-3.border {
|
||
border: var(--border-strong) !important;
|
||
}
|
||
</style>
|