feat: конфигурация Glance с виджетами и скриптами развертывания
- Главная конфигурация home.yml и виджеты (countdown, qbittorrent) - PowerShell и Bash скрипты для автоматической загрузки на сервер - Обновлен README с документацией
This commit is contained in:
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%)';
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
">
|
||||
Reference in New Issue
Block a user