1402 lines
51 KiB
HTML
1402 lines
51 KiB
HTML
<!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>
|