mirror of
https://github.com/glanceapp/glance.git
synced 2025-06-24 11:51:31 +02:00
Add new calendar and deprecate old
This commit is contained in:
parent
24a6107171
commit
306fb3cb33
33
internal/glance/static/js/animations.js
Normal file
33
internal/glance/static/js/animations.js
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
export const easeOutQuint = 'cubic-bezier(0.22, 1, 0.36, 1)';
|
||||||
|
|
||||||
|
export function directions(anim, opt, ...dirs) {
|
||||||
|
return dirs.map(dir => anim({ direction: dir, ...opt }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function slideFade({
|
||||||
|
direction = 'left',
|
||||||
|
fill = 'backwards',
|
||||||
|
duration = 200,
|
||||||
|
distance = '1rem',
|
||||||
|
easing = 'ease',
|
||||||
|
offset = 0,
|
||||||
|
}) {
|
||||||
|
const axis = direction === 'left' || direction === 'right' ? 'X' : 'Y';
|
||||||
|
const negative = direction === 'left' || direction === 'up' ? '-' : '';
|
||||||
|
const amount = negative + distance;
|
||||||
|
|
||||||
|
return {
|
||||||
|
keyframes: [
|
||||||
|
{
|
||||||
|
offset: offset,
|
||||||
|
opacity: 0,
|
||||||
|
transform: `translate${axis}(${amount})`,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
options: {
|
||||||
|
duration: duration,
|
||||||
|
easing: easing,
|
||||||
|
fill: fill,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
212
internal/glance/static/js/calendar.js
Normal file
212
internal/glance/static/js/calendar.js
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
import { directions, easeOutQuint, slideFade } from "./animations.js";
|
||||||
|
import { elem, repeat, text } from "./templating.js";
|
||||||
|
|
||||||
|
const FULL_MONTH_SLOTS = 7*6;
|
||||||
|
const WEEKDAY_ABBRS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
|
||||||
|
const MONTH_NAMES = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
|
||||||
|
|
||||||
|
const leftArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const rightArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const undoArrowSvg = `<svg stroke="var(--color-text-base)" fill="none" viewBox="0 0 24 24" stroke-width="1.5" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15 3 9m0 0 6-6M3 9h12a6 6 0 0 1 0 12h-3" />
|
||||||
|
</svg>`;
|
||||||
|
|
||||||
|
const [datesExitLeft, datesExitRight] = directions(
|
||||||
|
slideFade, { distance: "2rem", duration: 120, offset: 1 },
|
||||||
|
"left", "right"
|
||||||
|
);
|
||||||
|
|
||||||
|
const [datesEntranceLeft, datesEntranceRight] = directions(
|
||||||
|
slideFade, { distance: "0.8rem", duration: 500, easing: easeOutQuint },
|
||||||
|
"left", "right"
|
||||||
|
);
|
||||||
|
|
||||||
|
const undoEntrance = slideFade({ direction: "left", distance: "100%", duration: 300 });
|
||||||
|
|
||||||
|
export default function(element) {
|
||||||
|
element.swap(Calendar(
|
||||||
|
Number(element.dataset.firstDayOfWeek ?? 1)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when viewing the previous/next month, display the current date if it's within the spill-over days
|
||||||
|
function Calendar(firstDay) {
|
||||||
|
let header, dates;
|
||||||
|
let advanceTimeTicker;
|
||||||
|
let now = new Date();
|
||||||
|
let activeDate;
|
||||||
|
|
||||||
|
const update = (newDate) => {
|
||||||
|
header.component.update(now, newDate);
|
||||||
|
dates.component.update(now, newDate);
|
||||||
|
activeDate = newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoAdvanceNow = () => {
|
||||||
|
advanceTimeTicker = setTimeout(() => {
|
||||||
|
// TODO: don't auto advance if looking at a different month
|
||||||
|
update(now = new Date());
|
||||||
|
autoAdvanceNow();
|
||||||
|
}, msTillNextDay());
|
||||||
|
};
|
||||||
|
|
||||||
|
const adjacentMonth = (dir) => new Date(activeDate.getFullYear(), activeDate.getMonth() + dir, 1);
|
||||||
|
const nextClicked = () => update(adjacentMonth(1));
|
||||||
|
const prevClicked = () => update(adjacentMonth(-1));
|
||||||
|
const undoClicked = () => update(now);
|
||||||
|
|
||||||
|
const calendar = elem().classes("calendar").append(
|
||||||
|
header = Header(nextClicked, prevClicked, undoClicked),
|
||||||
|
dates = Dates(firstDay)
|
||||||
|
);
|
||||||
|
|
||||||
|
update(now);
|
||||||
|
autoAdvanceNow();
|
||||||
|
|
||||||
|
return calendar.component({
|
||||||
|
suspend: () => clearTimeout(advanceTimeTicker)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Header(nextClicked, prevClicked, undoClicked) {
|
||||||
|
let month, monthNumber, year, undo;
|
||||||
|
const button = () => elem("button").classes("calendar-header-button");
|
||||||
|
|
||||||
|
const monthAndYear = elem().classes("size-h2", "color-highlight").append(
|
||||||
|
month = text(),
|
||||||
|
" ",
|
||||||
|
year = elem("span").classes("size-h3"),
|
||||||
|
undo = button()
|
||||||
|
.hide()
|
||||||
|
.classes("calendar-undo-button")
|
||||||
|
.attr("title", "Back to current month")
|
||||||
|
.on("click", undoClicked)
|
||||||
|
.html(undoArrowSvg)
|
||||||
|
);
|
||||||
|
|
||||||
|
const monthSwitcher = elem()
|
||||||
|
.classes("flex", "gap-7", "items-center")
|
||||||
|
.append(
|
||||||
|
button()
|
||||||
|
.attr("title", "Previous month")
|
||||||
|
.on("click", prevClicked)
|
||||||
|
.html(leftArrowSvg),
|
||||||
|
monthNumber = elem()
|
||||||
|
.classes("color-highlight")
|
||||||
|
.styles({ marginTop: "0.1rem" }),
|
||||||
|
button()
|
||||||
|
.attr("title", "Next month")
|
||||||
|
.on("click", nextClicked)
|
||||||
|
.html(rightArrowSvg),
|
||||||
|
);
|
||||||
|
|
||||||
|
return elem().classes("flex", "justify-between", "items-center").append(
|
||||||
|
monthAndYear,
|
||||||
|
monthSwitcher
|
||||||
|
).component({
|
||||||
|
update: function (now, newDate) {
|
||||||
|
month.text(MONTH_NAMES[newDate.getMonth()]);
|
||||||
|
year.text(newDate.getFullYear());
|
||||||
|
const m = newDate.getMonth() + 1;
|
||||||
|
monthNumber.text((m < 10 ? "0" : "") + m);
|
||||||
|
|
||||||
|
if (!datesWithinSameMonth(now, newDate)) {
|
||||||
|
if (undo.isHidden()) undo.show().animate(undoEntrance);
|
||||||
|
} else {
|
||||||
|
undo.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function Dates(firstDay) {
|
||||||
|
let dates, lastRenderedDate;
|
||||||
|
|
||||||
|
const updateFullMonth = function(now, newDate) {
|
||||||
|
const firstWeekday = new Date(newDate.getFullYear(), newDate.getMonth(), 1).getDay();
|
||||||
|
const previousMonthSpilloverDays = (firstWeekday - firstDay + 7) % 7 || 7;
|
||||||
|
const currentMonthDays = daysInMonth(newDate.getFullYear(), newDate.getMonth());
|
||||||
|
const nextMonthSpilloverDays = FULL_MONTH_SLOTS - (previousMonthSpilloverDays + currentMonthDays);
|
||||||
|
const previousMonthDays = daysInMonth(newDate.getFullYear(), newDate.getMonth() - 1)
|
||||||
|
const isCurrentMonth = datesWithinSameMonth(now, newDate);
|
||||||
|
const currentDate = now.getDate();
|
||||||
|
|
||||||
|
let children = dates.children;
|
||||||
|
let index = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < FULL_MONTH_SLOTS; i++) {
|
||||||
|
children[i].clearClasses("calendar-spillover-date", "calendar-current-date");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < previousMonthSpilloverDays; i++, index++) {
|
||||||
|
children[index].classes("calendar-spillover-date").text(
|
||||||
|
previousMonthDays - previousMonthSpilloverDays + i + 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 1; i <= currentMonthDays; i++, index++) {
|
||||||
|
children[index]
|
||||||
|
.classesIf(isCurrentMonth && i === currentDate, "calendar-current-date")
|
||||||
|
.text(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < nextMonthSpilloverDays; i++, index++) {
|
||||||
|
children[index].classes("calendar-spillover-date").text(i + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastRenderedDate = newDate;
|
||||||
|
};
|
||||||
|
|
||||||
|
const update = function(now, newDate) {
|
||||||
|
if (lastRenderedDate === undefined || datesWithinSameMonth(newDate, lastRenderedDate)) {
|
||||||
|
updateFullMonth(now, newDate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = newDate > lastRenderedDate;
|
||||||
|
dates.animateUpdate(
|
||||||
|
() => updateFullMonth(now, newDate),
|
||||||
|
next ? datesExitLeft : datesExitRight,
|
||||||
|
next ? datesEntranceRight : datesEntranceLeft,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return elem().append(
|
||||||
|
elem().classes("calendar-dates", "margin-top-15").append(
|
||||||
|
...repeat(7, (i) => elem().classes("size-h6", "color-subdue").text(
|
||||||
|
WEEKDAY_ABBRS[(firstDay + i) % 7]
|
||||||
|
))
|
||||||
|
),
|
||||||
|
|
||||||
|
dates = elem().classes("calendar-dates", "margin-top-3").append(
|
||||||
|
...elem().classes("calendar-date").duplicate(FULL_MONTH_SLOTS)
|
||||||
|
)
|
||||||
|
).component({ update });
|
||||||
|
}
|
||||||
|
|
||||||
|
function datesWithinSameMonth(d1, d2) {
|
||||||
|
return d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
function daysInMonth(year, month) {
|
||||||
|
return new Date(year, month + 1, 0).getDate();
|
||||||
|
}
|
||||||
|
|
||||||
|
function msTillNextDay(now) {
|
||||||
|
now = now || new Date();
|
||||||
|
|
||||||
|
return 86_400_000 - (
|
||||||
|
now.getMilliseconds() +
|
||||||
|
now.getSeconds() * 1000 +
|
||||||
|
now.getMinutes() * 60_000 +
|
||||||
|
now.getHours() * 3_600_000
|
||||||
|
);
|
||||||
|
}
|
@ -625,6 +625,17 @@ function setupClocks() {
|
|||||||
updateClocks();
|
updateClocks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function setupCalendars() {
|
||||||
|
const elems = document.getElementsByClassName("calendar");
|
||||||
|
if (elems.length == 0) return;
|
||||||
|
|
||||||
|
// TODO: implement prefetching, currently loads as a nasty waterfall of requests
|
||||||
|
const calendar = await import ('./calendar.js');
|
||||||
|
|
||||||
|
for (let i = 0; i < elems.length; i++)
|
||||||
|
calendar.default(elems[i]);
|
||||||
|
}
|
||||||
|
|
||||||
function setupTruncatedElementTitles() {
|
function setupTruncatedElementTitles() {
|
||||||
const elements = document.querySelectorAll(".text-truncate, .single-line-titles .title, .text-truncate-2-lines, .text-truncate-3-lines");
|
const elements = document.querySelectorAll(".text-truncate, .single-line-titles .title, .text-truncate-2-lines, .text-truncate-3-lines");
|
||||||
|
|
||||||
@ -648,6 +659,7 @@ async function setupPage() {
|
|||||||
try {
|
try {
|
||||||
setupPopovers();
|
setupPopovers();
|
||||||
setupClocks()
|
setupClocks()
|
||||||
|
await setupCalendars();
|
||||||
setupCarousels();
|
setupCarousels();
|
||||||
setupSearchBoxes();
|
setupSearchBoxes();
|
||||||
setupCollapsibleLists();
|
setupCollapsibleLists();
|
||||||
|
190
internal/glance/static/js/templating.js
Normal file
190
internal/glance/static/js/templating.js
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
export function elem(tag = "div") {
|
||||||
|
return document.createElement(tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fragment(...children) {
|
||||||
|
const f = document.createDocumentFragment();
|
||||||
|
if (children) f.append(...children);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function text(str = "") {
|
||||||
|
return document.createTextNode(str);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repeat(n, fn) {
|
||||||
|
const elems = Array(n);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++)
|
||||||
|
elems[i] = fn(i);
|
||||||
|
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function find(selector) {
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findAll(selector) {
|
||||||
|
return document.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ep = HTMLElement.prototype;
|
||||||
|
const fp = DocumentFragment.prototype;
|
||||||
|
const tp = Text.prototype;
|
||||||
|
|
||||||
|
ep.classes = function(...classes) {
|
||||||
|
this.classList.add(...classes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.find = function(selector) {
|
||||||
|
return this.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.findAll = function(selector) {
|
||||||
|
return this.querySelectorAll(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.classesIf = function(cond, ...classes) {
|
||||||
|
cond ? this.classList.add(...classes) : this.classList.remove(...classes);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.hide = function() {
|
||||||
|
this.style.display = "none";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.show = function() {
|
||||||
|
this.style.removeProperty("display");
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.showIf = function(cond) {
|
||||||
|
cond ? this.show() : this.hide();
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.isHidden = function() {
|
||||||
|
return this.style.display === "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.clearClasses = function(...classes) {
|
||||||
|
classes.length ? this.classList.remove(...classes) : this.className = "";
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.hasClass = function(className) {
|
||||||
|
return this.classList.contains(className);
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.attr = function(name, value) {
|
||||||
|
this.setAttribute(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.attrs = function(attrs) {
|
||||||
|
for (const [name, value] of Object.entries(attrs))
|
||||||
|
this.setAttribute(name, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.tap = function(fn) {
|
||||||
|
fn(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.text = function(text) {
|
||||||
|
this.innerText = text;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.html = function(html) {
|
||||||
|
this.innerHTML = html;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.appendTo = function(parent) {
|
||||||
|
parent.appendChild(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.swap = function(element) {
|
||||||
|
this.replaceWith(element);
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.on = function(event, callback, options) {
|
||||||
|
if (typeof event === "string") {
|
||||||
|
this.addEventListener(event, callback, options);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < event.length; i++)
|
||||||
|
this.addEventListener(event[i], callback, options);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const epAppend = ep.append;
|
||||||
|
ep.append = function(...children) {
|
||||||
|
epAppend.apply(this, children);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.duplicate = function(n) {
|
||||||
|
const elems = Array(n);
|
||||||
|
|
||||||
|
for (let i = 0; i < n; i++)
|
||||||
|
elems[i] = this.cloneNode(true);
|
||||||
|
|
||||||
|
return elems;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.styles = function(s) {
|
||||||
|
Object.assign(this.style, s);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const epAnimate = ep.animate;
|
||||||
|
ep.animate = function(anim, callback) {
|
||||||
|
const a = epAnimate.call(this, anim.keyframes, anim.options);
|
||||||
|
if (callback) a.onfinish = () => callback(this, a);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.animateUpdate = function(update, exit, entrance) {
|
||||||
|
this.animate(exit, () => {
|
||||||
|
update(this);
|
||||||
|
this.animate(entrance);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.styleVar = function(name, value) {
|
||||||
|
this.style.setProperty(`--${name}`, value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.component = function (methods) {
|
||||||
|
this.component = methods;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fpAppend = fp.append;
|
||||||
|
fp.append = function(...children) {
|
||||||
|
fpAppend.apply(this, children);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
fp.appendTo = function(parent) {
|
||||||
|
parent.appendChild(this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
tp.text = function(text) {
|
||||||
|
this.nodeValue = text;
|
||||||
|
return this;
|
||||||
|
}
|
@ -17,7 +17,7 @@
|
|||||||
--cm: 1;
|
--cm: 1;
|
||||||
--tsm: 1;
|
--tsm: 1;
|
||||||
|
|
||||||
--widget-gap: 25px;
|
--widget-gap: 23px;
|
||||||
--widget-content-vertical-padding: 15px;
|
--widget-content-vertical-padding: 15px;
|
||||||
--widget-content-horizontal-padding: 17px;
|
--widget-content-horizontal-padding: 17px;
|
||||||
--widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
|
--widget-content-padding: var(--widget-content-vertical-padding) var(--widget-content-horizontal-padding);
|
||||||
@ -294,6 +294,12 @@ pre {
|
|||||||
width: 10px;
|
width: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary);
|
||||||
|
outline-offset: 0.1rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
}
|
||||||
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
@ -1074,19 +1080,78 @@ details[open] .summary::after {
|
|||||||
filter: invert(1);
|
filter: invert(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.calendar-day {
|
.old-calendar-day {
|
||||||
width: calc(100% / 7);
|
width: calc(100% / 7);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.6rem 0;
|
padding: 0.6rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.old-calendar-day-today {
|
||||||
.calendar-day-today {
|
|
||||||
border-radius: var(--border-radius);
|
border-radius: var(--border-radius);
|
||||||
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%)));
|
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) (var(--bgl)) + 6%)));
|
||||||
color: var(--color-text-highlight);
|
color: var(--color-text-highlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.calendar-dates {
|
||||||
|
text-align: center;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-date {
|
||||||
|
padding: 0.4rem 0;
|
||||||
|
color: var(--color-text-paragraph);
|
||||||
|
position: relative;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-current-date {
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-popover-border);
|
||||||
|
color: var(--color-text-highlight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-spillover-date {
|
||||||
|
color: var(--color-text-subdue);
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header-button {
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
z-index: 1;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header-button::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -0.2rem;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
background-color: var(--color-text-subdue);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-header-button:hover::before {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.calendar-undo-button {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: text-top;
|
||||||
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
|
margin-left: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
.dns-stats-totals {
|
.dns-stats-totals {
|
||||||
transition: opacity .3s;
|
transition: opacity .3s;
|
||||||
transition-delay: 50ms;
|
transition-delay: 50ms;
|
||||||
|
@ -2,33 +2,6 @@
|
|||||||
|
|
||||||
{{ define "widget-content" }}
|
{{ define "widget-content" }}
|
||||||
<div class="widget-small-content-bounds">
|
<div class="widget-small-content-bounds">
|
||||||
<div class="flex justify-between items-center">
|
<div class="calendar" data-first-day-of-week="{{ .FirstDay }}"></div>
|
||||||
<div class="color-highlight size-h1">{{ .Calendar.CurrentMonthName }}</div>
|
|
||||||
<ul class="list-horizontal-text color-highlight size-h4">
|
|
||||||
<li>Week {{ .Calendar.CurrentWeekNumber }}</li>
|
|
||||||
<li>{{ .Calendar.CurrentYear }}</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap size-h6 margin-top-10 color-subdue">
|
|
||||||
{{ if .StartSunday }}
|
|
||||||
<div class="calendar-day">Su</div>
|
|
||||||
{{ end }}
|
|
||||||
<div class="calendar-day">Mo</div>
|
|
||||||
<div class="calendar-day">Tu</div>
|
|
||||||
<div class="calendar-day">We</div>
|
|
||||||
<div class="calendar-day">Th</div>
|
|
||||||
<div class="calendar-day">Fr</div>
|
|
||||||
<div class="calendar-day">Sa</div>
|
|
||||||
{{ if not .StartSunday }}
|
|
||||||
<div class="calendar-day">Su</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap">
|
|
||||||
{{ range .Calendar.Days }}
|
|
||||||
<div class="calendar-day{{ if eq . $.Calendar.CurrentDay }} calendar-day-today{{ end }}">{{ . }}</div>
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
34
internal/glance/templates/old-calendar.html
Normal file
34
internal/glance/templates/old-calendar.html
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
{{ template "widget-base.html" . }}
|
||||||
|
|
||||||
|
{{ define "widget-content" }}
|
||||||
|
<div class="widget-small-content-bounds">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<div class="color-highlight size-h1">{{ .Calendar.CurrentMonthName }}</div>
|
||||||
|
<ul class="list-horizontal-text color-highlight size-h4">
|
||||||
|
<li>Week {{ .Calendar.CurrentWeekNumber }}</li>
|
||||||
|
<li>{{ .Calendar.CurrentYear }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap size-h6 margin-top-10 color-subdue">
|
||||||
|
{{ if .StartSunday }}
|
||||||
|
<div class="old-calendar-day">Su</div>
|
||||||
|
{{ end }}
|
||||||
|
<div class="old-calendar-day">Mo</div>
|
||||||
|
<div class="old-calendar-day">Tu</div>
|
||||||
|
<div class="old-calendar-day">We</div>
|
||||||
|
<div class="old-calendar-day">Th</div>
|
||||||
|
<div class="old-calendar-day">Fr</div>
|
||||||
|
<div class="old-calendar-day">Sa</div>
|
||||||
|
{{ if not .StartSunday }}
|
||||||
|
<div class="old-calendar-day">Su</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap">
|
||||||
|
{{ range .Calendar.Days }}
|
||||||
|
<div class="old-calendar-day{{ if eq . $.Calendar.CurrentDay }} old-calendar-day-today{{ end }}">{{ . }}</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
@ -1,86 +1,45 @@
|
|||||||
package glance
|
package glance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var calendarWidgetTemplate = mustParseTemplate("calendar.html", "widget-base.html")
|
var calendarWidgetTemplate = mustParseTemplate("calendar.html", "widget-base.html")
|
||||||
|
|
||||||
|
var calendarWeekdaysToInt = map[string]time.Weekday{
|
||||||
|
"sunday": time.Sunday,
|
||||||
|
"monday": time.Monday,
|
||||||
|
"tuesday": time.Tuesday,
|
||||||
|
"wednesday": time.Wednesday,
|
||||||
|
"thursday": time.Thursday,
|
||||||
|
"friday": time.Friday,
|
||||||
|
"saturday": time.Saturday,
|
||||||
|
}
|
||||||
|
|
||||||
type calendarWidget struct {
|
type calendarWidget struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
Calendar *calendar
|
FirstDayOfWeek string `yaml:"first-day-of-week"`
|
||||||
StartSunday bool `yaml:"start-sunday"`
|
FirstDay int `yaml:"-"`
|
||||||
|
cachedHTML template.HTML `yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *calendarWidget) initialize() error {
|
func (widget *calendarWidget) initialize() error {
|
||||||
widget.withTitle("Calendar").withCacheOnTheHour()
|
widget.withTitle("Calendar").withError(nil)
|
||||||
|
|
||||||
|
if widget.FirstDayOfWeek == "" {
|
||||||
|
widget.FirstDayOfWeek = "monday"
|
||||||
|
} else if _, ok := calendarWeekdaysToInt[widget.FirstDayOfWeek]; !ok {
|
||||||
|
return errors.New("invalid first day of week")
|
||||||
|
}
|
||||||
|
|
||||||
|
widget.FirstDay = int(calendarWeekdaysToInt[widget.FirstDayOfWeek])
|
||||||
|
widget.cachedHTML = widget.renderTemplate(widget, calendarWidgetTemplate)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (widget *calendarWidget) update(ctx context.Context) {
|
|
||||||
widget.Calendar = newCalendar(time.Now(), widget.StartSunday)
|
|
||||||
widget.withError(nil).scheduleNextUpdate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (widget *calendarWidget) Render() template.HTML {
|
func (widget *calendarWidget) Render() template.HTML {
|
||||||
return widget.renderTemplate(widget, calendarWidgetTemplate)
|
return widget.cachedHTML
|
||||||
}
|
|
||||||
|
|
||||||
type calendar struct {
|
|
||||||
CurrentDay int
|
|
||||||
CurrentWeekNumber int
|
|
||||||
CurrentMonthName string
|
|
||||||
CurrentYear int
|
|
||||||
Days []int
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: very inflexible, refactor to allow more customizability
|
|
||||||
// TODO: allow changing between showing the previous and next week and the entire month
|
|
||||||
func newCalendar(now time.Time, startSunday bool) *calendar {
|
|
||||||
year, week := now.ISOWeek()
|
|
||||||
weekday := now.Weekday()
|
|
||||||
if !startSunday {
|
|
||||||
weekday = (weekday + 6) % 7 // Shift Monday to 0
|
|
||||||
}
|
|
||||||
|
|
||||||
currentMonthDays := daysInMonth(now.Month(), year)
|
|
||||||
|
|
||||||
var previousMonthDays int
|
|
||||||
|
|
||||||
if previousMonthNumber := now.Month() - 1; previousMonthNumber < 1 {
|
|
||||||
previousMonthDays = daysInMonth(12, year-1)
|
|
||||||
} else {
|
|
||||||
previousMonthDays = daysInMonth(previousMonthNumber, year)
|
|
||||||
}
|
|
||||||
|
|
||||||
startDaysFrom := now.Day() - int(weekday) - 7
|
|
||||||
|
|
||||||
days := make([]int, 21)
|
|
||||||
|
|
||||||
for i := 0; i < 21; i++ {
|
|
||||||
day := startDaysFrom + i
|
|
||||||
|
|
||||||
if day < 1 {
|
|
||||||
day = previousMonthDays + day
|
|
||||||
} else if day > currentMonthDays {
|
|
||||||
day = day - currentMonthDays
|
|
||||||
}
|
|
||||||
|
|
||||||
days[i] = day
|
|
||||||
}
|
|
||||||
|
|
||||||
return &calendar{
|
|
||||||
CurrentDay: now.Day(),
|
|
||||||
CurrentWeekNumber: week,
|
|
||||||
CurrentMonthName: now.Month().String(),
|
|
||||||
CurrentYear: year,
|
|
||||||
Days: days,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func daysInMonth(m time.Month, year int) int {
|
|
||||||
return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
|
||||||
}
|
}
|
||||||
|
86
internal/glance/widget-old-calendar.go
Normal file
86
internal/glance/widget-old-calendar.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package glance
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"html/template"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var oldCalendarWidgetTemplate = mustParseTemplate("old-calendar.html", "widget-base.html")
|
||||||
|
|
||||||
|
type oldCalendarWidget struct {
|
||||||
|
widgetBase `yaml:",inline"`
|
||||||
|
Calendar *calendar
|
||||||
|
StartSunday bool `yaml:"start-sunday"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *oldCalendarWidget) initialize() error {
|
||||||
|
widget.withTitle("Calendar").withCacheOnTheHour()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *oldCalendarWidget) update(ctx context.Context) {
|
||||||
|
widget.Calendar = newCalendar(time.Now(), widget.StartSunday)
|
||||||
|
widget.withError(nil).scheduleNextUpdate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (widget *oldCalendarWidget) Render() template.HTML {
|
||||||
|
return widget.renderTemplate(widget, oldCalendarWidgetTemplate)
|
||||||
|
}
|
||||||
|
|
||||||
|
type calendar struct {
|
||||||
|
CurrentDay int
|
||||||
|
CurrentWeekNumber int
|
||||||
|
CurrentMonthName string
|
||||||
|
CurrentYear int
|
||||||
|
Days []int
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: very inflexible, refactor to allow more customizability
|
||||||
|
// TODO: allow changing between showing the previous and next week and the entire month
|
||||||
|
func newCalendar(now time.Time, startSunday bool) *calendar {
|
||||||
|
year, week := now.ISOWeek()
|
||||||
|
weekday := now.Weekday()
|
||||||
|
if !startSunday {
|
||||||
|
weekday = (weekday + 6) % 7 // Shift Monday to 0
|
||||||
|
}
|
||||||
|
|
||||||
|
currentMonthDays := daysInMonth(now.Month(), year)
|
||||||
|
|
||||||
|
var previousMonthDays int
|
||||||
|
|
||||||
|
if previousMonthNumber := now.Month() - 1; previousMonthNumber < 1 {
|
||||||
|
previousMonthDays = daysInMonth(12, year-1)
|
||||||
|
} else {
|
||||||
|
previousMonthDays = daysInMonth(previousMonthNumber, year)
|
||||||
|
}
|
||||||
|
|
||||||
|
startDaysFrom := now.Day() - int(weekday) - 7
|
||||||
|
|
||||||
|
days := make([]int, 21)
|
||||||
|
|
||||||
|
for i := 0; i < 21; i++ {
|
||||||
|
day := startDaysFrom + i
|
||||||
|
|
||||||
|
if day < 1 {
|
||||||
|
day = previousMonthDays + day
|
||||||
|
} else if day > currentMonthDays {
|
||||||
|
day = day - currentMonthDays
|
||||||
|
}
|
||||||
|
|
||||||
|
days[i] = day
|
||||||
|
}
|
||||||
|
|
||||||
|
return &calendar{
|
||||||
|
CurrentDay: now.Day(),
|
||||||
|
CurrentWeekNumber: week,
|
||||||
|
CurrentMonthName: now.Month().String(),
|
||||||
|
CurrentYear: year,
|
||||||
|
Days: days,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func daysInMonth(m time.Month, year int) int {
|
||||||
|
return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day()
|
||||||
|
}
|
@ -23,6 +23,8 @@ func newWidget(widgetType string) (widget, error) {
|
|||||||
switch widgetType {
|
switch widgetType {
|
||||||
case "calendar":
|
case "calendar":
|
||||||
w = &calendarWidget{}
|
w = &calendarWidget{}
|
||||||
|
case "calendar-legacy":
|
||||||
|
w = &oldCalendarWidget{}
|
||||||
case "clock":
|
case "clock":
|
||||||
w = &clockWidget{}
|
w = &clockWidget{}
|
||||||
case "weather":
|
case "weather":
|
||||||
|
Loading…
x
Reference in New Issue
Block a user