mirror of
https://github.com/Xevion/glance.git
synced 2025-12-15 00:11:53 -06:00
Merge branch 'release/v0.7.0' into main
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { setupPopovers } from './popover.js';
|
||||
import { setupMasonries } from './masonry.js';
|
||||
import { throttledDebounce, isElementVisible } from './utils.js';
|
||||
|
||||
async function fetchPageContent(pageData) {
|
||||
@@ -502,9 +503,34 @@ function timeInZone(now, zone) {
|
||||
timeInZone = now
|
||||
}
|
||||
|
||||
const diffInHours = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60 / 60);
|
||||
const diffInMinutes = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60);
|
||||
|
||||
return { time: timeInZone, diffInHours: diffInHours };
|
||||
return { time: timeInZone, diffInMinutes: diffInMinutes };
|
||||
}
|
||||
|
||||
function zoneDiffText(diffInMinutes) {
|
||||
if (diffInMinutes == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const sign = diffInMinutes < 0 ? "-" : "+";
|
||||
const signText = diffInMinutes < 0 ? "behind" : "ahead";
|
||||
|
||||
diffInMinutes = Math.abs(diffInMinutes);
|
||||
|
||||
const hours = Math.floor(diffInMinutes / 60);
|
||||
const minutes = diffInMinutes % 60;
|
||||
const hourSuffix = hours == 1 ? "" : "s";
|
||||
|
||||
if (minutes == 0) {
|
||||
return { text: `${sign}${hours}h`, title: `${hours} hour${hourSuffix} ${signText}` };
|
||||
}
|
||||
|
||||
if (hours == 0) {
|
||||
return { text: `${sign}${minutes}m`, title: `${minutes} minutes ${signText}` };
|
||||
}
|
||||
|
||||
return { text: `${sign}${hours}h~`, title: `${hours} hour${hourSuffix} and ${minutes} minutes ${signText}` };
|
||||
}
|
||||
|
||||
function setupClocks() {
|
||||
@@ -547,9 +573,11 @@ function setupClocks() {
|
||||
);
|
||||
|
||||
updateCallbacks.push((now) => {
|
||||
const { time, diffInHours } = timeInZone(now, timeZoneContainer.dataset.timeInZone);
|
||||
const { time, diffInMinutes } = timeInZone(now, timeZoneContainer.dataset.timeInZone);
|
||||
setZoneTime(time);
|
||||
diffElement.textContent = (diffInHours <= 0 ? diffInHours : '+' + diffInHours) + 'h';
|
||||
const { text, title } = zoneDiffText(diffInMinutes);
|
||||
diffElement.textContent = text;
|
||||
diffElement.title = title;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -581,6 +609,7 @@ async function setupPage() {
|
||||
setupCollapsibleLists();
|
||||
setupCollapsibleGrids();
|
||||
setupGroups();
|
||||
setupMasonries();
|
||||
setupDynamicRelativeTime();
|
||||
setupLazyImages();
|
||||
} finally {
|
||||
|
||||
53
internal/assets/static/js/masonry.js
Normal file
53
internal/assets/static/js/masonry.js
Normal file
@@ -0,0 +1,53 @@
|
||||
|
||||
import { clamp } from "./utils.js";
|
||||
|
||||
export function setupMasonries() {
|
||||
const masonryContainers = document.getElementsByClassName("masonry");
|
||||
|
||||
for (let i = 0; i < masonryContainers.length; i++) {
|
||||
const container = masonryContainers[i];
|
||||
|
||||
const options = {
|
||||
minColumnWidth: container.dataset.minColumnWidth || 330,
|
||||
maxColumns: container.dataset.maxColumns || 6,
|
||||
};
|
||||
|
||||
const items = Array.from(container.children);
|
||||
let previousColumnsCount = 0;
|
||||
|
||||
const render = function() {
|
||||
const columnsCount = clamp(
|
||||
Math.floor(container.offsetWidth / options.minColumnWidth),
|
||||
1,
|
||||
Math.min(options.maxColumns, items.length)
|
||||
);
|
||||
|
||||
if (columnsCount === previousColumnsCount) {
|
||||
return;
|
||||
} else {
|
||||
container.textContent = "";
|
||||
previousColumnsCount = columnsCount;
|
||||
}
|
||||
|
||||
const columnsFragment = document.createDocumentFragment();
|
||||
|
||||
for (let i = 0; i < columnsCount; i++) {
|
||||
const column = document.createElement("div");
|
||||
column.className = "masonry-column";
|
||||
columnsFragment.append(column);
|
||||
}
|
||||
|
||||
// poor man's masonry
|
||||
// TODO: add an option that allows placing items in the
|
||||
// shortest column instead of iterating the columns in order
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
columnsFragment.children[i % columnsCount].appendChild(items[i]);
|
||||
}
|
||||
|
||||
container.append(columnsFragment);
|
||||
};
|
||||
|
||||
const observer = new ResizeObserver(() => requestAnimationFrame(render));
|
||||
observer.observe(container);
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,8 @@ function clearTogglePopoverTimeout() {
|
||||
}
|
||||
|
||||
function showPopover() {
|
||||
if (pendingTarget === null) return;
|
||||
|
||||
activeTarget = pendingTarget;
|
||||
pendingTarget = null;
|
||||
|
||||
@@ -109,9 +111,10 @@ function repositionContainer() {
|
||||
|
||||
const containerBounds = containerElement.getBoundingClientRect();
|
||||
const containerInlinePadding = parseInt(containerComputedStyle.getPropertyValue("padding-inline"));
|
||||
const targetBoundsWidthOffset = targetBounds.width * (activeTarget.dataset.popoverOffset || 0.5);
|
||||
const targetBoundsWidthOffset = targetBounds.width * (activeTarget.dataset.popoverTargetOffset || 0.5);
|
||||
const position = activeTarget.dataset.popoverPosition || "below";
|
||||
const left = Math.round(targetBounds.left + targetBoundsWidthOffset - (containerBounds.width / 2));
|
||||
const popoverOffest = activeTarget.dataset.popoverOffset || 0.5;
|
||||
const left = Math.round(targetBounds.left + targetBoundsWidthOffset - (containerBounds.width * popoverOffest));
|
||||
|
||||
if (left < 0) {
|
||||
containerElement.style.left = 0;
|
||||
@@ -124,7 +127,7 @@ function repositionContainer() {
|
||||
} else {
|
||||
containerElement.style.removeProperty("right");
|
||||
containerElement.style.left = left + "px";
|
||||
containerElement.style.removeProperty("--triangle-offset");
|
||||
containerElement.style.setProperty("--triangle-offset", ((targetBounds.left + targetBoundsWidthOffset) - left - containerInlinePadding) + "px");
|
||||
}
|
||||
|
||||
const distanceFromTarget = activeTarget.dataset.popoverMargin || defaultDistanceFromTarget;
|
||||
|
||||
@@ -23,3 +23,7 @@ export function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||
export function isElementVisible(element) {
|
||||
return !!(element.offsetWidth || element.offsetHeight || element.getClientRects().length);
|
||||
}
|
||||
|
||||
export function clamp(value, min, max) {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
@@ -440,6 +440,17 @@ kbd:active {
|
||||
box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
|
||||
}
|
||||
|
||||
.masonry {
|
||||
display: flex;
|
||||
gap: var(--widget-gap);
|
||||
}
|
||||
|
||||
.masonry-column {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.popover-container, [data-popover-html] {
|
||||
display: none;
|
||||
}
|
||||
@@ -1339,6 +1350,10 @@ details[open] .summary::after {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.clock-time {
|
||||
min-width: 8ch;
|
||||
}
|
||||
|
||||
.clock-time span {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
@@ -1493,6 +1508,14 @@ details[open] .summary::after {
|
||||
border: 2px solid var(--color-widget-background);
|
||||
}
|
||||
|
||||
.twitch-stream-preview {
|
||||
max-width: 100%;
|
||||
width: 400px;
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: var(--border-radius);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.reddit-card-thumbnail {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
@@ -39,9 +39,11 @@ var (
|
||||
ExtensionTemplate = compileTemplate("extension.html", "widget-base.html")
|
||||
GroupTemplate = compileTemplate("group.html", "widget-base.html")
|
||||
DNSStatsTemplate = compileTemplate("dns-stats.html", "widget-base.html")
|
||||
SplitColumnTemplate = compileTemplate("split-column.html", "widget-base.html")
|
||||
CustomAPITemplate = compileTemplate("custom-api.html", "widget-base.html")
|
||||
)
|
||||
|
||||
var globalTemplateFunctions = template.FuncMap{
|
||||
var GlobalTemplateFunctions = template.FuncMap{
|
||||
"relativeTime": relativeTimeSince,
|
||||
"formatViewerCount": formatViewerCount,
|
||||
"formatNumber": intl.Sprint,
|
||||
@@ -58,7 +60,7 @@ var globalTemplateFunctions = template.FuncMap{
|
||||
|
||||
func compileTemplate(primary string, dependencies ...string) *template.Template {
|
||||
t, err := template.New(primary).
|
||||
Funcs(globalTemplateFunctions).
|
||||
Funcs(GlobalTemplateFunctions).
|
||||
ParseFS(TemplateFS, append([]string{primary}, dependencies...)...)
|
||||
|
||||
if err != nil {
|
||||
|
||||
7
internal/assets/templates/custom-api.html
Normal file
7
internal/assets/templates/custom-api.html
Normal file
@@ -0,0 +1,7 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}{{ if .Frameless }}widget-content-frameless{{ end }}{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ .CompiledHTML }}
|
||||
{{ end }}
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="flex items-center gap-15">
|
||||
<div class="min-width-0">
|
||||
<a{{ if ne "" .SymbolLink }} href="{{ .SymbolLink }}" target="_blank" rel="noreferrer"{{ end }} class="color-highlight size-h3 block text-truncate">{{ .Symbol }}</a>
|
||||
<div class="text-truncate">{{ .Name }}</div>
|
||||
<div title="{{ .Name }}" class="text-truncate">{{ .Name }}</div>
|
||||
</div>
|
||||
|
||||
<a class="market-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
<div class="mobile-navigation-icons">
|
||||
<a class="mobile-navigation-label" href="#top">↑</a>
|
||||
{{ range $i, $column := .Page.Columns }}
|
||||
<label class="mobile-navigation-label"><input type="radio" class="mobile-navigation-input" name="column" value="{{ $i }}" autocomplete="off"{{ if eq "full" $column.Size }} checked{{ end }}><div class="mobile-navigation-pill"></div></label>
|
||||
<label class="mobile-navigation-label"><input type="radio" class="mobile-navigation-input" name="column" value="{{ $i }}" autocomplete="off"{{ if eq $i $.Page.PrimaryColumnIndex }} checked{{ end }}><div class="mobile-navigation-pill"></div></label>
|
||||
{{ end }}
|
||||
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"><div class="hamburger-icon"></div></label>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-10 collapsible-container single-line-titles" data-collapse-after="{{ .CollapseAfter }}">
|
||||
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Releases }}
|
||||
<li>
|
||||
<div class="flex items-center gap-10">
|
||||
|
||||
11
internal/assets/templates/split-column.html
Normal file
11
internal/assets/templates/split-column.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="masonry" data-max-columns="{{ .MaxColumns }}">
|
||||
{{ range .Widgets }}
|
||||
{{ .Render }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -5,9 +5,15 @@
|
||||
{{ range .Channels }}
|
||||
<li>
|
||||
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-parent">
|
||||
<div class="twitch-channel-avatar-container">
|
||||
<div class="twitch-channel-avatar-container"{{ if .IsLive }} data-popover-type="html" data-popover-position="above" data-popover-margin="0.15rem" data-popover-offset="0.2"{{ end }}>
|
||||
{{ if .IsLive }}
|
||||
<div data-popover-html>
|
||||
<img class="twitch-stream-preview" src="https://static-cdn.jtvnw.net/previews-ttv/live_user_{{ .Login }}-440x248.jpg" loading="lazy" alt="">
|
||||
<p class="margin-top-10 color-highlight text-truncate-3-lines">{{ .StreamTitle }}</p>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ if .Exists }}
|
||||
<a href="https://twitch.tv/{{ .Login }}" class="twitch-channel-avatar-link" target="_blank" rel="noreferrer">
|
||||
<a href="https://twitch.tv/{{ .Login }}" target="_blank" rel="noreferrer">
|
||||
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
|
||||
</a>
|
||||
{{ else }}
|
||||
|
||||
Reference in New Issue
Block a user