feat: конфигурация Glance с виджетами и скриптами развертывания
- Главная конфигурация home.yml и виджеты (countdown, qbittorrent) - PowerShell и Bash скрипты для автоматической загрузки на сервер - Обновлен README с документацией
This commit is contained in:
45
widgets/bookmarks-general.yml
Normal file
45
widgets/bookmarks-general.yml
Normal file
@@ -0,0 +1,45 @@
|
||||
- type: bookmarks
|
||||
groups:
|
||||
- title: Email
|
||||
type: main
|
||||
links:
|
||||
- title: Gmail
|
||||
url: https://mail.google.com/mail/u/0/
|
||||
- title: Games
|
||||
type: main
|
||||
links:
|
||||
- title: itch.io Idle
|
||||
url: https://itch.io/games/new-and-popular/tag-idle
|
||||
- title: Entertainment
|
||||
color: 10 70 50
|
||||
type: public
|
||||
links:
|
||||
- title: YouTube
|
||||
url: https://www.youtube.com/
|
||||
- title: Social
|
||||
type: main
|
||||
links:
|
||||
- title: Twitter(X)
|
||||
url: https://x.com/home
|
||||
- title: Shopping
|
||||
color: 10 70 50
|
||||
type: public
|
||||
links:
|
||||
- title: Yandex
|
||||
url: https://market.yandex.ru/
|
||||
- title: Ozon
|
||||
url: https://www.ozon.ru/
|
||||
- title: AI
|
||||
type: main
|
||||
links:
|
||||
- title: ChatGPT
|
||||
url: https://chatgpt.com/
|
||||
- title: Gemini
|
||||
url: https://gemini.google.com/
|
||||
- title: Trackers
|
||||
type: main
|
||||
links:
|
||||
- title: Rutracker
|
||||
url: https://rutracker.net/
|
||||
- title: RuTor
|
||||
url: https://pornolab.net/
|
||||
44
widgets/bookmarks-homelab.yml
Normal file
44
widgets/bookmarks-homelab.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
- type: bookmarks
|
||||
groups:
|
||||
- title: Homelab - Services
|
||||
color: 200 50 50
|
||||
type: homelab
|
||||
links:
|
||||
- title: Router
|
||||
url: https://192.168.50.1:8443
|
||||
- title: Proxmox
|
||||
url: http://192.168.50.113:8006
|
||||
- title: NPM
|
||||
url: http://192.168.50.38:81/nginx/proxy
|
||||
- title: Gitea
|
||||
url: http://192.168.50.109:3000/
|
||||
- title: Immich
|
||||
url: http://192.168.50.101:2283/
|
||||
- title: Media
|
||||
url: http://192.168.50.108:9999
|
||||
- title: qBittorrent
|
||||
url: http://192.168.50.108:8080
|
||||
- title: Homelab - Useful Links
|
||||
color: 200 50 50
|
||||
type: homelab
|
||||
links:
|
||||
- title: Proxmox Community Scripts
|
||||
url: https://community-scripts.github.io/ProxmoxVE/scripts
|
||||
- title: Homelab - Monitoring
|
||||
color: 200 50 50
|
||||
type: homelab
|
||||
links:
|
||||
- title: Grafana
|
||||
url: http://192.168.50.106:3000/d/af346btjrod8gd/main?from=now-6h&to=now&timezone=browser&refresh=5s
|
||||
- title: Prometheus
|
||||
url: http://192.168.50.105:9090/targets
|
||||
- title: Homelab - Cloud
|
||||
color: 200 50 50
|
||||
type: homelab
|
||||
links:
|
||||
- title: Timeweb Cloud
|
||||
url: https://timeweb.cloud/my/projects/1882441
|
||||
- title: Senko VM
|
||||
url: https://vm.senko.digital/vm/manager/host/list
|
||||
- title: Aeza VM
|
||||
url: https://my.aeza.net/services
|
||||
348
widgets/countdown.yml
Normal file
348
widgets/countdown.yml
Normal file
@@ -0,0 +1,348 @@
|
||||
- type: custom-api
|
||||
title: "Важные даты"
|
||||
url: http://192.168.50.114:5555/countdown
|
||||
cache: 5s
|
||||
template: |
|
||||
{{/* ===== НАСТРОЙКИ ВРЕМЕННОЙ ШКАЛЫ ===== */}}
|
||||
{{/* Переменная шкала: 2 недели (14 дней) + 40 недель = ~1 год */}}
|
||||
{{/* Первая зона: 2 недели = 14 делений (по дням) = 25% шкалы */}}
|
||||
{{/* Вторая зона: 40 недель = 40 делений (по неделям) = 75% шкалы */}}
|
||||
{{ $daysInFirstZone := 14 }}
|
||||
{{ $weeksInSecondZone := 40 }}
|
||||
{{ $firstZonePercent := 25.0 }}
|
||||
{{ $secondZonePercent := 75.0 }}
|
||||
|
||||
<div style="position: relative; padding: 20px 10px;">
|
||||
<!-- ===== КОНТЕЙНЕР TIMELINE3 ===== -->
|
||||
<div id="glance-timeline" style="position: relative; height: 70px; margin: 10px 0;">
|
||||
|
||||
{{/* ===== ОТРИСОВКА ЛИНИИ ВРЕМЕНИ ===== */}}
|
||||
{{/* Зона 1 (0-25%): 14 делений по дням - с мягким красным градиентом */}}
|
||||
{{/* Зона 2 (25-100%): 40 делений по неделям */}}
|
||||
<div style="position: absolute; left: 0; width: 25%; top: 50%; transform: translateY(-50%); height: 3px; background: linear-gradient(to right, rgba(185, 28, 28, 0.4), rgba(128, 128, 128, 0.4)); background-image: linear-gradient(to right, rgba(185, 28, 28, 0.4), rgba(128, 128, 128, 0.4)), linear-gradient(to right, rgba(180, 180, 180, 0.6) 1px, transparent 1px); background-size: 100% 100%, calc(100% / 14) 100%; background-repeat: no-repeat, repeat-x;">
|
||||
</div>
|
||||
<div style="position: absolute; left: 25%; width: 75%; top: 50%; transform: translateY(-50%); height: 3px; background-color: transparent; background-image: linear-gradient(to right, rgba(128,128,128,0.45) 1px, transparent 1px), linear-gradient(to right, rgba(128,128,128,0.75) 1px, transparent 1px); background-size: calc(100% / 40) 100%, calc(100% / 10) 100%; background-repeat: repeat-x, repeat-x;">
|
||||
</div>
|
||||
|
||||
{{/* ===== МАРКЕР "СЕГОДНЯ" ===== */}}
|
||||
{{/* Огонёк показывает текущий момент */}}
|
||||
<div style="position: absolute; left: 0; top: 50%; transform: translate(-50%, -50%); font-size: 24px; text-shadow: 0 0 8px rgba(255, 100, 0, 0.6); filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); z-index: 15;">
|
||||
🔥
|
||||
</div>
|
||||
|
||||
{{/* ===== ОТОБРАЖЕНИЕ СОБЫТИЙ ===== */}}
|
||||
{{/* Позиции рассчитываются с учетом двух зон шкалы */}}
|
||||
{{ $daysZ1 := toFloat $daysInFirstZone }}
|
||||
{{ $weeksZ2 := toFloat $weeksInSecondZone }}
|
||||
{{ $z1Pct := $firstZonePercent }}
|
||||
{{ $z2Pct := $secondZonePercent }}
|
||||
{{ $maxDays := add $daysZ1 (mul $weeksZ2 7.0) }}
|
||||
|
||||
{{ range .JSON.Array "events" }}
|
||||
{{/* Получаем количество дней до события из API /countdown */}}
|
||||
{{ $days := .Int "days" }}
|
||||
{{ $daysFloat := toFloat $days }}
|
||||
|
||||
{{/* ФОРМУЛА: Рассчитываем позицию в зависимости от зоны */}}
|
||||
{{ $pos := 0.0 }}
|
||||
|
||||
{{/* Показываем только события в пределах диапазона (14 дней + 40 недель = 294 дня) */}}
|
||||
{{ if lt $daysFloat $maxDays }}
|
||||
{{ if lt $daysFloat $daysZ1 }}
|
||||
{{/* Зона 1: 0-14 дней -> 0-25% шкалы */}}
|
||||
{{ $pos = mul (div $daysFloat $daysZ1) $z1Pct }}
|
||||
{{ else }}
|
||||
{{/* Зона 2: 14+ дней -> 25-100% шкалы */}}
|
||||
{{ $daysAfterZ1 := sub $daysFloat $daysZ1 }}
|
||||
{{ $weeksInZ2 := div $daysAfterZ1 7.0 }}
|
||||
{{ $posInZ2 := mul (div $weeksInZ2 $weeksZ2) $z2Pct }}
|
||||
{{ $pos = add $z1Pct $posInZ2 }}
|
||||
{{ end }}
|
||||
|
||||
{{/* ===== КОНТЕЙНЕР СОБЫТИЯ ===== */}}
|
||||
{{/* Абсолютное позиционирование: left = рассчитанный процент */}}
|
||||
{{/* transform: translate(-50%, -50%) центрирует emoji точно на линии */}}
|
||||
<div class="event-item" style="position: absolute; left: {{ $pos }}%; top: 35%; transform: translate(-50%, -50%); cursor: pointer; z-index: 10; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);">
|
||||
|
||||
{{/* ===== EMOJI СОБЫТИЯ ===== */}}
|
||||
{{/* При наведении увеличивается и раздвигает соседей */}}
|
||||
<div class="event-emoji" style="font-size: 28px; text-shadow: 0 0 3px rgba(0,0,0,0.3), 0 2px 4px rgba(0,0,0,0.2); transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);">{{ .String "emoji" }}</div>
|
||||
|
||||
{{/* ===== РАСШИРЕННЫЙ TOOLTIP ===== */}}
|
||||
{{/* Большой информативный tooltip с детальной информацией */}}
|
||||
<div class="event-tooltip" style="position: absolute; bottom: 45px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(20,20,25,0.98) 0%, rgba(30,30,40,0.98) 100%); color: white; padding: 12px 16px; border-radius: 10px; font-size: 12px; white-space: nowrap; opacity: 0; pointer-events: none; transition: all 0.2s ease; box-shadow: 0 8px 24px rgba(0,0,0,0.4), 0 0 0 1px rgba(255,255,255,0.1); backdrop-filter: blur(10px); min-width: 200px;">
|
||||
|
||||
{{/* Emoji и название */}}
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 1px solid rgba(255,255,255,0.15);">
|
||||
<div style="font-size: 24px;">{{ .String "emoji" }}</div>
|
||||
<div style="font-weight: bold; font-size: 14px; color: #fff;">{{ .String "name" }}</div>
|
||||
</div>
|
||||
|
||||
{{/* Точная дата события */}}
|
||||
<div style="margin-bottom: 8px; display: flex; align-items: center; gap: 6px;">
|
||||
<div style="opacity: 0.7; font-size: 11px;">📅 Дата:</div>
|
||||
<div style="font-weight: 600; color: #8ab4f8;">{{ .String "date" }}</div>
|
||||
</div>
|
||||
|
||||
{{/* Информация о годовщине (если это anniversary) */}}
|
||||
{{ if eq (.String "type") "anniversary" }}
|
||||
<div style="background: rgba(147, 51, 234, 0.15); padding: 8px; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #9333ea;">
|
||||
<div style="opacity: 0.9; font-size: 11px; margin-bottom: 3px;">🎉 Годовщина</div>
|
||||
{{ $years := .Int "years_passed" }}
|
||||
{{ $lastDigit := mod $years 10 }}
|
||||
{{ $lastTwoDigits := mod $years 100 }}
|
||||
<div style="font-weight: 700; font-size: 15px; color: #c084fc;">{{ $years }} {{ if and (eq $lastDigit 1) (ne $lastTwoDigits 11) }}год{{ else if and (or (eq $lastDigit 2) (eq $lastDigit 3) (eq $lastDigit 4)) (not (or (eq $lastTwoDigits 12) (eq $lastTwoDigits 13) (eq $lastTwoDigits 14))) }}года{{ else }}лет{{ end }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{/* Детальный countdown */}}
|
||||
<div style="background: rgba(255,255,255,0.08); padding: 8px; border-radius: 6px;">
|
||||
<div style="opacity: 0.7; font-size: 10px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px;">⏰ Осталось</div>
|
||||
{{ $days := .Int "days" }}
|
||||
{{ if ge $days 30 }}
|
||||
{{/* Если >= 30 дней: показываем месяцы и дни */}}
|
||||
{{ $months := div $days 30 }}
|
||||
{{ $remainingDays := mod $days 30 }}
|
||||
<div style="display: flex; gap: 12px; justify-content: space-around;">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 16px; font-weight: bold; color: #ff6b9d;">{{ $months }}</div>
|
||||
<div style="font-size: 9px; opacity: 0.6; margin-top: 2px;">{{ if eq $months 1 }}месяц{{ else if or (eq $months 2) (eq $months 3) (eq $months 4) }}месяца{{ else }}месяцев{{ end }}</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 16px; font-weight: bold; color: #ffa94d;">{{ $remainingDays }}</div>
|
||||
<div style="font-size: 9px; opacity: 0.6; margin-top: 2px;">{{ if eq $remainingDays 1 }}день{{ else if or (eq $remainingDays 2) (eq $remainingDays 3) (eq $remainingDays 4) }}дня{{ else }}дней{{ end }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
{{/* Если < 30 дней: показываем дни и часы */}}
|
||||
<div style="display: flex; gap: 12px; justify-content: space-around;">
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 16px; font-weight: bold; color: #ff6b9d;">{{ $days }}</div>
|
||||
<div style="font-size: 9px; opacity: 0.6; margin-top: 2px;">{{ if eq $days 1 }}день{{ else if or (eq $days 2) (eq $days 3) (eq $days 4) }}дня{{ else }}дней{{ end }}</div>
|
||||
</div>
|
||||
<div style="text-align: center;">
|
||||
<div style="font-size: 16px; font-weight: bold; color: #ffa94d;">{{ .Int "hours" }}</div>
|
||||
<div style="font-size: 9px; opacity: 0.6; margin-top: 2px;">{{ if eq (.Int "hours") 1 }}час{{ else if or (eq (.Int "hours") 2) (eq (.Int "hours") 3) (eq (.Int "hours") 4) }}часа{{ else }}часов{{ end }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{/* Треугольная стрелка вниз */}}
|
||||
<div style="position: absolute; bottom: -6px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 6px solid transparent; border-right: 6px solid transparent; border-top: 6px solid rgba(20,20,25,0.98);"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* ===== ИНТЕРАКТИВНАЯ СЕТКА ДЛЯ НАВЕДЕНИЯ ===== */}}
|
||||
{{/* Невидимые зоны, которые показывают дату при наведении */}}
|
||||
{{ range .JSON.Array "timeline_grid" }}
|
||||
{{ $gridDays := .Int "days" }}
|
||||
{{ $gridDaysFloat := toFloat $gridDays }}
|
||||
|
||||
{{/* Рассчитываем позицию точки на шкале */}}
|
||||
{{ $gridPos := 0.0 }}
|
||||
{{ $gridWidth := 0.0 }}
|
||||
|
||||
{{ if lt $gridDaysFloat $maxDays }}
|
||||
{{ if lt $gridDaysFloat $daysZ1 }}
|
||||
{{/* Зона 1: деления по дням */}}
|
||||
{{ $gridPos = mul (div $gridDaysFloat $daysZ1) $z1Pct }}
|
||||
{{ $gridWidth = div $z1Pct $daysZ1 }}
|
||||
{{ else }}
|
||||
{{/* Зона 2: деления по неделям */}}
|
||||
{{ $daysAfterZ1 := sub $gridDaysFloat $daysZ1 }}
|
||||
{{ $weeksInZ2 := div $daysAfterZ1 7.0 }}
|
||||
{{ $posInZ2 := mul (div $weeksInZ2 $weeksZ2) $z2Pct }}
|
||||
{{ $gridPos = add $z1Pct $posInZ2 }}
|
||||
{{ $gridWidth = div $z2Pct $weeksZ2 }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Интерактивная зона */}}
|
||||
<div class="timeline-grid-item" style="position: absolute; left: {{ $gridPos }}%; top: 35%; transform: translateY(-50%); width: {{ $gridWidth }}%; height: 40px; cursor: pointer; z-index: 5;">
|
||||
|
||||
{{/* Tooltip с датой */}}
|
||||
<div class="grid-tooltip" style="position: absolute; bottom: 45px; left: 50%; transform: translateX(-50%); background: linear-gradient(135deg, rgba(30,30,40,0.95) 0%, rgba(40,40,50,0.95) 100%); color: white; padding: 8px 12px; border-radius: 8px; font-size: 11px; white-space: nowrap; opacity: 0; pointer-events: none; transition: all 0.15s ease; box-shadow: 0 4px 12px rgba(0,0,0,0.3), 0 0 0 1px rgba(255,255,255,0.08); backdrop-filter: blur(8px);">
|
||||
|
||||
{{/* День недели */}}
|
||||
<div style="text-align: center; margin-bottom: 4px; font-weight: 600; color: #8ab4f8; font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px;">
|
||||
{{ .String "weekday" }}
|
||||
</div>
|
||||
|
||||
{{/* Дата */}}
|
||||
<div style="text-align: center; font-weight: 500; font-size: 13px;">
|
||||
📅 {{ .String "date" }}
|
||||
</div>
|
||||
|
||||
{{/* Треугольная стрелка */}}
|
||||
<div style="position: absolute; bottom: -5px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 5px solid rgba(30,30,40,0.95);"></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{/* ===== МЕТКИ МЕСЯЦЕВ ПОД ШКАЛОЙ ===== */}}
|
||||
{{/* Используем данные о месяцах из API для элегантного отображения */}}
|
||||
{{ range .JSON.Array "months" }}
|
||||
{{ $monthDays := .Int "days" }}
|
||||
{{ $monthDaysFloat := toFloat $monthDays }}
|
||||
|
||||
{{/* Показываем только метки в пределах видимого диапазона */}}
|
||||
{{ if lt $monthDaysFloat $maxDays }}
|
||||
{{/* Рассчитываем позицию метки */}}
|
||||
{{ $monthPos := 0.0 }}
|
||||
{{if lt $monthDaysFloat $daysZ1 }}
|
||||
{{ $monthPos = mul (div $monthDaysFloat $daysZ1) $z1Pct }}
|
||||
{{ else }}
|
||||
{{ $daysAfter := sub $monthDaysFloat $daysZ1 }}
|
||||
{{ $weeksIn := div $daysAfter 7.0 }}
|
||||
{{ $posIn := mul (div $weeksIn $weeksZ2) $z2Pct }}
|
||||
{{ $monthPos = add $z1Pct $posIn }}
|
||||
{{ end }}
|
||||
|
||||
{{/* Отображаем метку месяца */}}
|
||||
<div style="position: absolute; left: {{ $monthPos }}%; top: 65%; transform: translateX(-50%); font-size: 10px; color: rgba(128,128,128,0.8); white-space: nowrap;">
|
||||
{{ .String "name" }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Показываем tooltip при наведении на родительский элемент */
|
||||
.event-item:hover > .event-tooltip { opacity: 1 !important; pointer-events: auto; }
|
||||
|
||||
/* Показываем tooltip при наведении на интерактивную зону сетки */
|
||||
.timeline-grid-item:hover > .grid-tooltip { opacity: 1 !important; pointer-events: auto; }
|
||||
|
||||
/* Увеличиваем эмодзи при наведении */
|
||||
.event-item:hover .event-emoji {
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
/* Поднимаем z-index у наведенного элемента */
|
||||
.event-item:hover {
|
||||
z-index: 100 !important;
|
||||
}
|
||||
|
||||
/* Плавные переходы */
|
||||
.event-item {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Анимации тряски с разной интенсивностью */
|
||||
@keyframes shake-weak {
|
||||
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
25% { transform: translate(-50%, -50%) rotate(0.5deg); }
|
||||
75% { transform: translate(-50%, -50%) rotate(-0.5deg); }
|
||||
}
|
||||
|
||||
@keyframes shake-medium {
|
||||
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
25% { transform: translate(-50%, -50%) rotate(1deg); }
|
||||
75% { transform: translate(-50%, -50%) rotate(-1deg); }
|
||||
}
|
||||
|
||||
@keyframes shake-strong {
|
||||
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
25% { transform: translate(-50%, -50%) rotate(2deg); }
|
||||
75% { transform: translate(-50%, -50%) rotate(-2deg); }
|
||||
}
|
||||
|
||||
@keyframes shake-very-strong {
|
||||
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
10% { transform: translate(-50%, -50%) rotate(3deg); }
|
||||
30% { transform: translate(-50%, -50%) rotate(-3deg); }
|
||||
50% { transform: translate(-50%, -50%) rotate(3deg); }
|
||||
70% { transform: translate(-50%, -50%) rotate(-3deg); }
|
||||
90% { transform: translate(-50%, -50%) rotate(3deg); }
|
||||
}
|
||||
|
||||
.shake-weak { animation: shake-weak 2s ease-in-out infinite; }
|
||||
.shake-medium { animation: shake-medium 1.5s ease-in-out infinite; }
|
||||
.shake-strong { animation: shake-strong 1s ease-in-out infinite; }
|
||||
.shake-very-strong { animation: shake-very-strong 0.5s ease-in-out infinite; }
|
||||
|
||||
a:hover { color: rgba(255,255,255,0.8) !important; }
|
||||
</style>
|
||||
|
||||
|
||||
<div style="text-align: right; margin-top: 5px; padding-right: 10px;">
|
||||
<a href="http://192.168.50.114:5555/manage" target="_blank" style="color: rgba(255,255,255,0.3); text-decoration: none; font-size: 10px; transition: color 0.2s;">⚙️ Управление</a>
|
||||
</div>
|
||||
|
||||
<img src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7" style="display:none;" onload="
|
||||
(function() {
|
||||
const timeline = document.getElementById('glance-timeline');
|
||||
if (!timeline) return;
|
||||
|
||||
const eventItems = Array.from(timeline.querySelectorAll('.event-item'));
|
||||
if (eventItems.length === 0) return;
|
||||
|
||||
// Применяем тряску в зависимости от позиции (чем левее - тем сильнее)
|
||||
eventItems.forEach(item => {
|
||||
const style = window.getComputedStyle(item);
|
||||
const leftValue = parseFloat(style.left);
|
||||
|
||||
// Позиция в процентах: 0-25% = первая зона (0-14 дней)
|
||||
if (leftValue <= 25) {
|
||||
// Чем ближе к 0%, тем сильнее тряска
|
||||
if (leftValue <= 5) {
|
||||
item.classList.add('shake-very-strong');
|
||||
} else if (leftValue <= 12) {
|
||||
item.classList.add('shake-strong');
|
||||
} else if (leftValue <= 18) {
|
||||
item.classList.add('shake-medium');
|
||||
} else {
|
||||
item.classList.add('shake-weak');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const REPULSION_RADIUS = 80;
|
||||
const REPULSION_STRENGTH = 20;
|
||||
|
||||
eventItems.forEach(hoverItem => {
|
||||
hoverItem.addEventListener('mouseenter', function() {
|
||||
const hoveredRect = hoverItem.getBoundingClientRect();
|
||||
const timelineRect = timeline.getBoundingClientRect();
|
||||
const hoveredX = hoveredRect.left + hoveredRect.width / 2 - timelineRect.left;
|
||||
const hoveredY = hoveredRect.top + hoveredRect.height / 2 - timelineRect.top;
|
||||
|
||||
eventItems.forEach(item => {
|
||||
if (item === hoverItem) return;
|
||||
|
||||
const itemRect = item.getBoundingClientRect();
|
||||
const itemX = itemRect.left + itemRect.width / 2 - timelineRect.left;
|
||||
const itemY = itemRect.top + itemRect.height / 2 - timelineRect.top;
|
||||
|
||||
const dx = itemX - hoveredX;
|
||||
const dy = itemY - hoveredY;
|
||||
const distance = Math.sqrt(dx * dx + dy * dy);
|
||||
|
||||
if (distance < REPULSION_RADIUS && distance > 0) {
|
||||
const nx = dx / distance;
|
||||
const ny = dy / distance;
|
||||
const force = (1 - distance / REPULSION_RADIUS) * REPULSION_STRENGTH;
|
||||
|
||||
const offsetX = nx * force;
|
||||
const offsetY = ny * force;
|
||||
|
||||
item.style.transform = 'translate(calc(-50% + ' + offsetX + 'px), calc(-50% + ' + offsetY + 'px))';
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
hoverItem.addEventListener('mouseleave', function() {
|
||||
eventItems.forEach(item => {
|
||||
if (item !== hoverItem) {
|
||||
item.style.transform = 'translate(-50%, -50%)';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
">
|
||||
187
widgets/qbittorrent.yml
Normal file
187
widgets/qbittorrent.yml
Normal file
@@ -0,0 +1,187 @@
|
||||
- type: custom-api
|
||||
title: qBittorrent
|
||||
cache: 10s
|
||||
options:
|
||||
view: "basic" # "basic" or "detailed"
|
||||
mode: "default" # "default" or "upload"
|
||||
subrequests:
|
||||
transfer:
|
||||
url: http://192.168.50.108:8080/api/v2/transfer/info
|
||||
seeding:
|
||||
url: http://192.168.50.108:8080/api/v2/torrents/info
|
||||
parameters:
|
||||
filter: seeding
|
||||
leeching:
|
||||
url: http://192.168.50.108:8080/api/v2/torrents/info
|
||||
parameters:
|
||||
filter: downloading
|
||||
template: |
|
||||
{{ $transfer := .Subrequest "transfer" }}
|
||||
{{ $seeding := .Subrequest "seeding" }}
|
||||
{{ $leeching := .Subrequest "leeching" }}
|
||||
|
||||
{{ if and (eq $transfer.Response.StatusCode 200) (eq $seeding.Response.StatusCode 200) (eq $leeching.Response.StatusCode 200) }}
|
||||
|
||||
{{ $isDetailed := eq (.Options.StringOr "view" "detailed") "detailed" }}
|
||||
{{ $mode := .Options.StringOr "mode" "default" }}
|
||||
|
||||
{{ if $isDetailed }}
|
||||
<!-- Detailed View -->
|
||||
<div class="list" style="--list-gap: 15px;">
|
||||
<div class="flex justify-between text-center">
|
||||
<div>
|
||||
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }}
|
||||
{{ if eq $mode "upload" }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}</div>
|
||||
{{ else }}
|
||||
{{ if lt $dlSpeed 1048576.0 }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.0f KiB/s" (div $dlSpeed 1024.0) }}</div>
|
||||
{{ else }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.1f MiB/s" (div $dlSpeed 1048576.0) }}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="size-h6">DOWNLOADING</div>
|
||||
</div>
|
||||
|
||||
{{ if eq $mode "upload" }}
|
||||
<div>
|
||||
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}</div>
|
||||
<div class="size-h6">UPLOADING</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div>
|
||||
<div class="color-highlight size-h3">{{ len ($seeding.JSON.Array "") }}</div>
|
||||
<div class="size-h6">SEEDING</div>
|
||||
</div>
|
||||
|
||||
{{ if eq $mode "default" }}
|
||||
<div>
|
||||
<div class="color-highlight size-h3">{{ len ($leeching.JSON.Array "") }}</div>
|
||||
<div class="size-h6">LEECHING</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<!-- Downloading list -->
|
||||
{{ $downloadingTorrents := $leeching.JSON.Array "" }}
|
||||
{{ if gt (len $downloadingTorrents) 0 }}
|
||||
<div style="margin-top: 15px;">
|
||||
<ul class="list collapsible-container" data-collapse-after="0" style="--list-gap: 15px;">
|
||||
{{ range $t := $downloadingTorrents }}
|
||||
{{ $state := $t.String "state" }}
|
||||
{{ $icon := "?" }}
|
||||
{{ if ge ($t.Int "completed") ($t.Int "size") }}{{ $icon = "✔" }}
|
||||
{{ else if eq $state "downloading" "forcedDL" }}{{ $icon = "↓" }}
|
||||
{{ else if eq $state "pausedDL" "stoppedDL" "pausedUP" "stalledDL" "stalledUP" "queuedDL" "queuedUP" }}{{ $icon = "❚❚" }}
|
||||
{{ else if eq $state "error" "missingFiles" }}{{ $icon = "!" }}
|
||||
{{ else if eq $state "checkingDL" "checkingUP" "allocating" }}{{ $icon = "…" }}
|
||||
{{ else if eq $state "checkingResumeData" }}{{ $icon = "⟳" }}
|
||||
{{ end }}
|
||||
|
||||
<li class="flex items-center" style="gap: 10px;">
|
||||
<div class="size-h4" style="flex-shrink: 0;">{{ $icon }}</div>
|
||||
<div style="flex-grow: 1; min-width: 0;">
|
||||
<div class="text-truncate color-highlight">{{ $t.String "name" }}</div>
|
||||
<div title="{{ $t.Float "progress" | mul 100 | printf "%.1f" }}%" style="background: rgba(128, 128, 128, 0.2); border-radius: 5px; height: 6px; margin-top: 5px; overflow: hidden;">
|
||||
<div style="width: {{ $t.Float "progress" | mul 100 }}%; background-color: var(--color-positive); height: 100%; border-radius: 5px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-shrink: 0; text-align: right; width: 80px;">
|
||||
{{ $dlSpeed := $t.Float "dlspeed" }}
|
||||
<div class="size-sm color-paragraph">
|
||||
{{ if eq $mode "upload" }}
|
||||
{{ if lt $dlSpeed 1000.0 }}--{{ else }}{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}{{ end }}
|
||||
{{ else }}
|
||||
{{ if lt $dlSpeed 1024.0 }}--{{ else if lt $dlSpeed 1048576.0 }}{{ printf "%.0f KiB/s" (div $dlSpeed 1024.0) }}{{ else }}{{ printf "%.1f MiB/s" (div $dlSpeed 1048576.0) }}{{ end }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ $eta := $t.Int "eta" }}
|
||||
<div class="size-sm color-paragraph">
|
||||
{{ if eq $eta 8640000 }}∞
|
||||
{{ else if gt $eta 3600 }}{{ printf "%dh %dm" (div $eta 3600) (mod (div $eta 60) 60) }}
|
||||
{{ else if gt $eta 0 }}{{ printf "%dm" (div $eta 60) }}
|
||||
{{ else }}--{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<!-- Seeding list -->
|
||||
{{ if eq $mode "upload" }}
|
||||
{{ $seedingTorrents := $seeding.JSON.Array "" }}
|
||||
{{ if gt (len $seedingTorrents) 0 }}
|
||||
<div style="margin-top: 20px;">
|
||||
<ul class="list collapsible-container" data-collapse-after="0" style="--list-gap: 15px;">
|
||||
{{ range $t := $seedingTorrents }}
|
||||
{{ $state := $t.String "state" }}
|
||||
{{ $icon := "↑" }}
|
||||
{{ if eq $state "pausedUP" "stoppedUP" "stalledUP" "queuedUP" }}{{ $icon = "❚❚" }}
|
||||
{{ else if eq $state "error" "missingFiles" }}{{ $icon = "!" }}
|
||||
{{ else if eq $state "checkingUP" }}{{ $icon = "…" }}
|
||||
{{ else if eq $state "checkingResumeData" }}{{ $icon = "⟳" }}
|
||||
{{ end }}
|
||||
|
||||
<li class="flex items-center" style="gap: 10px;">
|
||||
<div class="size-h4" style="flex-shrink: 0;">{{ $icon }}</div>
|
||||
<div style="flex-grow: 1; min-width: 0;">
|
||||
<div class="text-truncate color-highlight">{{ $t.String "name" }}</div>
|
||||
<div class="size-sm color-paragraph">
|
||||
Ratio: {{ printf "%.2f" ($t.Float "ratio") }} |
|
||||
Size: {{ printf "%.1f GB" (div ($t.Float "size") 1073741824.0) }}
|
||||
</div>
|
||||
</div>
|
||||
<div style="flex-shrink: 0; text-align: right; width: 80px;">
|
||||
{{ $ulSpeed := $t.Float "upspeed" }}
|
||||
<div class="size-sm color-paragraph">
|
||||
{{ if lt $ulSpeed 1000.0 }}--{{ else }}{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}{{ end }}
|
||||
</div>
|
||||
<div class="size-sm color-paragraph">Upload</div>
|
||||
</div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
</div>
|
||||
|
||||
{{ else }}
|
||||
<!-- Basic View -->
|
||||
<div class="flex justify-between text-center">
|
||||
<div>
|
||||
{{ $dlSpeed := $transfer.JSON.Float "dl_info_speed" }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $dlSpeed 1000000.0) }}</div>
|
||||
<div class="size-h6">DOWNLOADING</div>
|
||||
</div>
|
||||
{{ if eq $mode "upload" }}
|
||||
<div>
|
||||
{{ $ulSpeed := $transfer.JSON.Float "up_info_speed" }}
|
||||
<div class="color-highlight size-h3">{{ printf "%.1f MB/s" (div $ulSpeed 1000000.0) }}</div>
|
||||
<div class="size-h6">UPLOADING</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
<div>
|
||||
<div class="color-highlight size-h3">{{ len ($seeding.JSON.Array "") }}</div>
|
||||
<div class="size-h6">SEEDING</div>
|
||||
</div>
|
||||
{{ if eq $mode "default" }}
|
||||
<div>
|
||||
<div class="color-highlight size-h3">{{ len ($leeching.JSON.Array "") }}</div>
|
||||
<div class="size-h6">LEECHING</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ else }}
|
||||
<div class="color-negative text-center">
|
||||
<p>Error fetching qBittorrent data.</p>
|
||||
<p class="size-sm">Check URL and authentication bypass settings.</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
Reference in New Issue
Block a user