feat: конфигурация Glance с виджетами и скриптами развертывания

- Главная конфигурация home.yml и виджеты (countdown, qbittorrent)
- PowerShell и Bash скрипты для автоматической загрузки на сервер
- Обновлен README с документацией
This commit is contained in:
2025-12-06 09:45:17 +03:00
parent c9748f1bbd
commit abd62d8f73
8 changed files with 987 additions and 1 deletions

348
widgets/countdown.yml Normal file
View 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%)';
}
});
});
});
})();
">