LoaderSpot/table/index.html
2025-02-27 02:11:40 +03:00

1402 lines
51 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Official Spotify full installers</title>
<link type="image/png" rel="icon" href="favicon.png">
<link rel="preconnect" href="https://upgrade.scdn.co" crossorigin>
<link rel="preconnect" href="https://broad-pine-bbc0.amd64fox1.workers.dev" crossorigin>
<link rel="preload" href="https://encore.scdn.co/1.2.3/CircularSpotify-UI-Latin-OS2v3-Book.woff2" as="font"
type="font/woff2" crossorigin="anonymous" />
<link rel="preload" href="https://encore.scdn.co/1.2.3/CircularSpotify-UI-Latin-OS2v3-Bold.woff2" as="font"
type="font/woff2" crossorigin="anonymous" />
<style>
html {
/* Всегда показывать вертикальную полосу прокрутки, для более плавного переключения вкладок таблицы */
overflow-y: scroll;
/* Стили для адаптации scrollbar под тёмную тему для Firefox */
scrollbar-color: #666 #2a2a2a;
scrollbar-width: auto;
}
@font-face {
font-family: 'Circular Spotify';
src: url('https://encore.scdn.co/1.2.3/CircularSpotify-UI-Latin-OS2v3-Book.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Circular Spotify';
src: url('https://encore.scdn.co/1.2.3/CircularSpotify-UI-Latin-OS2v3-Bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Circular Spotify', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: #181a1b;
color: #a59d92;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.nav-container {
background: #000000;
padding: 2rem 2rem;
margin-bottom: 20px;
position: sticky;
top: 0;
z-index: 1000;
display: flex;
justify-content: center;
}
.navbar {
width: 90%;
max-width: 1000px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.nav-brand {
display: flex;
align-items: center;
gap: 5px;
color: #e8e6e3;
position: absolute;
left: 0;
pointer-events: none;
}
.nav-center {
position: absolute;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 30px;
margin: 0;
padding: 0;
list-style: none;
}
.nav-center a {
color: #e8e6e3;
text-decoration: none;
padding: 8px 16px;
border-radius: 20px;
transition: all 0.3s ease;
font-weight: 700;
}
.nav-center a.active {
background: #1db954;
color: #000;
}
.nav-center a:hover:not(.active) {
color: #1db954;
}
header {
padding: 20px;
text-align: center;
}
header h1 {
font-size: 2rem;
margin-bottom: 20px;
color: #e8e6e3;
font-weight: 400;
}
header p {
font-size: 1.2rem;
color: #ccc;
}
.container {
width: 90%;
max-width: 1000px;
margin: 40px auto;
position: relative;
overflow-x: auto;
/* Чтобы таблица прокручивалась при узком экране */
}
/* Стили таблицы */
table.version-table {
width: 100%;
border-collapse: collapse;
min-width: 600px;
/* чтобы колонки не сжимались слишком сильно */
table-layout: fixed;
/* Фиксированная разметка таблицы */
}
/* Добавляем новый стиль для разделения версий */
table.version-table tr:has(.version-cell) {
border-top: 2px solid #444;
}
/* Для первой строки убираем верхнюю границу */
table.version-table tbody tr:first-child {
border-top: none;
border-top: 2px solid #444;
}
/* Добавляем стиль для нижней границы последней версии */
table.version-table tbody tr:last-child {
border-bottom: 2px solid #444;
}
table.version-table thead th {
text-align: left;
padding: 8px;
border-bottom: 1px solid #444;
font-weight: 700;
}
table.version-table tbody td {
padding: 7px;
border-bottom: 1px solid #333;
vertical-align: middle;
font-size: 14px
}
/* Чтобы версия визуально выделялась при наведении/копирование */
.version-cell {
font-weight: 500;
color: #b3b3b3;
}
.version-text {
cursor: pointer;
display: inline-block;
/* Чтобы курсор был только на тексте */
position: relative;
/* для корректного позиционирования tooltip */
}
.version-text:hover {
color: #fff;
}
.short-version {
font-size: 1.2em;
font-weight: bold;
display: block;
margin-bottom: 4px;
user-select: none;
}
.full-version {
font-size: 0.9em;
color: #777;
display: block;
user-select: none;
}
/* Кнопка скачать (иконка) */
.download-link {
display: inline-flex;
align-items: center;
gap: 4px;
color: #a59d92;
text-decoration: none;
font-weight: 500;
background: #ffffff08;
padding: 6px 10px;
border-radius: 4px;
transition: background 0.2s;
position: relative;
/* для корректного позиционирования tooltip */
cursor: pointer;
/* добавлено для отображения указателя при наведении */
}
.download-link:hover {
background: #ffffff12;
}
.download-icon {
width: 16px;
height: 16px;
fill: currentColor;
transition: fill 0.2s;
position: relative;
/* для корректного позиционирования tooltip */
}
.download-link:hover .download-icon {
fill: #1db954;
}
/* Тост-уведомление (скопировано в буфер) */
.toast-notification {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: #464746;
color: #b3b3b3;
padding: 12px 24px;
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 1000;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
}
.toast-notification.show {
opacity: 1;
}
.action-cell {
text-align: center;
/* Выравнивание содержимого ячейки по центру */
}
/* Стили для кнопок фильтрации */
.filter-buttons {
display: flex;
gap: 10px;
padding: 8px;
}
/* Стили для ячейки с кнопками */
table.version-table thead td {
border: none;
padding: 0;
}
/* Кнопки внутри таблицы */
table.version-table .filter-button {
padding: 6px 20px;
border: 1px solid #474747;
background: transparent;
color: #a6a7a7;
border-radius: 20px;
cursor: pointer;
font-family: inherit;
font-weight: 700;
transition: all 0.3s ease;
height: 34px;
}
table.version-table .filter-button.active {
background: #1db954;
color: #000;
pointer-events: none;
/* блокируем клики на активной кнопке */
}
table.version-table .filter-button:hover:not(.active) {
background: #d6d6d62a;
}
/* Плавный переход для контейнера таблицы */
#versions-container {
transition: opacity 0.3s ease-in-out;
}
/* Стили для Linux-репозитория */
.linux-repo-wrapper {
background: #242628;
border-radius: 8px;
overflow: hidden;
}
.repo-header {
display: grid;
grid-template-columns: 1fr 100px 150px;
gap: 16px;
padding: 12px 16px;
background: #2a2e2f;
border-bottom: 1px solid #333;
font-weight: 700;
color: #e8e6e3;
}
.repo-content {
height: 170px;
overflow-y: auto;
}
.repo-item {
display: grid;
grid-template-columns: 1fr 100px 150px;
gap: 16px;
padding: 8px 16px;
border-bottom: 1px solid #333;
transition: background 0.2s;
text-decoration: none;
color: #a59d92;
}
.repo-item:hover {
background: #2a2e2f;
}
.loading-indicator {
padding: 16px;
text-align: center;
color: #777;
}
.repo-content::-webkit-scrollbar {
width: 8px;
}
.repo-content::-webkit-scrollbar-track {
background: #242628;
}
.repo-content::-webkit-scrollbar-thumb {
background: #444;
border-radius: 4px;
}
.repo-content::-webkit-scrollbar-thumb:hover {
background: #555;
}
/* Стили для контейнера поиска */
.search-container {
padding: 4px 12px;
border-radius: 20px;
border: 1px solid #474747;
display: inline-flex;
align-items: center;
gap: 8px;
height: 34px;/
}
/* Стили для поля поиска */
.search-container input[type="search"] {
border: none;
background: transparent;
outline: none;
color: #b3b3b3 !important;
width: 180px;
font-size: 14px;
padding: 4px 0;
}
/* Крестик очистки для WebKit */
.search-container input[type="search"]::-webkit-search-cancel-button {
-webkit-appearance: none;
height: 16px;
width: 16px;
margin-top: 4px;
background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23e8e6e3' viewBox='0 0 20 20'%3E%3Cpath d='M14.348 5.652a1 1 0 10-1.414-1.414L10 7.172 7.066 4.238a1 1 0 10-1.414 1.414L8.586 8.586l-2.934 2.934a1 1 0 101.414 1.414L10 10l2.934 2.934a1 1 0 001.414-1.414L11.414 8.586l2.934-2.934z'/%3E%3C/svg%3E") no-repeat center;
cursor: pointer;
}
/* Стиль для иконки поиска */
.search-icon svg {
width: 16px;
height: 16px;
fill: #e8e6e3;
display: block;
}
/* Правило для подсветки найденного текста */
mark {
background-color: rgba(255, 255, 0, 0.4);
}
/* Данные столбца Downloads по центру */
table.version-table td:nth-child(5) {
width: 10%;
text-align: center;
}
.download-text {
display: inline-block;
transform: translateX(-1px);
/* смещает содержимое влево, не влияя на соседний столбец */
}
/* Стиль счетчика скачиваний */
.download-counter {
position: relative;
color: #a7a6a6b0;
padding: 2px 6px;
border-radius: 12px;
font-size: 0.75em;
font-weight: bold;
transition: transform 0.2s ease;
margin-left: 9px;
cursor: default;
white-space: nowrap;
/* чтобы все помещалось в одну строку */
}
.download-counter:hover {
transform: scale(1.1);
}
/* Стиль для контейнера скачивания: элементы расположены в ряд */
.download-container {
display: flex;
flex-direction: row;
align-items: center;
gap: 4px;
}
/* Стили для адаптации scrollbar под тёмную тему */
::-webkit-scrollbar {
background-color: #2a2a2a;
}
::-webkit-scrollbar-thumb {
background-color: #666;
}
::-webkit-scrollbar-track {
background-color: #3a3a3a;
}
/* Стили для кнопок фильтра архитектур */
.arch-filter-buttons {
display: inline-flex;
gap: 10px;
padding: 8px;
min-height: 50px;
/* Добавляем минимальную высоту */
transition: opacity 0.3s ease;
/* Плавное скрытие/появление */
opacity: 1;
height: auto;
overflow: hidden;
}
/* Класс для скрытия фильтров без влияния на layout */
.arch-filter-buttons.hidden {
opacity: 0;
pointer-events: none;
/* Блокируем клики на скрытых элементах */
}
.arch-filter-button {
padding: 6px 12px;
border: 1px solid #474747;
background: transparent;
color: #a6a7a7;
border-radius: 20px;
cursor: pointer;
font-weight: 700;
transition: all 0.3s ease;
}
.arch-filter-button.active {
background: #1db954;
color: #000;
}
.arch-filter-button:hover:not(.active) {
background: #d6d6d62a;
}
/* Добавляем стиль для строки с фильтрами архитектур */
tr.arch-filters-row {
height: 50px;
/* Фиксированная высота для строки с фильтрами архитектур */
transition: height 0.3s ease;
/* Плавное изменение высоты */
}
/* Стиль для строки с фильтрами OS и поиском */
tr.os-filters-row {
position: relative;
/* Чтобы row не прыгала при исчезновении arch-фильтров */
z-index: 2;
/* Строка с фильтрами OS должна быть выше */
}
</style>
</head>
<body>
<nav class="nav-container">
<div class="navbar">
<div class="nav-brand">
<!-- место для лого слева -->
</div>
<ul class="nav-center">
<li><a href="index.html" class="active" style="pointer-events: none; cursor: default;">Versions</a></li>
<li><a href="help.html" style="pointer-events: none; cursor: default;">Help</a></li>
<li><a href="links.html" style="pointer-events: none; cursor: default;">Links</a></li>
</ul>
</div>
</nav>
<header>
<h1>Official Spotify full installers</h1>
<p>Site in testing mode...</p>
</header>
<!-- Контейнер с таблицей -->
<div class="container">
<table class="version-table" id="versionsTable">
<!-- Регулировки ширины столбцов -->
<colgroup>
<col style="width:15%"><!-- Version -->
<col style="width:12%"><!-- Architecture -->
<col style="width:12%"><!-- Date -->
<col style="width:12%"><!-- Size -->
<col style="width:10%"><!-- Кнопка -->
</colgroup>
<thead>
<!-- ряд фильтрации: разделяем на два ряда -->
<tr class="arch-filters-row">
<td colspan="5">
<!-- Фильтры архитектур над кнопками ОС -->
<div id="arch-filters" class="arch-filter-buttons">
</div>
</td>
</tr>
<tr class="os-filters-row">
<td colspan="5">
<div class="filter-buttons" style="display: inline-flex; align-items: center; gap: 10px;">
<button class="filter-button active" data-os="win">Windows</button>
<button class="filter-button" data-os="mac">Mac</button>
<button class="filter-button" data-os="linux">Linux</button>
<!--блок поиска -->
<div class="search-container">
<span class="search-icon">
<svg xmlns="hKV Namespacettp://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path
d="M11.742 10.344a6.5 6.5 0 10-1.398 1.398h-.001l3.85 3.85a1 1 0 001.414-1.414l-3.85-3.85zm-5.242 0a4.5 4.5 0 110-9 4.5 4.5 0 010 9z" />
</svg>
</span>
<input type="search" id="versionSearch" placeholder="Search">
</div>
</div>
</td>
</tr>
<tr class="table-headers">
<th>Version</th>
<th>Architecture</th>
<th>Date</th>
<th>Size</th>
<th> </th>
</tr>
</thead>
<tbody id="versions-container">
<tr>
<td colspan="5">Loading data...</td>
</tr>
</tbody>
</table>
</div>
<!-- Тост-уведомление (копирование) -->
<div class="toast-notification" id="toast">Copied to clipboard</div>
<script>
const downloadIcon = `
<svg class="download-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M12 15.575c-.183 0-.36-.073-.49-.203l-4.095-4.095a.694.694 0 01.981-.981L12 13.901l3.604-3.604a.694.694 0 01.981.98l-4.095 4.095a.692.692 0 01-.49.203z"/>
<path d="M12 15.575a.694.694 0 01-.694-.694V3.694a.694.694 0 011.388 0v11.187c0 .383-.31.694-.694.694z"/>
<path d="M16.306 20.306H7.694a4.167 4.167 0 01-4.162-4.163v-2.777a.694.694 0 011.388 0v2.777a2.778 2.778 0 002.774 2.775h8.612a2.778 2.778 0 002.775-2.775v-2.777a.694.694 0 011.387 0v2.777a4.167 4.167 0 01-4.162 4.163z"/>
</svg>
`;
// Функция отображения тоста
function showToast(message) {
const toast = document.getElementById('toast');
if (!toast) return;
toast.textContent = message;
toast.classList.add('show');
setTimeout(() => {
toast.classList.remove('show');
}, 2000);
}
// Функция fallback для копирования текста
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement('textarea');
textArea.value = text;
textArea.style.position = 'fixed';
textArea.style.top = '0';
textArea.style.left = '0';
textArea.style.width = '2em';
textArea.style.height = '2em';
textArea.style.padding = '0';
textArea.style.border = 'none';
textArea.style.outline = 'none';
textArea.style.boxShadow = 'none';
textArea.style.background = 'transparent';
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
try {
document.execCommand('copy');
showToast('Copied to clipboard');
} catch (err) {
showToast('Error while copying');
console.error('Error while copying:', err);
}
document.body.removeChild(textArea);
}
// Глобальный кэш для результатов HEAD-запросов
const headCache = new Map();
// Флаг для отслеживания необходимости отложить HEAD-запросы
let headRequestsStarted = false;
// Таймер для начала HEAD-запросов даже если Worker не ответил
let headRequestsTimer = null;
// Очередь HEAD-запросов
const pendingHeadRequests = [];
// Функция для выполнения отложенных HEAD-запросов
function processPendingHeadRequests() {
if (headRequestsStarted) return; // Если запросы уже запущены, не запускаем снова
headRequestsStarted = true;
// Отменяем таймер, если он был установлен
if (headRequestsTimer) {
clearTimeout(headRequestsTimer);
headRequestsTimer = null;
}
// Выполняем все HEAD-запросы из очереди
for (const request of pendingHeadRequests) {
executeHeadRequest(request.dateCell, request.sizeCell, request.url);
}
pendingHeadRequests.length = 0; // Очищаем очередь
}
// Функция updateLinkInfo с использованием очереди
function updateLinkInfo(dateCell, sizeCell, url) {
// Добавляем запрос в очередь
pendingHeadRequests.push({ dateCell, sizeCell, url });
// Если запросы уже запущены, выполняем немедленно
if (headRequestsStarted) {
executeHeadRequest(dateCell, sizeCell, url);
// И удаляем из очереди
const index = pendingHeadRequests.findIndex(req =>
req.dateCell === dateCell && req.sizeCell === sizeCell && req.url === url);
if (index > -1) {
pendingHeadRequests.splice(index, 1);
}
}
}
// Функция для выполнения HEAD-запроса
async function executeHeadRequest(dateCell, sizeCell, url) {
if (headCache.has(url)) {
const cached = headCache.get(url);
dateCell.textContent = cached.date;
sizeCell.textContent = cached.size;
return;
}
try {
const response = await fetch(url, { method: 'HEAD' });
const lastModified = response.headers.get('Last-Modified');
const contentLength = response.headers.get('Content-Length');
let formattedDate = '—';
let sizeMb = '—';
if (lastModified) {
const date = new Date(lastModified);
formattedDate =
String(date.getDate()).padStart(2, '0') + '.' +
String(date.getMonth() + 1).padStart(2, '0') + '.' +
date.getFullYear();
}
if (contentLength) {
sizeMb = (parseInt(contentLength, 10) / (1024 * 1024)).toFixed(2) + ' MB';
}
// Сохраняем результат в кэше
headCache.set(url, { date: formattedDate, size: sizeMb });
dateCell.textContent = formattedDate;
sizeCell.textContent = sizeMb;
} catch (error) {
console.error('Error getting headers', error);
dateCell.textContent = '—';
sizeCell.textContent = '—';
headCache.set(url, { date: '—', size: '—' });
}
}
// Глобальный кэш для всех счетчиков скачиваний, полученных из Worker
let allDownloadCounters = {};
// Флаг для отслеживания загрузки счетчиков
let countersLoaded = false;
// Хранилище для ожидающих элементов UI, которые нужно обновить после загрузки счетчиков
const pendingCounterElements = new Map();
// Функция для получения всех счетчиков одним запросом
async function loadAllDownloadCounters() {
try {
const workerUrl = 'https://broad-pine-bbc0.amd64fox1.workers.dev/?all=true';
// Таймаут для запроса к Worker - 3 секунды
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 3000);
const response = await fetch(workerUrl, { signal: controller.signal });
clearTimeout(timeoutId);
allDownloadCounters = await response.json();
countersLoaded = true;
// Обновляем все ожидающие элементы UI
pendingCounterElements.forEach((countElement, fileUrl) => {
const count = allDownloadCounters[fileUrl] || "0";
countElement.innerHTML = count === "0" ? "" : `<span class="download-counter">${count} downloads</span>`;
});
pendingCounterElements.clear();
// Разрешаем начать HEAD-запросы после получения счетчиков
processPendingHeadRequests();
} catch (err) {
console.error('Error loading all download counters:', err);
countersLoaded = true; // помечаем как загруженные, чтобы избежать повторных запросов
// Запускаем HEAD-запросы даже при ошибке загрузки счетчиков
processPendingHeadRequests();
}
}
function updateDownloadCount(fileUrl, countElement) {
if (countersLoaded) {
// Если данные уже загружены, используем их
const count = allDownloadCounters[fileUrl] || "0";
countElement.innerHTML = count === "0" ? "" : `<span class="download-counter">${count} downloads</span>`;
} else {
// Сохраняем элемент для отложенного обновления
pendingCounterElements.set(fileUrl, countElement);
}
}
// Кэш для регулярных выражений
const regexCache = new Map();
// Подсветки найденного текста с кэшированием регулярных выражений
function highlight(text, term) {
if (!term) return text;
// Используем кэшированное регулярное выражение или создаём новое
if (!regexCache.has(term)) {
// Экранируем специальные символы в поисковом запросе
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
regexCache.set(term, new RegExp(`(${escapedTerm})`, 'gi'));
}
return text.replace(regexCache.get(term), '<mark>$1</mark>');
}
// Функция для оптимистичного обновления счетчика в UI
function incrementDownloadCounter(countElement) {
// Извлекаем текущее число из элемента и увеличиваем на 1
let current = parseInt(countElement.textContent) || 0;
current++;
countElement.innerHTML = `<span class="download-counter">${current} downloads</span>`;
}
// Глобальная переменная для архитектурного фильтра
let currentArch = 'all';
// Функция обновления фильтров архитектур
function updateArchFilters() {
const archContainer = document.getElementById('arch-filters');
// Для Linux скрываем фильтры архитектур, а не очищаем контейнер
if (currentOS === 'linux') {
archContainer.classList.add('hidden');
return;
}
// Для других OS показываем и заполняем фильтрами
archContainer.classList.remove('hidden');
archContainer.innerHTML = ''; // очистка содержимого
const archSet = new Set();
// Собираем архитектуры из всех версий для текущей ОС
allVersions.forEach(([, data]) => {
if (data.links[currentOS]) {
Object.keys(data.links[currentOS]).forEach(arch => archSet.add(arch));
}
});
let archArr = [];
if (currentOS === 'win') {
// для Windows фиксированный порядок, если присутствуют в данных
const order = ["x86", "x64", "arm64"];
archArr = order.filter(arch => archSet.has(arch));
} else if (currentOS === 'mac') {
// для mac фиксированный порядок, если присутствуют в данных
const order = ["intel", "arm64"];
archArr = order.filter(arch => archSet.has(arch));
} else {
archArr = Array.from(archSet).sort();
}
// Создаем по кнопке для каждой архитектуры
archArr.forEach(arch => {
const btn = document.createElement('button');
btn.className = 'arch-filter-button' + (currentArch === arch ? ' active' : '');
btn.textContent = arch;
btn.addEventListener('click', () => {
// Если нажата уже активная кнопка, сбрасываем фильтр (возвращаемся к "всему")
if (currentArch === arch) {
currentArch = 'all';
} else {
currentArch = arch;
}
updateArchFilters();
reRenderVersions();
});
archContainer.appendChild(btn);
});
}
// Функция для повторного рендера версий с учетом текущих фильтров (ОС + архитектура + поиск)
function reRenderVersions() {
container.style.opacity = "0";
observer.unobserve(sentinel);
setTimeout(() => {
container.innerHTML = "";
currentIndex = 0;
loadMoreRows();
if (currentSearchResults === null && currentIndex < allVersions.length) {
observer.observe(sentinel);
} else if (currentSearchResults && currentIndex < currentSearchResults.length) {
observer.observe(sentinel);
}
container.style.opacity = "1";
}, 300);
}
// Создаем строки таблицы для одной версии
function createVersionRows(versionKey, data, searchTerm = '') {
const shortVersion = versionKey;
const archCombos = [];
let totalRowsForVersion = 0;
// Фильтруем только по текущей ОС
if (data.links[currentOS]) {
for (const arch of Object.keys(data.links[currentOS])) {
// Фильтруем по архитектуре, если выбран конкретный
if (currentArch !== 'all' && arch !== currentArch) continue;
const link = data.links[currentOS][arch];
if (link) {
archCombos.push({ arch, link });
totalRowsForVersion++;
}
}
}
if (totalRowsForVersion === 0) return [];
const rows = [];
let isFirstVersionRow = true;
archCombos.forEach((combo) => {
const row = document.createElement('tr');
if (isFirstVersionRow) {
const versionCell = document.createElement('td');
versionCell.className = 'version-cell';
versionCell.rowSpan = totalRowsForVersion;
// Используем подсветку, если searchTerm непустой
const displayedShort = searchTerm ? highlight(shortVersion, searchTerm) : shortVersion;
const displayedFull = searchTerm ? highlight(data.fullversion, searchTerm) : data.fullversion;
const versionText = document.createElement('div');
versionText.className = 'version-text';
versionText.innerHTML = `<span class="short-version">${displayedShort}</span><span class="full-version">${displayedFull}</span>`;
versionText.addEventListener('click', async () => {
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(data.fullversion);
showToast('Copied to clipboard');
} else {
fallbackCopyTextToClipboard(data.fullversion);
}
} catch (err) {
console.error('Error while copying:', err);
fallbackCopyTextToClipboard(data.fullversion);
}
});
versionCell.appendChild(versionText);
row.appendChild(versionCell);
isFirstVersionRow = false;
}
// Архитектура
const archCell = document.createElement('td');
archCell.textContent = combo.arch;
row.appendChild(archCell);
// Дата
const dateCell = document.createElement('td');
dateCell.textContent = '—';
row.appendChild(dateCell);
// Размер
const sizeCell = document.createElement('td');
sizeCell.textContent = '—';
row.appendChild(sizeCell);
// Кнопка скачать с отображением счётчика
const actionCell = document.createElement('td');
actionCell.className = 'action-cell';
const downloadContainer = document.createElement('div');
downloadContainer.className = 'download-container';
const downloadLink = document.createElement('a');
downloadLink.className = 'download-link';
// Заменяем установку href:
downloadLink.removeAttribute('href');
downloadLink.dataset.download = `https://broad-pine-bbc0.amd64fox1.workers.dev/?file=${encodeURIComponent(combo.link)}`;
// Формируем тултип в зависимости от OS
let extension = '';
if (currentOS === 'win') {
extension = '.exe';
} else if (currentOS === 'mac') {
extension = '.tbz';
} else if (currentOS === 'linux') {
extension = '.deb';
}
downloadLink.title = `download ${shortVersion}-${combo.arch}${extension}`;
downloadLink.addEventListener('click', (e) => {
e.preventDefault();
// немедленно обновляем UI (оптимистично увеличиваем значение счетчика)
incrementDownloadCounter(downloadCountSpan);
// Обновляем глобальный кэш счетчиков
allDownloadCounters[combo.link] = (parseInt(allDownloadCounters[combo.link] || "0", 10) + 1).toString();
// параллельно посылаем асинхронный запрос к воркеру (fire-and-forget)
fetch(`https://broad-pine-bbc0.amd64fox1.workers.dev/?file=${encodeURIComponent(combo.link)}`, { redirect: 'manual' })
.catch(err => console.error('Worker update error:', err));
// запускаем скачивание файла сразу
window.open(combo.link, '_blank');
});
downloadLink.innerHTML = downloadIcon;
downloadContainer.appendChild(downloadLink);
// Отображения счетчика скачиваний
const downloadCountSpan = document.createElement('div');
downloadCountSpan.style.fontSize = 'small';
downloadCountSpan.title = "downloads"; // добавляем системный тултип
downloadContainer.appendChild(downloadCountSpan);
actionCell.appendChild(downloadContainer);
row.appendChild(actionCell);
// Обновляем инфу о дате и размере
updateLinkInfo(dateCell, sizeCell, combo.link);
// Запрашиваем и обновляем счетчик скачиваний
updateDownloadCount(combo.link, downloadCountSpan);
rows.push(row);
});
return rows;
}
// Переменные для "ленивой" подгрузки
let allVersions = [];
const ITEMS_PER_BATCH = 5; // по 5 версий за раз
let currentIndex = 0;
let currentSearchResults = null; // Новая переменная для результатов поиска
const container = document.getElementById('versions-container');
let currentSearchTerm = ''; // Новая переменная для хранения текущего запроса
// Функция подгрузки очередной порции версий, использует currentSearchResults если заданы
function loadMoreRows() {
const dataSource = currentSearchResults || allVersions;
const endIndex = Math.min(currentIndex + ITEMS_PER_BATCH, dataSource.length);
for (let i = currentIndex; i < endIndex; i++) {
const [versionKey, versionData] = dataSource[i];
// Передаём currentSearchTerm для подсветки найденного текста
const versionRows = createVersionRows(versionKey, versionData, currentSearchTerm);
versionRows.forEach(r => container.appendChild(r));
}
currentIndex = endIndex;
if (currentIndex < dataSource.length) {
container.appendChild(sentinel);
}
}
// Невидимый "сторож" для Intersection Observer
const sentinel = document.createElement('tr');
sentinel.style.height = '1px';
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && currentIndex < allVersions.length) {
observer.unobserve(sentinel);
loadMoreRows();
if (currentIndex < allVersions.length) {
observer.observe(sentinel);
}
}
});
}, { root: null, rootMargin: '100px', threshold: 0 });
async function initializeApp() {
try {
// 1. Загружаем основные данные JSON
const response = await fetch('versions.json');
const data = await response.json();
container.innerHTML = ''; // Очищаем "Загрузка данных..."
allVersions = Object.entries(data);
// 2. Запускаем загрузку счетчиков скачиваний
loadAllDownloadCounters().catch(err => console.error('Counter fetch error:', err));
// 3. Устанавливаем таймер для запуска HEAD-запросов если Worker не отвечает
headRequestsTimer = setTimeout(() => {
console.log('Starting HEAD requests after timeout');
processPendingHeadRequests();
}, 2000); // Ждем 2 секунды и запускаем HEAD-запросы в любом случае
// 4. Отображаем первую партию данных
loadMoreRows();
if (currentIndex < allVersions.length) {
observer.observe(sentinel);
}
updateArchFilters();
} catch (err) {
console.error('Error loading version data:', err);
container.innerHTML = '<tr><td colspan="5">Failed to load version data.</td></tr>';
}
}
// Запускаем инициализацию приложения
initializeApp();
// Переменная для отслеживания текущей ОС
let currentOS = 'win';
// Получаем ссылки на таблицу и Linux-контейнер
const versionsTable = document.getElementById('versionsTable');
// Функция для сортировки результатов поиска
function sortSearchResults(filtered, term) {
return filtered.sort((a, b) => {
const [versionKeyA, dataA] = a;
const [versionKeyB, dataB] = b;
const fullVersionA = dataA.fullversion.toLowerCase();
const fullVersionB = dataB.fullversion.toLowerCase();
// 1. Проверяем точное совпадение целого числа (окруженного точками или границами строки)
const termWithBoundaries = `(^|\\.|\\ )${term}($|\\.|\\s)`;
const reExact = new RegExp(termWithBoundaries);
const exactMatchA = reExact.test(versionKeyA.toLowerCase()) || reExact.test(fullVersionA);
const exactMatchB = reExact.test(versionKeyB.toLowerCase()) || reExact.test(fullVersionB);
// Если у одного точное совпадение, а у другого нет
if (exactMatchA && !exactMatchB) return -1;
if (!exactMatchA && exactMatchB) return 1;
// 2. Проверяем совпадение в первой части версии (до 'g')
// Разделяем версию на основную часть и хеш
const mainPartA = fullVersionA.split('g')[0];
const mainPartB = fullVersionB.split('g')[0];
const inMainA = mainPartA.includes(term);
const inMainB = mainPartB.includes(term);
// Приоритизируем совпадения в основной части версии
if (inMainA && !inMainB) return -1;
if (!inMainA && inMainB) return 1;
// 3. Дальше сортируем по версии (более новые сначала)
return versionKeyB.localeCompare(versionKeyA, undefined, { numeric: true, sensitivity: 'base' });
});
}
// Обработчик для поиска по версиям с ленивой загрузкой результатов
const versionSearch = document.getElementById('versionSearch');
versionSearch.addEventListener('input', (e) => {
const term = e.target.value.trim().toLowerCase();
currentSearchTerm = term; // Сохраняем поисковый запрос
container.style.opacity = "0";
setTimeout(() => {
// Если поле пустое, восстанавливаем обычную подгрузку
if (term === "") {
currentSearchResults = null;
currentIndex = 0;
container.innerHTML = "";
if (currentOS === 'linux') {
loadLinuxPackages();
} else {
loadMoreRows();
if (currentIndex < allVersions.length) {
observer.observe(sentinel);
}
}
container.style.opacity = "1";
return;
}
// Фильтруем все версии по короткой или полной версии
const filtered = allVersions.filter(([versionKey, data]) => {
return (versionKey.toLowerCase().includes(term) ||
data.fullversion.toLowerCase().includes(term)) &&
data.links[currentOS] &&
(currentArch === 'all' || data.links[currentOS].hasOwnProperty(currentArch)); // Фильтруем по текущей ОС и архитектуре
});
// Сортируем результаты поиска
const sortedResults = sortSearchResults(filtered, term);
// Отключаем ленивую подгрузку с обычными данными
observer.unobserve(sentinel);
container.innerHTML = "";
currentIndex = 0;
currentSearchResults = sortedResults;
if (sortedResults.length > 0) {
loadMoreRows();
if (currentIndex < sortedResults.length) {
observer.observe(sentinel);
}
} else {
container.innerHTML = '<tr><td colspan="5">No results</td></tr>';
}
container.style.opacity = "1";
}, 300);
});
// Локальные версии для Linux
async function loadLinuxPackages() {
// Очищаем контейнер для Linux-версий
container.innerHTML = '';
const linuxPackages = [
{
version: {
short: '1.2.56.502',
full: '1.2.56.502.ga68d2d4f'
},
architectures: [
{
arch: 'amd64',
link: 'http://repository.spotify.com/pool/non-free/s/spotify-client/spotify-client_1.2.56.502.ga68d2d4f_amd64.deb',
date: '10.02.2025',
size: '136.36 MB'
}
]
},
{
version: {
short: '1.2.53.440',
full: '1.2.53.440.g7b2f582a'
},
architectures: [
{
arch: 'amd64',
link: 'http://repository.spotify.com/pool/non-free/s/spotify-client/spotify-client_1.2.53.440.g7b2f582a_amd64.deb',
date: '14.01.2025',
size: '136.86 MB'
}
]
}
];
try {
// Для каждой версии Linux-пакетов формируем строки таблицы
linuxPackages.forEach(version => {
const versionCell = document.createElement('td');
versionCell.className = 'version-cell';
versionCell.rowSpan = version.architectures.length;
const versionText = document.createElement('div');
versionText.className = 'version-text';
versionText.innerHTML = `<span class="short-version">${version.version.short}</span><span class="full-version">${version.version.full}</span>`;
versionText.addEventListener('click', () => {
navigator.clipboard.writeText(version.version.full)
.then(() => showToast('Copied to clipboard'))
.catch(() => fallbackCopyTextToClipboard(version.version.full));
});
versionCell.appendChild(versionText);
version.architectures.forEach((arch, index) => {
const row = document.createElement('tr');
if (index === 0) {
row.appendChild(versionCell);
}
const archCell = document.createElement('td');
archCell.textContent = arch.arch;
row.appendChild(archCell);
const dateCell = document.createElement('td');
dateCell.textContent = arch.date;
row.appendChild(dateCell);
const sizeCell = document.createElement('td');
sizeCell.textContent = arch.size;
row.appendChild(sizeCell);
// Кнопка скачать с обновлением счетчика
const actionCell = document.createElement('td');
actionCell.className = 'action-cell';
const downloadContainer = document.createElement('div');
downloadContainer.className = 'download-container';
const downloadLink = document.createElement('a');
downloadLink.className = 'download-link';
downloadLink.removeAttribute('href');
downloadLink.dataset.download = `https://broad-pine-bbc0.amd64fox1.workers.dev/?file=${encodeURIComponent(arch.link)}`;
// Изменяем тултип для Linux-версий
downloadLink.title = `download ${version.version.short}-${arch.arch}.deb`;
downloadLink.addEventListener('click', (e) => {
e.preventDefault();
// Быстро обновляем UI
incrementDownloadCounter(downloadCountSpan);
// Обновляем глобальный кэш
allDownloadCounters[arch.link] = (parseInt(allDownloadCounters[arch.link] || "0", 10) + 1).toString();
// Параллельно посылаем запрос к воркеру (fire-and-forget)
fetch(`https://broad-pine-bbc0.amd64fox1.workers.dev/?file=${encodeURIComponent(arch.link)}`, { redirect: 'manual' })
.catch(err => console.error('Worker update error:', err));
// Запускаем скачивание файла сразу
window.open(arch.link, '_blank');
});
downloadLink.innerHTML = downloadIcon;
downloadContainer.appendChild(downloadLink);
const downloadCountSpan = document.createElement('div');
downloadCountSpan.style.fontSize = 'small';
downloadCountSpan.title = "downloads";
downloadContainer.appendChild(downloadCountSpan);
actionCell.appendChild(downloadContainer);
row.appendChild(actionCell);
// Добавляем строку в контейнер
container.appendChild(row);
updateDownloadCount(arch.link, downloadCountSpan);
});
});
} catch (error) {
console.error('Ошибка загрузки Linux пакетов:', error);
container.innerHTML = '<tr><td colspan="5">Ошибка загрузки списка пакетов</td></tr>';
}
}
// Логика для принудительного выбора ОС через URL параметры:
document.addEventListener('DOMContentLoaded', () => {
const query = window.location.search;
let osParam = "";
if (query) {
const params = query.slice(1).split('&').filter(p => p.trim());
if (params.length) {
osParam = params[0].toLowerCase();
}
}
if (osParam && ['win', 'mac', 'linux'].includes(osParam)) {
const button = document.querySelector(`.filter-button[data-os="${osParam}"]`);
if (button) {
button.click();
}
}
});
// Обработчик для переключения вкладок (фильтрации по ОС)
document.querySelectorAll('.filter-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.filter-button').forEach(btn => btn.classList.remove('active'));
button.classList.add('active');
currentOS = button.dataset.os;
// Сброс фильтра архитектуры, если выбранный фильтр недоступен для новой ОС
let availableArch = new Set();
allVersions.forEach(([, data]) => {
if (data.links[currentOS]) {
Object.keys(data.links[currentOS]).forEach(arch => availableArch.add(arch));
}
});
if (!availableArch.has(currentArch)) {
currentArch = 'all';
}
// Отменяем контроллер запросов при переключении вкладок, если таковой есть
container.style.opacity = "0";
// Обновляем фильтры архитектур перед анимацией контейнера
updateArchFilters();
setTimeout(() => {
// Если поисковый запрос не пустой, применяем фильтр и сортировку
if (currentSearchTerm !== "") {
// Фильтруем версии с учетом текущей ОС
const filtered = allVersions.filter(([versionKey, data]) => {
return (versionKey.toLowerCase().includes(currentSearchTerm) ||
data.fullversion.toLowerCase().includes(currentSearchTerm)) &&
data.links[currentOS] &&
(currentArch === 'all' || data.links[currentOS].hasOwnProperty(currentArch));
});
// Применяем тот же алгоритм сортировки, что и при поиске
const sortedResults = sortSearchResults(filtered, currentSearchTerm);
observer.unobserve(sentinel);
container.innerHTML = "";
currentIndex = 0;
currentSearchResults = sortedResults;
if (sortedResults.length > 0) {
loadMoreRows();
if (currentIndex < sortedResults.length) {
observer.observe(sentinel);
}
} else {
container.innerHTML = '<tr><td colspan="5">No results for selected OS</td></tr>';
}
} else {
// Если поиска нет, просто загружаем версии для текущей ОС
currentSearchResults = null;
if (currentOS === 'linux') {
loadLinuxPackages();
} else {
container.innerHTML = "";
currentIndex = 0;
loadMoreRows();
if (currentIndex < allVersions.length) {
observer.observe(sentinel);
}
}
}
container.style.opacity = "1";
updateArchFilters();
}, 300);
});
});
</script>
</body>
</html>