mirror of
https://github.com/Xevion/glance.git
synced 2025-12-09 16:07:20 -06:00
Merge branch 'main' into lobsters-widget
This commit is contained in:
@@ -102,12 +102,13 @@
|
||||
.list { --list-half-gap: 0rem; }
|
||||
.list-gap-2 { --list-half-gap: 0.1rem; }
|
||||
.list-gap-4 { --list-half-gap: 0.2rem; }
|
||||
.list-gap-10 { --list-half-gap: 0.5rem; }
|
||||
.list-gap-14 { --list-half-gap: 0.7rem; }
|
||||
.list-gap-20 { --list-half-gap: 1rem; }
|
||||
.list-gap-24 { --list-half-gap: 1.2rem; }
|
||||
|
||||
.list > *:not(:first-child) {
|
||||
margin-top: calc(var(--list-half-gap) * 2 + 1px);
|
||||
margin-top: calc(var(--list-half-gap) * 2);
|
||||
}
|
||||
|
||||
.list-with-separator > *:not(:first-child) {
|
||||
@@ -205,11 +206,29 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-color: var(--color-separator);
|
||||
}
|
||||
|
||||
img, svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
img[loading=lazy].loaded:not(.finished-transition) {
|
||||
transition: opacity .4s;
|
||||
}
|
||||
|
||||
img[loading=lazy].cached:not(.finished-transition) {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
img[loading=lazy]:not(.loaded, .cached) {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--color-text-subdue) transparent;
|
||||
scroll-behavior: smooth;
|
||||
@@ -314,6 +333,44 @@ body {
|
||||
padding: 0 var(--content-bounds-padding);
|
||||
}
|
||||
|
||||
.dynamic-columns {
|
||||
gap: calc(var(--widget-content-vertical-padding) / 2);
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--columns-per-row), 1fr);
|
||||
margin: calc(0px - var(--widget-content-vertical-padding) / 2) calc(0px - var(--widget-content-horizontal-padding) / 2);
|
||||
}
|
||||
|
||||
.dynamic-columns > * {
|
||||
padding: calc(var(--widget-content-vertical-padding) / 2) calc(var(--widget-content-horizontal-padding) / 1.5);
|
||||
background-color: var(--color-background);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
|
||||
.dynamic-columns:has(> :nth-child(5)) { --columns-per-row: 5; }
|
||||
|
||||
@container widget (max-width: 1500px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
.dynamic-columns:has(> :nth-child(4)) { --columns-per-row: 4; }
|
||||
}
|
||||
@container widget (max-width: 1250px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
.dynamic-columns:has(> :nth-child(3)) { --columns-per-row: 3; }
|
||||
}
|
||||
@container widget (max-width: 850px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
.dynamic-columns:has(> :nth-child(2)) { --columns-per-row: 2; }
|
||||
}
|
||||
@container widget (max-width: 550px) {
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
}
|
||||
|
||||
.cards-vertical {
|
||||
flex-direction: column;
|
||||
}
|
||||
@@ -322,30 +379,44 @@ body {
|
||||
--cards-per-row: 6.5;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
--cards-per-row: 6;
|
||||
}
|
||||
|
||||
.cards-horizontal, .cards-vertical, .cards-grid {
|
||||
.cards-horizontal, .cards-vertical {
|
||||
--cards-gap: calc(var(--widget-content-vertical-padding) * 0.7);
|
||||
display: flex;
|
||||
gap: var(--cards-gap);
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.cards-horizontal .card {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
|
||||
}
|
||||
|
||||
.cards-grid .card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.cards-horizontal {
|
||||
overflow-x: auto;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
flex-wrap: wrap;
|
||||
--cards-per-row: 6;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--cards-per-row), 1fr);
|
||||
gap: calc(var(--widget-content-vertical-padding) * 0.7);
|
||||
}
|
||||
|
||||
@container widget (max-width: 1300px) { .cards-horizontal { --cards-per-row: 5.5; } }
|
||||
@container widget (max-width: 1100px) { .cards-horizontal { --cards-per-row: 4.5; } }
|
||||
@container widget (max-width: 850px) { .cards-horizontal { --cards-per-row: 3.5; } }
|
||||
@container widget (max-width: 750px) { .cards-horizontal { --cards-per-row: 3.5; } }
|
||||
@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.2; } }
|
||||
@container widget (max-width: 650px) { .cards-horizontal { --cards-per-row: 2.5; } }
|
||||
@container widget (max-width: 450px) { .cards-horizontal { --cards-per-row: 2.3; } }
|
||||
|
||||
@container widget (max-width: 1300px) { .cards-grid { --cards-per-row: 5; } }
|
||||
@container widget (max-width: 1100px) { .cards-grid { --cards-per-row: 4; } }
|
||||
@@ -353,12 +424,7 @@ body {
|
||||
@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
|
||||
@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
|
||||
|
||||
.card {
|
||||
flex-shrink: 0;
|
||||
width: calc(100% / var(--cards-per-row) - var(--cards-gap) * (var(--cards-per-row) - 1) / var(--cards-per-row));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.widget-error-header {
|
||||
display: flex;
|
||||
@@ -490,7 +556,7 @@ body {
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
|
||||
.mobile-navigation {
|
||||
.mobile-navigation, .mobile-reachability-header {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -517,6 +583,10 @@ body {
|
||||
width: 6.5rem;
|
||||
}
|
||||
|
||||
.stock-chart svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stock-values {
|
||||
min-width: 8rem;
|
||||
}
|
||||
@@ -553,7 +623,7 @@ body {
|
||||
|
||||
.video-thumbnail {
|
||||
width: 100%;
|
||||
aspect-ratio: 16 / 9;
|
||||
aspect-ratio: 16 / 8.9;
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
@@ -788,6 +858,7 @@ body {
|
||||
}
|
||||
|
||||
.monitor-site-status-icon {
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
@@ -805,11 +876,48 @@ body {
|
||||
}
|
||||
|
||||
.rss-card-image {
|
||||
height: 10rem;
|
||||
height: var(--rss-thumbnail-height, 10rem);
|
||||
object-fit: cover;
|
||||
border-radius: var(--border-radius) var(--border-radius) 0 0;
|
||||
}
|
||||
|
||||
.rss-card-2 {
|
||||
position: relative;
|
||||
height: var(--rss-card-height, 27rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.rss-card-2::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
background-image: linear-gradient(
|
||||
0deg,
|
||||
var(--color-widget-background),
|
||||
hsla(var(--color-widget-background-hsl-values), 0.8) 6rem, transparent 14rem
|
||||
);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.rss-card-2-image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
/* +1px is required to fix some weird graphical bug where the image overflows on the bottom in firefox */
|
||||
border-radius: calc(var(--border-radius) + 1px);
|
||||
opacity: 0.9;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rss-card-2-content {
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
bottom: var(--widget-content-vertical-padding);
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.twitch-category-thumbnail {
|
||||
width: 5rem;
|
||||
border-radius: var(--border-radius);
|
||||
@@ -1013,6 +1121,8 @@ body {
|
||||
--content-bounds-padding: 10px;
|
||||
}
|
||||
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
|
||||
.forum-post-list-item {
|
||||
flex-flow: row-reverse;
|
||||
}
|
||||
@@ -1020,6 +1130,15 @@ body {
|
||||
.hide-on-mobile {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-reachability-header {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
padding: 10dvh 1rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-highlight);
|
||||
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
}
|
||||
|
||||
.size-h1 { font-size: var(--font-size-h1); }
|
||||
@@ -1041,6 +1160,8 @@ body {
|
||||
.text-left { text-align: left; }
|
||||
.text-right { text-align: right; }
|
||||
.text-center { text-align: center; }
|
||||
.text-elevate { margin-top: -0.2em; }
|
||||
.text-compact { word-spacing: -0.18em; }
|
||||
.rtl { direction: rtl; }
|
||||
.shrink { flex-shrink: 1; }
|
||||
.shrink-0 { flex-shrink: 0; }
|
||||
@@ -1069,6 +1190,11 @@ body {
|
||||
.margin-top-7 { margin-top: 0.7rem; }
|
||||
.margin-top-10 { margin-top: 1rem; }
|
||||
.margin-top-15 { margin-top: 1.5rem; }
|
||||
.margin-block-3 { margin-block: 0.3rem; }
|
||||
.margin-block-5 { margin-block: 0.5rem; }
|
||||
.margin-block-7 { margin-block: 0.7rem; }
|
||||
.margin-block-10 { margin-block: 1rem; }
|
||||
.margin-block-15 { margin-block: 1.5rem; }
|
||||
.margin-bottom-3 { margin-bottom: 0.3rem; }
|
||||
.margin-bottom-5 { margin-bottom: 0.5rem; }
|
||||
.margin-bottom-7 { margin-bottom: 0.7rem; }
|
||||
|
||||
@@ -142,6 +142,33 @@ function setupDynamicRelativeTime() {
|
||||
});
|
||||
}
|
||||
|
||||
function setupLazyImages() {
|
||||
const images = document.querySelectorAll("img[loading=lazy]");
|
||||
|
||||
if (images.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
function imageFinishedTransition(image) {
|
||||
image.classList.add("finished-transition");
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
|
||||
if (image.complete) {
|
||||
image.classList.add("cached");
|
||||
setTimeout(() => imageFinishedTransition(image), 5);
|
||||
} else {
|
||||
// TODO: also handle error event
|
||||
image.addEventListener("load", () => {
|
||||
image.classList.add("loaded");
|
||||
setTimeout(() => imageFinishedTransition(image), 500);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function setupPage() {
|
||||
const pageElement = document.getElementById("page");
|
||||
const pageContents = await fetchPageContents(pageData.slug);
|
||||
@@ -152,6 +179,7 @@ async function setupPage() {
|
||||
document.body.classList.add("animate-element-transition");
|
||||
}, 150);
|
||||
|
||||
setTimeout(setupLazyImages, 5);
|
||||
setupCarousels();
|
||||
setupDynamicRelativeTime();
|
||||
}
|
||||
|
||||
@@ -22,13 +22,16 @@ var (
|
||||
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
|
||||
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
|
||||
ReleasesTemplate = compileTemplate("releases.html", "widget-base.html")
|
||||
VideosTemplate = compileTemplate("videos.html", "widget-base.html")
|
||||
VideosTemplate = compileTemplate("videos.html", "widget-base.html", "video-card-contents.html")
|
||||
VideosGridTemplate = compileTemplate("videos-grid.html", "widget-base.html", "video-card-contents.html")
|
||||
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
|
||||
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
|
||||
RSSCardsTemplate = compileTemplate("rss-cards.html", "widget-base.html")
|
||||
RSSHorizontalCardsTemplate = compileTemplate("rss-horizontal-cards.html", "widget-base.html")
|
||||
RSSHorizontalCards2Template = compileTemplate("rss-horizontal-cards-2.html", "widget-base.html")
|
||||
MonitorTemplate = compileTemplate("monitor.html", "widget-base.html")
|
||||
TwitchGamesListTemplate = compileTemplate("twitch-games-list.html", "widget-base.html")
|
||||
TwitchChannelsTemplate = compileTemplate("twitch-channels.html", "widget-base.html")
|
||||
RepositoryTemplate = compileTemplate("repository.html", "widget-base.html")
|
||||
)
|
||||
|
||||
var globalTemplateFunctions = template.FuncMap{
|
||||
|
||||
@@ -1,23 +1,37 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-24 list-with-separator">
|
||||
{{ range .Groups }}
|
||||
<li class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
|
||||
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .Links }}
|
||||
<li class="flex items-center gap-10">
|
||||
{{ if ne "" .Icon }}
|
||||
<div class="bookmarks-icon-container">
|
||||
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ template "group" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="dynamic-columns">
|
||||
{{ range .Groups }}
|
||||
<div class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
|
||||
{{ template "group" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "group" }}
|
||||
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .Links }}
|
||||
<li class="flex items-center gap-10">
|
||||
{{ if ne "" .Icon }}
|
||||
<div class="bookmarks-icon-container">
|
||||
<img class="bookmarks-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" src="{{ .Icon }}" alt="" loading="lazy">
|
||||
</div>
|
||||
{{ end }}
|
||||
<a href="{{ .URL }}" class="bookmarks-link {{ if .HideArrow }}bookmarks-link-no-arrow {{ end }}color-highlight size-h4" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
{{ if .Page.ShowMobileHeader }}
|
||||
<div class="mobile-reachability-header">{{ .Page.Title }}</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="page-columns">
|
||||
{{ range .Page.Columns }}
|
||||
<div class="page-column page-column-{{ .Size }}">
|
||||
|
||||
@@ -1,39 +1,53 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-20 list-with-separator">
|
||||
{{ range .Sites }}
|
||||
<li class="monitor-site flex items-center gap-15">
|
||||
{{ if .IconUrl }}
|
||||
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
|
||||
{{ end }}
|
||||
<div>
|
||||
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
{{ if not .Status.Error }}
|
||||
<li>{{ .StatusText }}</li>
|
||||
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
|
||||
{{ else if .Status.TimedOut }}
|
||||
<li class="color-negative">Timed Out</li>
|
||||
{{ else }}
|
||||
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .StatusStyle "good" }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
|
||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
|
||||
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ template "site" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<ul class="dynamic-columns">
|
||||
{{ range .Sites }}
|
||||
<div class="flex items-center gap-15">
|
||||
{{ template "site" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "site" }}
|
||||
{{ if .IconUrl }}
|
||||
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
|
||||
{{ end }}
|
||||
<div>
|
||||
<a class="size-h3 color-highlight" href="{{ .Url }}" {{ if not .SameTab }}target="_blank"{{ end }} rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
{{ if not .Status.Error }}
|
||||
<li>{{ .StatusText }}</li>
|
||||
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
|
||||
{{ else if .Status.TimedOut }}
|
||||
<li class="color-negative">Timed Out</li>
|
||||
{{ else }}
|
||||
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .StatusStyle "good" }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
|
||||
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="monitor-site-status-icon">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
|
||||
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
<ul class="list list-gap-10 list-collapsible">
|
||||
{{ range $i, $release := .Releases }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
||||
|
||||
44
internal/assets/templates/repository.html
Normal file
44
internal/assets/templates/repository.html
Normal file
@@ -0,0 +1,44 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<a class="size-h4 color-highlight" href="https://github.com/{{ $.RepositoryDetails.Name }}" target="_blank" rel="noreferrer">{{ .RepositoryDetails.Name }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li>{{ .RepositoryDetails.Stars | formatNumber }} stars</li>
|
||||
<li>{{ .RepositoryDetails.Forks | formatNumber }} forks</li>
|
||||
</ul>
|
||||
|
||||
{{ if gt (len .RepositoryDetails.PullRequests) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/pulls" target="_blank" rel="noreferrer">Open pull requests ({{ .RepositoryDetails.OpenPullRequests | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.PullRequests }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/pull/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .RepositoryDetails.Issues) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<a class="text-compact" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues" target="_blank" rel="noreferrer">Open issues ({{ .RepositoryDetails.OpenIssues | formatNumber }} total)</a>
|
||||
<div class="flex gap-7 size-h5 margin-top-3">
|
||||
<ul class="list list-gap-2">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
{{ range .RepositoryDetails.Issues }}
|
||||
<li><a class="color-primary-if-not-visited text-truncate block" title="{{ .Title }}" target="_blank" rel="noreferrer" href="https://github.com/{{ $.RepositoryDetails.Name }}/issues/{{ .Number }}">{{ .Title }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
28
internal/assets/templates/rss-horizontal-cards-2.html
Normal file
28
internal/assets/templates/rss-horizontal-cards-2.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .CardHeight }} style="--rss-card-height: {{ .CardHeight }}rem;"{{ end }}>
|
||||
{{ range .Items }}
|
||||
<div class="card rss-card-2 widget-content-frame thumbnail-container">
|
||||
{{ if ne "" .ImageURL }}
|
||||
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
|
||||
{{ else }}
|
||||
<svg class="rss-card-2-image" style="transform: scale(0.35) translateY(-25%)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="var(--color-text-subdue)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z" />
|
||||
</svg>
|
||||
{{ end }}
|
||||
<div class="rss-card-2-content padding-inline-widget">
|
||||
<a href="{{ .Link }}" title="{{ .Title }}" class="block text-truncate color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-5">
|
||||
<li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
|
||||
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="cards-horizontal carousel-items-container">
|
||||
<div class="cards-horizontal carousel-items-container"{{ if ne 0.0 .ThumbnailHeight }} style="--rss-thumbnail-height: {{ .ThumbnailHeight }}rem;"{{ end }}>
|
||||
{{ range .Items }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
{{ if ne "" .ImageURL }}
|
||||
@@ -1,23 +1,39 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-20 list-with-separator">
|
||||
{{ range .Stocks }}
|
||||
<li class="flex items-center gap-15">
|
||||
<div class="shrink min-width-0">
|
||||
<div class="color-highlight size-h3 text-truncate">{{ .Symbol }}</div>
|
||||
<div class="text-truncate">{{ .Name }}</div>
|
||||
</div>
|
||||
|
||||
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
|
||||
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
|
||||
</svg>
|
||||
|
||||
<div class="stock-values shrink-0">
|
||||
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
|
||||
</div>
|
||||
{{ template "stock" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="dynamic-columns">
|
||||
{{ range .Stocks }}
|
||||
<div class="flex items-center gap-15">
|
||||
{{ template "stock" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "stock" }}
|
||||
<div class="shrink 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>
|
||||
|
||||
<a class="stock-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
|
||||
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
|
||||
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
<div class="stock-values shrink-0">
|
||||
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
|
||||
<div class="text-right">{{ .Currency }}{{ .Price | formatPrice }}</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
12
internal/assets/templates/video-card-contents.html
Normal file
12
internal/assets/templates/video-card-contents.html
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ define "video-card-contents" }}
|
||||
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
|
||||
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
|
||||
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-7">
|
||||
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li class="shrink min-width-0">
|
||||
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
13
internal/assets/templates/videos-grid.html
Normal file
13
internal/assets/templates/videos-grid.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="cards-grid">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -4,19 +4,10 @@
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="carousel-container">
|
||||
<div class="videos cards-horizontal carousel-items-container">
|
||||
<div class="cards-horizontal carousel-items-container">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
|
||||
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget">
|
||||
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-7">
|
||||
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li class="shrink min-width-0">
|
||||
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -115,3 +116,133 @@ func FetchLatestReleasesFromGithub(repositories []string, token string) (AppRele
|
||||
|
||||
return appReleases, nil
|
||||
}
|
||||
|
||||
type GithubTicket struct {
|
||||
Number int
|
||||
CreatedAt time.Time
|
||||
Title string
|
||||
}
|
||||
|
||||
type RepositoryDetails struct {
|
||||
Name string
|
||||
Stars int
|
||||
Forks int
|
||||
OpenPullRequests int
|
||||
PullRequests []GithubTicket
|
||||
OpenIssues int
|
||||
Issues []GithubTicket
|
||||
}
|
||||
|
||||
type githubRepositoryDetailsResponseJson struct {
|
||||
Name string `json:"full_name"`
|
||||
Stars int `json:"stargazers_count"`
|
||||
Forks int `json:"forks_count"`
|
||||
}
|
||||
|
||||
type githubTicketResponseJson struct {
|
||||
Count int `json:"total_count"`
|
||||
Tickets []struct {
|
||||
Number int `json:"number"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
} `json:"items"`
|
||||
}
|
||||
|
||||
func FetchRepositoryDetailsFromGithub(repository string, token string, maxPRs int, maxIssues int) (RepositoryDetails, error) {
|
||||
repositoryRequest, err := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s", repository), nil)
|
||||
|
||||
if err != nil {
|
||||
return RepositoryDetails{}, fmt.Errorf("%w: could not create request with repository: %v", ErrNoContent, err)
|
||||
}
|
||||
|
||||
PRsRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:pr+is:open+repo:%s&per_page=%d", repository, maxPRs), nil)
|
||||
issuesRequest, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/search/issues?q=is:issue+is:open+repo:%s&per_page=%d", repository, maxIssues), nil)
|
||||
|
||||
if token != "" {
|
||||
token = fmt.Sprintf("Bearer %s", token)
|
||||
repositoryRequest.Header.Add("Authorization", token)
|
||||
PRsRequest.Header.Add("Authorization", token)
|
||||
issuesRequest.Header.Add("Authorization", token)
|
||||
}
|
||||
|
||||
var detailsResponse githubRepositoryDetailsResponseJson
|
||||
var detailsErr error
|
||||
var PRsResponse githubTicketResponseJson
|
||||
var PRsErr error
|
||||
var issuesResponse githubTicketResponseJson
|
||||
var issuesErr error
|
||||
var wg sync.WaitGroup
|
||||
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
detailsResponse, detailsErr = decodeJsonFromRequest[githubRepositoryDetailsResponseJson](defaultClient, repositoryRequest)
|
||||
})()
|
||||
|
||||
if maxPRs > 0 {
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
PRsResponse, PRsErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, PRsRequest)
|
||||
})()
|
||||
}
|
||||
|
||||
if maxIssues > 0 {
|
||||
wg.Add(1)
|
||||
go (func() {
|
||||
defer wg.Done()
|
||||
issuesResponse, issuesErr = decodeJsonFromRequest[githubTicketResponseJson](defaultClient, issuesRequest)
|
||||
})()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
if detailsErr != nil {
|
||||
return RepositoryDetails{}, fmt.Errorf("%w: could not get repository details: %s", ErrNoContent, detailsErr)
|
||||
}
|
||||
|
||||
details := RepositoryDetails{
|
||||
Name: detailsResponse.Name,
|
||||
Stars: detailsResponse.Stars,
|
||||
Forks: detailsResponse.Forks,
|
||||
PullRequests: make([]GithubTicket, 0, len(PRsResponse.Tickets)),
|
||||
Issues: make([]GithubTicket, 0, len(issuesResponse.Tickets)),
|
||||
}
|
||||
|
||||
err = nil
|
||||
|
||||
if maxPRs > 0 {
|
||||
if PRsErr != nil {
|
||||
err = fmt.Errorf("%w: could not get PRs: %s", ErrPartialContent, PRsErr)
|
||||
} else {
|
||||
details.OpenPullRequests = PRsResponse.Count
|
||||
|
||||
for i := range PRsResponse.Tickets {
|
||||
details.PullRequests = append(details.PullRequests, GithubTicket{
|
||||
Number: PRsResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(PRsResponse.Tickets[i].CreatedAt),
|
||||
Title: PRsResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if maxIssues > 0 {
|
||||
if issuesErr != nil {
|
||||
// TODO: fix, overwriting the previous error
|
||||
err = fmt.Errorf("%w: could not get issues: %s", ErrPartialContent, issuesErr)
|
||||
} else {
|
||||
details.OpenIssues = issuesResponse.Count
|
||||
|
||||
for i := range issuesResponse.Tickets {
|
||||
details.Issues = append(details.Issues, GithubTicket{
|
||||
Number: issuesResponse.Tickets[i].Number,
|
||||
CreatedAt: parseGithubTime(issuesResponse.Tickets[i].CreatedAt),
|
||||
Title: issuesResponse.Tickets[i].Title,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return details, err
|
||||
}
|
||||
|
||||
@@ -18,8 +18,8 @@ type hackerNewsPostResponseJson struct {
|
||||
TimePosted int64 `json:"time"`
|
||||
}
|
||||
|
||||
func getHackerNewsTopPostIds() ([]int, error) {
|
||||
request, _ := http.NewRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", nil)
|
||||
func getHackerNewsPostIds(sort string) ([]int, error) {
|
||||
request, _ := http.NewRequest("GET", fmt.Sprintf("https://hacker-news.firebaseio.com/v0/%sstories.json", sort), nil)
|
||||
response, err := decodeJsonFromRequest[[]int](defaultClient, request)
|
||||
|
||||
if err != nil {
|
||||
@@ -83,8 +83,8 @@ func getHackerNewsPostsFromIds(postIds []int, commentsUrlTemplate string) (Forum
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
func FetchHackerNewsTopPosts(limit int, commentsUrlTemplate string) (ForumPosts, error) {
|
||||
postIds, err := getHackerNewsTopPostIds()
|
||||
func FetchHackerNewsPosts(sort string, limit int, commentsUrlTemplate string) (ForumPosts, error) {
|
||||
postIds, err := getHackerNewsPostIds(sort)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -86,12 +86,14 @@ var currencyToSymbol = map[string]string{
|
||||
}
|
||||
|
||||
type Stock struct {
|
||||
Name string
|
||||
Symbol string
|
||||
Currency string
|
||||
Price float64
|
||||
PercentChange float64
|
||||
SvgChartPoints string
|
||||
Name string `yaml:"name"`
|
||||
Symbol string `yaml:"symbol"`
|
||||
ChartLink string `yaml:"chart-link"`
|
||||
SymbolLink string `yaml:"symbol-link"`
|
||||
Currency string `yaml:"-"`
|
||||
Price float64 `yaml:"-"`
|
||||
PercentChange float64 `yaml:"-"`
|
||||
SvgChartPoints string `yaml:"-"`
|
||||
}
|
||||
|
||||
type Stocks []Stock
|
||||
|
||||
@@ -30,12 +30,29 @@ type subredditResponseJson struct {
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUrlTemplate string) (ForumPosts, error) {
|
||||
subreddit = url.QueryEscape(subreddit)
|
||||
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", subreddit)
|
||||
func FetchSubredditPosts(subreddit, sort, topPeriod, search, commentsUrlTemplate, requestUrlTemplate string) (ForumPosts, error) {
|
||||
query := url.Values{}
|
||||
var requestUrl string
|
||||
|
||||
if search != "" {
|
||||
query.Set("q", search+" subreddit:"+subreddit)
|
||||
query.Set("sort", sort)
|
||||
}
|
||||
|
||||
if sort == "top" {
|
||||
query.Set("t", topPeriod)
|
||||
}
|
||||
|
||||
if search != "" {
|
||||
requestUrl = fmt.Sprintf("https://www.reddit.com/search.json?%s", query.Encode())
|
||||
} else {
|
||||
requestUrl = fmt.Sprintf("https://www.reddit.com/r/%s/%s.json?%s", subreddit, sort, query.Encode())
|
||||
}
|
||||
|
||||
if requestUrlTemplate != "" {
|
||||
requestUrl = strings.ReplaceAll(requestUrlTemplate, "{REQUEST-URL}", requestUrl)
|
||||
}
|
||||
|
||||
request, err := http.NewRequest("GET", requestUrl, nil)
|
||||
|
||||
if err != nil {
|
||||
@@ -93,7 +110,5 @@ func FetchSubredditPosts(subreddit string, commentsUrlTemplate string, requestUr
|
||||
posts = append(posts, forumPost)
|
||||
}
|
||||
|
||||
posts.CalculateEngagement()
|
||||
|
||||
return posts, nil
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func extractDomainFromUrl(u string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.TrimPrefix(parsed.Host, "www.")
|
||||
return strings.TrimPrefix(strings.ToLower(parsed.Host), "www.")
|
||||
}
|
||||
|
||||
func SvgPolylineCoordsFromYValues(width float64, height float64, values []float64) string {
|
||||
|
||||
@@ -24,15 +24,10 @@ type stockResponseJson struct {
|
||||
} `json:"chart"`
|
||||
}
|
||||
|
||||
type StockRequest struct {
|
||||
Symbol string
|
||||
Name string
|
||||
}
|
||||
|
||||
// TODO: allow changing chart time frame
|
||||
const stockChartDays = 21
|
||||
|
||||
func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
|
||||
func FetchStocksDataFromYahoo(stockRequests Stocks) (Stocks, error) {
|
||||
requests := make([]*http.Request, 0, len(stockRequests))
|
||||
|
||||
for i := range stockRequests {
|
||||
@@ -86,10 +81,12 @@ func FetchStocksDataFromYahoo(stockRequests []StockRequest) (Stocks, error) {
|
||||
}
|
||||
|
||||
stocks = append(stocks, Stock{
|
||||
Name: stockRequests[i].Name,
|
||||
Symbol: response.Chart.Result[0].Meta.Symbol,
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
Name: stockRequests[i].Name,
|
||||
Symbol: response.Chart.Result[0].Meta.Symbol,
|
||||
SymbolLink: stockRequests[i].SymbolLink,
|
||||
ChartLink: stockRequests[i].ChartLink,
|
||||
Price: response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
Currency: currency,
|
||||
PercentChange: percentChange(
|
||||
response.Chart.Result[0].Meta.RegularMarketPrice,
|
||||
previous,
|
||||
|
||||
@@ -55,10 +55,11 @@ type templateData struct {
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
Title string `yaml:"name"`
|
||||
Slug string `yaml:"slug"`
|
||||
Columns []Column `yaml:"columns"`
|
||||
mu sync.Mutex
|
||||
Title string `yaml:"name"`
|
||||
Slug string `yaml:"slug"`
|
||||
ShowMobileHeader bool `yaml:"show-mobile-header"`
|
||||
Columns []Column `yaml:"columns"`
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (p *Page) UpdateOutdatedWidgets() {
|
||||
|
||||
@@ -22,6 +22,7 @@ type Bookmarks struct {
|
||||
HideArrow bool `yaml:"hide-arrow"`
|
||||
} `yaml:"links"`
|
||||
} `yaml:"groups"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Bookmarks) Initialize() error {
|
||||
|
||||
@@ -13,6 +13,8 @@ type HackerNews struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Posts feed.ForumPosts `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
SortBy string `yaml:"sort-by"`
|
||||
ExtraSortBy string `yaml:"extra-sort-by"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
CommentsUrlTemplate string `yaml:"comments-url-template"`
|
||||
ShowThumbnails bool `yaml:"-"`
|
||||
@@ -29,18 +31,24 @@ func (widget *HackerNews) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if widget.SortBy != "top" && widget.SortBy != "new" && widget.SortBy != "best" {
|
||||
widget.SortBy = "top"
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *HackerNews) Update(ctx context.Context) {
|
||||
posts, err := feed.FetchHackerNewsTopPosts(40, widget.CommentsUrlTemplate)
|
||||
posts, err := feed.FetchHackerNewsPosts(widget.SortBy, 40, widget.CommentsUrlTemplate)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
if widget.ExtraSortBy == "engagement" {
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
}
|
||||
|
||||
if widget.Limit < len(posts) {
|
||||
posts = posts[:widget.Limit]
|
||||
|
||||
@@ -46,14 +46,15 @@ func statusCodeToStyle(status int) string {
|
||||
type Monitor struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Sites []struct {
|
||||
Title string `yaml:"title"`
|
||||
Url string `yaml:"url"`
|
||||
IconUrl string `yaml:"icon"`
|
||||
SameTab bool `yaml:"same-tab"`
|
||||
Status *feed.SiteStatus `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
StatusStyle string `yaml:"-"`
|
||||
Title string `yaml:"title"`
|
||||
Url OptionalEnvString `yaml:"url"`
|
||||
IconUrl string `yaml:"icon"`
|
||||
SameTab bool `yaml:"same-tab"`
|
||||
Status *feed.SiteStatus `yaml:"-"`
|
||||
StatusText string `yaml:"-"`
|
||||
StatusStyle string `yaml:"-"`
|
||||
} `yaml:"sites"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Monitor) Initialize() error {
|
||||
@@ -66,7 +67,7 @@ func (widget *Monitor) Update(ctx context.Context) {
|
||||
requests := make([]*http.Request, len(widget.Sites))
|
||||
|
||||
for i := range widget.Sites {
|
||||
request, err := http.NewRequest("GET", widget.Sites[i].Url, nil)
|
||||
request, err := http.NewRequest("GET", string(widget.Sites[i].Url), nil)
|
||||
|
||||
if err != nil {
|
||||
message := fmt.Errorf("failed to create http request for %s: %s", widget.Sites[i].Url, err)
|
||||
|
||||
@@ -17,6 +17,10 @@ type Reddit struct {
|
||||
Subreddit string `yaml:"subreddit"`
|
||||
Style string `yaml:"style"`
|
||||
ShowThumbnails bool `yaml:"show-thumbnails"`
|
||||
SortBy string `yaml:"sort-by"`
|
||||
TopPeriod string `yaml:"top-period"`
|
||||
Search string `yaml:"search"`
|
||||
ExtraSortBy string `yaml:"extra-sort-by"`
|
||||
CommentsUrlTemplate string `yaml:"comments-url-template"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
@@ -36,6 +40,14 @@ func (widget *Reddit) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if !isValidRedditSortType(widget.SortBy) {
|
||||
widget.SortBy = "hot"
|
||||
}
|
||||
|
||||
if !isValidRedditTopPeriod(widget.TopPeriod) {
|
||||
widget.TopPeriod = "day"
|
||||
}
|
||||
|
||||
if widget.RequestUrlTemplate != "" {
|
||||
if !strings.Contains(widget.RequestUrlTemplate, "{REQUEST-URL}") {
|
||||
return errors.New("no `{REQUEST-URL}` placeholder specified")
|
||||
@@ -47,8 +59,32 @@ func (widget *Reddit) Initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func isValidRedditSortType(sortBy string) bool {
|
||||
return sortBy == "hot" ||
|
||||
sortBy == "new" ||
|
||||
sortBy == "top" ||
|
||||
sortBy == "rising"
|
||||
}
|
||||
|
||||
func isValidRedditTopPeriod(period string) bool {
|
||||
return period == "hour" ||
|
||||
period == "day" ||
|
||||
period == "week" ||
|
||||
period == "month" ||
|
||||
period == "year" ||
|
||||
period == "all"
|
||||
}
|
||||
|
||||
func (widget *Reddit) Update(ctx context.Context) {
|
||||
posts, err := feed.FetchSubredditPosts(widget.Subreddit, widget.CommentsUrlTemplate, widget.RequestUrlTemplate)
|
||||
// TODO: refactor, use a struct to pass all of these
|
||||
posts, err := feed.FetchSubredditPosts(
|
||||
widget.Subreddit,
|
||||
widget.SortBy,
|
||||
widget.TopPeriod,
|
||||
widget.Search,
|
||||
widget.CommentsUrlTemplate,
|
||||
widget.RequestUrlTemplate,
|
||||
)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
@@ -58,7 +94,11 @@ func (widget *Reddit) Update(ctx context.Context) {
|
||||
posts = posts[:widget.Limit]
|
||||
}
|
||||
|
||||
posts.SortByEngagement()
|
||||
if widget.ExtraSortBy == "engagement" {
|
||||
posts.CalculateEngagement()
|
||||
posts.SortByEngagement()
|
||||
}
|
||||
|
||||
widget.Posts = posts
|
||||
}
|
||||
|
||||
|
||||
52
internal/widget/repository-overview.go
Normal file
52
internal/widget/repository-overview.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package widget
|
||||
|
||||
import (
|
||||
"context"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/glanceapp/glance/internal/assets"
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
RequestedRepository string `yaml:"repository"`
|
||||
Token OptionalEnvString `yaml:"token"`
|
||||
PullRequestsLimit int `yaml:"pull-requests-limit"`
|
||||
IssuesLimit int `yaml:"issues-limit"`
|
||||
RepositoryDetails feed.RepositoryDetails
|
||||
}
|
||||
|
||||
func (widget *Repository) Initialize() error {
|
||||
widget.withTitle("Repository").withCacheDuration(1 * time.Hour)
|
||||
|
||||
if widget.PullRequestsLimit == 0 || widget.PullRequestsLimit < -1 {
|
||||
widget.PullRequestsLimit = 3
|
||||
}
|
||||
|
||||
if widget.IssuesLimit == 0 || widget.IssuesLimit < -1 {
|
||||
widget.IssuesLimit = 3
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (widget *Repository) Update(ctx context.Context) {
|
||||
details, err := feed.FetchRepositoryDetailsFromGithub(
|
||||
widget.RequestedRepository,
|
||||
string(widget.Token),
|
||||
widget.PullRequestsLimit,
|
||||
widget.IssuesLimit,
|
||||
)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
}
|
||||
|
||||
widget.RepositoryDetails = details
|
||||
}
|
||||
|
||||
func (widget *Repository) Render() template.HTML {
|
||||
return widget.render(widget, assets.RepositoryTemplate)
|
||||
}
|
||||
@@ -10,12 +10,14 @@ import (
|
||||
)
|
||||
|
||||
type RSS struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
|
||||
Style string `yaml:"style"`
|
||||
Items feed.RSSFeedItems `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
widgetBase `yaml:",inline"`
|
||||
FeedRequests []feed.RSSFeedRequest `yaml:"feeds"`
|
||||
Style string `yaml:"style"`
|
||||
ThumbnailHeight float64 `yaml:"thumbnail-height"`
|
||||
CardHeight float64 `yaml:"card-height"`
|
||||
Items feed.RSSFeedItems `yaml:"-"`
|
||||
Limit int `yaml:"limit"`
|
||||
CollapseAfter int `yaml:"collapse-after"`
|
||||
}
|
||||
|
||||
func (widget *RSS) Initialize() error {
|
||||
@@ -29,6 +31,14 @@ func (widget *RSS) Initialize() error {
|
||||
widget.CollapseAfter = 5
|
||||
}
|
||||
|
||||
if widget.ThumbnailHeight < 0 {
|
||||
widget.ThumbnailHeight = 0
|
||||
}
|
||||
|
||||
if widget.CardHeight < 0 {
|
||||
widget.CardHeight = 0
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -48,7 +58,11 @@ func (widget *RSS) Update(ctx context.Context) {
|
||||
|
||||
func (widget *RSS) Render() template.HTML {
|
||||
if widget.Style == "horizontal-cards" {
|
||||
return widget.render(widget, assets.RSSCardsTemplate)
|
||||
return widget.render(widget, assets.RSSHorizontalCardsTemplate)
|
||||
}
|
||||
|
||||
if widget.Style == "horizontal-cards-2" {
|
||||
return widget.render(widget, assets.RSSHorizontalCards2Template)
|
||||
}
|
||||
|
||||
return widget.render(widget, assets.RSSListTemplate)
|
||||
|
||||
@@ -9,11 +9,12 @@ import (
|
||||
"github.com/glanceapp/glance/internal/feed"
|
||||
)
|
||||
|
||||
// TODO: rename to Markets at some point
|
||||
type Stocks struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Stocks feed.Stocks `yaml:"-"`
|
||||
Sort string `yaml:"sort-by"`
|
||||
Tickers []feed.StockRequest `yaml:"stocks"`
|
||||
Stocks feed.Stocks `yaml:"stocks"`
|
||||
Sort string `yaml:"sort-by"`
|
||||
Style string `yaml:"style"`
|
||||
}
|
||||
|
||||
func (widget *Stocks) Initialize() error {
|
||||
@@ -23,7 +24,7 @@ func (widget *Stocks) Initialize() error {
|
||||
}
|
||||
|
||||
func (widget *Stocks) Update(ctx context.Context) {
|
||||
stocks, err := feed.FetchStocksDataFromYahoo(widget.Tickers)
|
||||
stocks, err := feed.FetchStocksDataFromYahoo(widget.Stocks)
|
||||
|
||||
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||
return
|
||||
|
||||
@@ -13,6 +13,7 @@ type Videos struct {
|
||||
widgetBase `yaml:",inline"`
|
||||
Videos feed.Videos `yaml:"-"`
|
||||
VideoUrlTemplate string `yaml:"video-url-template"`
|
||||
Style string `yaml:"style"`
|
||||
Channels []string `yaml:"channels"`
|
||||
Limit int `yaml:"limit"`
|
||||
}
|
||||
@@ -42,5 +43,9 @@ func (widget *Videos) Update(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (widget *Videos) Render() template.HTML {
|
||||
if widget.Style == "grid-cards" {
|
||||
return widget.render(widget, assets.VideosGridTemplate)
|
||||
}
|
||||
|
||||
return widget.render(widget, assets.VideosTemplate)
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ func New(widgetType string) (Widget, error) {
|
||||
return &TwitchChannels{}, nil
|
||||
case "lobsters":
|
||||
return &Lobsters{}, nil
|
||||
case "repository":
|
||||
return &Repository{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown widget type: %s", widgetType)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user