mirror of
https://github.com/Xevion/glance.git
synced 2025-12-09 22:07:19 -06:00
Merge branch 'release/v0.6.0' into github-commit
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
//go:embed static
|
||||
@@ -13,3 +19,38 @@ var _templateFS embed.FS
|
||||
|
||||
var PublicFS, _ = fs.Sub(_publicFS, "static")
|
||||
var TemplateFS, _ = fs.Sub(_templateFS, "templates")
|
||||
|
||||
func getFSHash(files fs.FS) string {
|
||||
hash := md5.New()
|
||||
|
||||
err := fs.WalkDir(files, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
file, err := files.Open(path)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
return hex.EncodeToString(hash.Sum(nil))[:10]
|
||||
}
|
||||
|
||||
slog.Warn("Could not compute assets cache", "err", err)
|
||||
return strconv.FormatInt(time.Now().Unix(), 10)
|
||||
}
|
||||
|
||||
var PublicFSHash = getFSHash(PublicFS)
|
||||
|
||||
BIN
internal/assets/static/app-icon.png
Normal file
BIN
internal/assets/static/app-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.8 KiB |
1
internal/assets/static/icons/dockerhub.svg
Normal file
1
internal/assets/static/icons/dockerhub.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M13.983 11.078h2.119a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.119a.185.185 0 00-.185.185v1.888c0 .102.083.185.185.185m-2.954-5.43h2.118a.186.186 0 00.186-.186V3.574a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m0 2.716h2.118a.187.187 0 00.186-.186V6.29a.186.186 0 00-.186-.185h-2.118a.185.185 0 00-.185.185v1.887c0 .102.082.185.185.186m-2.93 0h2.12a.186.186 0 00.184-.186V6.29a.185.185 0 00-.185-.185H8.1a.185.185 0 00-.185.185v1.887c0 .102.083.185.185.186m-2.964 0h2.119a.186.186 0 00.185-.186V6.29a.185.185 0 00-.185-.185H5.136a.186.186 0 00-.186.185v1.887c0 .102.084.185.186.186m5.893 2.715h2.118a.186.186 0 00.186-.185V9.006a.186.186 0 00-.186-.186h-2.118a.185.185 0 00-.185.185v1.888c0 .102.082.185.185.185m-2.93 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.083.185.185.185m-2.964 0h2.119a.185.185 0 00.185-.185V9.006a.185.185 0 00-.184-.186h-2.12a.186.186 0 00-.186.186v1.887c0 .102.084.185.186.185m-2.92 0h2.12a.185.185 0 00.184-.185V9.006a.185.185 0 00-.184-.186h-2.12a.185.185 0 00-.184.185v1.888c0 .102.082.185.185.185M23.763 9.89c-.065-.051-.672-.51-1.954-.51-.338.001-.676.03-1.01.087-.248-1.7-1.653-2.53-1.716-2.566l-.344-.199-.226.327c-.284.438-.49.922-.612 1.43-.23.97-.09 1.882.403 2.661-.595.332-1.55.413-1.744.42H.751a.751.751 0 00-.75.748 11.376 11.376 0 00.692 4.062c.545 1.428 1.355 2.48 2.41 3.124 1.18.723 3.1 1.137 5.275 1.137.983.003 1.963-.086 2.93-.266a12.248 12.248 0 003.823-1.389c.98-.567 1.86-1.288 2.61-2.136 1.252-1.418 1.998-2.997 2.553-4.4h.221c1.372 0 2.215-.549 2.68-1.009.309-.293.55-.65.707-1.046l.098-.288Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
1
internal/assets/static/icons/github.svg
Normal file
1
internal/assets/static/icons/github.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
After Width: | Height: | Size: 802 B |
1
internal/assets/static/icons/gitlab.svg
Normal file
1
internal/assets/static/icons/gitlab.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="m23.6004 9.5927-.0337-.0862L20.3.9814a.851.851 0 0 0-.3362-.405.8748.8748 0 0 0-.9997.0539.8748.8748 0 0 0-.29.4399l-2.2055 6.748H7.5375l-2.2057-6.748a.8573.8573 0 0 0-.29-.4412.8748.8748 0 0 0-.9997-.0537.8585.8585 0 0 0-.3362.4049L.4332 9.5015l-.0325.0862a6.0657 6.0657 0 0 0 2.0119 7.0105l.0113.0087.03.0213 4.976 3.7264 2.462 1.8633 1.4995 1.1321a1.0085 1.0085 0 0 0 1.2197 0l1.4995-1.1321 2.4619-1.8633 5.006-3.7489.0125-.01a6.0682 6.0682 0 0 0 2.0094-7.003z"/></svg>
|
||||
|
After Width: | Height: | Size: 553 B |
@@ -37,6 +37,7 @@
|
||||
|
||||
--ths: var(--bgh), calc(var(--bgs) * var(--tsm));
|
||||
--color-text-base: hsl(var(--ths), calc(var(--scheme) var(--cm) * 58%));
|
||||
--color-text-base-muted: hsl(var(--ths), calc(var(--scheme) var(--cm) * 52%));
|
||||
--color-text-highlight: hsl(var(--ths), calc(var(--scheme) var(--cm) * 85%));
|
||||
--color-text-subdue: hsl(var(--ths), calc(var(--scheme) var(--cm) * 35%));
|
||||
|
||||
@@ -53,7 +54,20 @@
|
||||
--scheme: 100% -;
|
||||
}
|
||||
|
||||
.size-title-dynamic {
|
||||
.page {
|
||||
height: 100%;
|
||||
padding-block: var(--widget-gap);
|
||||
}
|
||||
|
||||
.page-content, .page.content-ready .page-loading-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page.content-ready > .page-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.page-column-small .size-title-dynamic {
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
|
||||
@@ -71,14 +85,16 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.text-truncate-3-lines {
|
||||
.text-truncate-2-lines, .text-truncate-3-lines {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
-webkit-line-clamp: 3;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.text-truncate-3-lines { -webkit-line-clamp: 3; }
|
||||
.text-truncate-2-lines { -webkit-line-clamp: 2; }
|
||||
|
||||
.visited-indicator:not(.text-truncate)::after,
|
||||
.visited-indicator.text-truncate::before,
|
||||
.bookmarks-link:not(.bookmarks-link-no-arrow)::after {
|
||||
@@ -106,6 +122,7 @@
|
||||
.list-gap-14 { --list-half-gap: 0.7rem; }
|
||||
.list-gap-20 { --list-half-gap: 1rem; }
|
||||
.list-gap-24 { --list-half-gap: 1.2rem; }
|
||||
.list-gap-34 { --list-half-gap: 1.7rem; }
|
||||
|
||||
.list > *:not(:first-child) {
|
||||
margin-top: calc(var(--list-half-gap) * 2);
|
||||
@@ -117,70 +134,135 @@
|
||||
padding-top: var(--list-half-gap);
|
||||
}
|
||||
|
||||
@keyframes listItemReveal {
|
||||
.collapsible-container:not(.container-expanded) > .collapsible-item {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible-item {
|
||||
animation: collapsibleItemReveal .25s backwards;
|
||||
}
|
||||
|
||||
@keyframes collapsibleItemReveal {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
}
|
||||
|
||||
.list-collapsible-item {
|
||||
display: none;
|
||||
animation: listItemReveal 0.3s backwards;
|
||||
animation-delay: var(--animation-delay);
|
||||
}
|
||||
|
||||
.list-collapsible-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
.expand-toggle-button {
|
||||
font: inherit;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
color: var(--color-text-base);
|
||||
text-transform: uppercase;
|
||||
font-size: var(--font-size-h4);
|
||||
padding: var(--widget-content-vertical-padding) 0;
|
||||
background: var(--color-widget-background);
|
||||
}
|
||||
|
||||
.list-collapsible-label:has(.list-collapsible-input:checked) {
|
||||
.expand-toggle-button.container-expanded {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
/* -1px to hide 1px gap on chrome */
|
||||
bottom: -1px;
|
||||
}
|
||||
|
||||
.list-collapsible:has(+ .list-collapsible-label > .list-collapsible-input:checked) .list-collapsible-item {
|
||||
display: block;
|
||||
.expand-toggle-button-icon {
|
||||
display: inline-block;
|
||||
margin-left: 1rem;
|
||||
position: relative;
|
||||
top: -.2rem;
|
||||
}
|
||||
|
||||
.list-collapsible-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.list-collapsible-label::before, .list-collapsible-label::after {
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.list-collapsible-label::before {
|
||||
content: 'SHOW MORE';
|
||||
font-size: var(--font-size-h4);
|
||||
}
|
||||
|
||||
.list-collapsible-label:has(.list-collapsible-input:checked)::before {
|
||||
content: 'SHOW LESS';
|
||||
}
|
||||
|
||||
.list-collapsible-label::after {
|
||||
.expand-toggle-button-icon::before {
|
||||
content: '';
|
||||
font-size: 0.8rem;
|
||||
transform: rotate(90deg);
|
||||
line-height: 1;
|
||||
display: inline-block;
|
||||
transition: transform 0.3s;
|
||||
}
|
||||
|
||||
.list-collapsible-label:has(.list-collapsible-input:checked)::after {
|
||||
.expand-toggle-button.container-expanded .expand-toggle-button-icon::before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.widget-content:has(.list-collapsible-label:last-child) {
|
||||
.widget-group-header {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
.widget-group-title {
|
||||
background: none;
|
||||
font: inherit;
|
||||
border: none;
|
||||
color: inherit;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
padding-bottom: 0.1rem;
|
||||
transition: color .3s, border-color .3s;
|
||||
}
|
||||
|
||||
.widget-group-title:hover:not(.widget-group-title-current) {
|
||||
border-bottom-color: var(--color-text-subdue);
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.widget-group-title-current {
|
||||
border-bottom-color: var(--color-primary);
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.widget-group-content {
|
||||
animation: widgetGroupContentEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
|
||||
.widget-group-content[data-direction="right"] {
|
||||
--direction: 5px;
|
||||
}
|
||||
|
||||
.widget-group-content[data-direction="left"] {
|
||||
--direction: -5px;
|
||||
}
|
||||
|
||||
@keyframes widgetGroupContentEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(var(--direction));
|
||||
}
|
||||
}
|
||||
|
||||
.widget-group-content:not(.widget-group-content-current) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.widget-content:has(.expand-toggle-button:last-child) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.cards-grid.collapsible-container + .expand-toggle-button {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
background-color: var(--color-background);
|
||||
}
|
||||
|
||||
.attachments {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.attachments > * {
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.1rem 0.5rem;
|
||||
font-size: var(--font-size-h6);
|
||||
background-color: var(--color-separator);
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
|
||||
color: var(--color-text-highlight);
|
||||
@@ -234,9 +316,14 @@ html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
ul {
|
||||
@@ -266,7 +353,6 @@ body {
|
||||
.page-columns {
|
||||
display: flex;
|
||||
gap: var(--widget-gap);
|
||||
margin: var(--widget-gap) 0;
|
||||
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
|
||||
@@ -278,13 +364,19 @@ body {
|
||||
}
|
||||
|
||||
.page-loading-container {
|
||||
margin: 50px auto;
|
||||
width: fit-content;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: loadingContainerEntrance 200ms backwards;
|
||||
animation-delay: 150ms;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.page-loading-container > .loading-icon {
|
||||
translate: 0 -250%;
|
||||
}
|
||||
|
||||
@keyframes loadingContainerEntrance {
|
||||
from {
|
||||
opacity: 0;
|
||||
@@ -327,12 +419,38 @@ body {
|
||||
border: 1px solid var(--color-negative);
|
||||
}
|
||||
|
||||
kbd {
|
||||
font: inherit;
|
||||
padding: 0.1rem 0.8rem;
|
||||
border-radius: var(--border-radius);
|
||||
border: 2px solid var(--color-widget-background-highlight);
|
||||
box-shadow: 0 2px 0 var(--color-widget-background-highlight);
|
||||
user-select: none;
|
||||
transition: transform .1s, box-shadow .1s;
|
||||
font-size: var(--font-size-h5);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
kbd:active {
|
||||
transform: translateY(2px);
|
||||
box-shadow: 0 0 0 0 var(--color-widget-background-highlight);
|
||||
}
|
||||
|
||||
.content-bounds {
|
||||
max-width: 1600px;
|
||||
width: 100%;
|
||||
margin-inline: auto;
|
||||
padding: 0 var(--content-bounds-padding);
|
||||
}
|
||||
|
||||
.page-width-wide .content-bounds {
|
||||
max-width: 1920px;
|
||||
}
|
||||
|
||||
.page-width-slim .content-bounds {
|
||||
max-width: 1100px;
|
||||
}
|
||||
|
||||
.dynamic-columns {
|
||||
gap: calc(var(--widget-content-vertical-padding) / 2);
|
||||
display: grid;
|
||||
@@ -401,6 +519,7 @@ body {
|
||||
|
||||
.cards-horizontal {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
@@ -424,8 +543,6 @@ body {
|
||||
@container widget (max-width: 750px) { .cards-grid { --cards-per-row: 3; } }
|
||||
@container widget (max-width: 650px) { .cards-grid { --cards-per-row: 2; } }
|
||||
|
||||
|
||||
|
||||
.widget-error-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -495,7 +612,7 @@ body {
|
||||
.widget-header {
|
||||
padding: 0 calc(var(--widget-content-horizontal-padding) + 1px);
|
||||
font-size: var(--font-size-h4);
|
||||
margin-bottom: 1rem;
|
||||
margin-bottom: 0.9rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
@@ -541,6 +658,15 @@ body {
|
||||
padding-right: var(--widget-content-horizontal-padding);
|
||||
}
|
||||
|
||||
.logo:has(img) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
max-height: 2.7rem;
|
||||
}
|
||||
|
||||
.nav {
|
||||
height: 100%;
|
||||
gap: var(--header-items-gap);
|
||||
@@ -551,7 +677,8 @@ body {
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-block: calc(var(--widget-gap) * 1.5);
|
||||
padding-bottom: calc(var(--widget-gap) * 1.5);
|
||||
padding-top: calc(var(--widget-gap) / 2);
|
||||
animation: loadingContainerEntrance 200ms backwards;
|
||||
animation-delay: 150ms;
|
||||
}
|
||||
@@ -578,16 +705,23 @@ body {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.stock-chart {
|
||||
.release-source-icon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
flex-shrink: 0;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.market-chart {
|
||||
margin-left: auto;
|
||||
width: 6.5rem;
|
||||
}
|
||||
|
||||
.stock-chart svg {
|
||||
.market-chart svg {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.stock-values {
|
||||
.market-values {
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
@@ -638,6 +772,86 @@ body {
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
width: 2.3rem;
|
||||
}
|
||||
|
||||
.search-icon-container {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* gives a wider hit area for the 3 people that will notice the animation : ) */
|
||||
.search-icon-container::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -1rem;
|
||||
}
|
||||
|
||||
.search-icon-container:hover > .search-icon {
|
||||
animation: searchIconHover 2.9s forwards;
|
||||
}
|
||||
|
||||
@keyframes searchIconHover {
|
||||
0%, 39% { translate: 0 0; }
|
||||
20% { scale: 1.3; }
|
||||
40% { scale: 1; }
|
||||
50% { translate: -30% 30%; }
|
||||
70% { translate: 30% -30%; }
|
||||
90% { translate: -30% -30%; }
|
||||
100% { translate: 0 0; }
|
||||
}
|
||||
|
||||
.search {
|
||||
transition: border-color .2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.search:hover {
|
||||
border-color: var(--color-text-subdue);
|
||||
}
|
||||
|
||||
.search:focus-within {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.search-input {
|
||||
border: 0;
|
||||
background: none;
|
||||
width: 100%;
|
||||
height: 6rem;
|
||||
font: inherit;
|
||||
outline: none;
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-text-base-muted);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.search-bangs { display: none; }
|
||||
|
||||
.search-bang {
|
||||
border-radius: calc(var(--border-radius) * 2);
|
||||
background: var(--color-widget-background-highlight);
|
||||
padding: 0.3rem 1rem;
|
||||
flex-shrink: 0;
|
||||
font-size: var(--font-size-h5);
|
||||
animation: searchBangsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
|
||||
@keyframes searchBangsEntrance {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
}
|
||||
|
||||
.search-bang:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.forum-post-list-item {
|
||||
display: flex;
|
||||
gap: 1.2rem;
|
||||
@@ -653,6 +867,10 @@ body {
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.forum-post-tags-container {
|
||||
transform: translateY(-0.15rem);
|
||||
}
|
||||
|
||||
.bookmarks-group {
|
||||
--bookmarks-group-color: var(--color-primary);
|
||||
}
|
||||
@@ -670,6 +888,7 @@ body {
|
||||
background-color: var(--color-widget-background-highlight);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 0.5rem;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.bookmarks-icon {
|
||||
@@ -678,10 +897,6 @@ body {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.simple-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:root:not(.light-scheme) .simple-icon {
|
||||
filter: invert(1);
|
||||
}
|
||||
@@ -706,7 +921,7 @@ body {
|
||||
flex-direction: column;
|
||||
width: calc(100% / 12);
|
||||
padding-top: 3px;
|
||||
max-width: 3.5rem;
|
||||
max-width: 30px;
|
||||
}
|
||||
|
||||
.weather-column-value, .weather-columns:hover .weather-column-value {
|
||||
@@ -840,6 +1055,10 @@ body {
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.clock-time span {
|
||||
color: var(--color-text-highlight);
|
||||
}
|
||||
|
||||
.monitor-site-icon {
|
||||
display: block;
|
||||
opacity: 0.8;
|
||||
@@ -852,6 +1071,10 @@ body {
|
||||
transition: filter 0.3s, opacity 0.3s;
|
||||
}
|
||||
|
||||
.monitor-site-icon.simple-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.monitor-site:hover .monitor-site-icon {
|
||||
filter: grayscale(0);
|
||||
opacity: 1;
|
||||
@@ -866,11 +1089,22 @@ body {
|
||||
|
||||
.thumbnail {
|
||||
filter: grayscale(0.2) contrast(0.9);
|
||||
transition: all 0.2s;
|
||||
opacity: 0.8;
|
||||
transition: filter 0.2s, opacity .2s;
|
||||
}
|
||||
|
||||
.thumbnail-container:hover .thumbnail {
|
||||
.thumbnail-container {
|
||||
flex-shrink: 0;
|
||||
border: 1px solid var(--color-separator);
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
.thumbnail-container > * {
|
||||
border-radius: var(--border-radius);
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.thumbnail-parent:hover .thumbnail {
|
||||
opacity: 1;
|
||||
filter: none;
|
||||
}
|
||||
@@ -918,8 +1152,23 @@ body {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.rss-detailed-description {
|
||||
max-width: 55rem;
|
||||
color: var(--color-text-base-muted);
|
||||
}
|
||||
|
||||
.rss-detailed-thumbnail {
|
||||
margin-top: 0.3rem;
|
||||
}
|
||||
|
||||
.rss-detailed-thumbnail > * {
|
||||
aspect-ratio: 3 / 2;
|
||||
height: 8.7rem;
|
||||
}
|
||||
|
||||
.twitch-category-thumbnail {
|
||||
width: 5rem;
|
||||
aspect-ratio: 3 / 4;
|
||||
border-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
@@ -985,7 +1234,7 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.page-column-full .size-title-dynamic {
|
||||
.page-column-small .size-title-dynamic {
|
||||
font-size: var(--font-size-h3);
|
||||
}
|
||||
|
||||
@@ -996,10 +1245,10 @@ body {
|
||||
|
||||
.page-column {
|
||||
display: none;
|
||||
animation: columnEntrance 0s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
animation: columnEntrance .0s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
|
||||
.animate-element-transition .page-column {
|
||||
.page-columns-transitioned .page-column {
|
||||
animation-duration: .3s;
|
||||
}
|
||||
|
||||
@@ -1010,8 +1259,9 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
padding-bottom: calc(var(--mobile-navigation-height) + var(--content-bounds-padding));
|
||||
.mobile-navigation-offset {
|
||||
height: var(--mobile-navigation-height);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mobile-navigation {
|
||||
@@ -1044,7 +1294,8 @@ body {
|
||||
padding: 15px var(--content-bounds-padding);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow-x: scroll;
|
||||
overflow-x: auto;
|
||||
scrollbar-width: thin;
|
||||
gap: 2.5rem;
|
||||
}
|
||||
|
||||
@@ -1107,9 +1358,48 @@ body {
|
||||
box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor;
|
||||
}
|
||||
|
||||
.list-collapsible-label:has(.list-collapsible-input:checked) {
|
||||
.expand-toggle-button.container-expanded {
|
||||
bottom: var(--mobile-navigation-height);
|
||||
}
|
||||
|
||||
.cards-grid + .expand-toggle-button.container-expanded {
|
||||
/* hides content that peeks through the rounded borders of the mobile navigation */
|
||||
box-shadow: 0 var(--border-radius) 0 0 var(--color-background);
|
||||
}
|
||||
|
||||
.weather-column-rain::before {
|
||||
background-size: 7px 7px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1190px) and (display-mode: standalone) {
|
||||
:root {
|
||||
--safe-area-inset-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
.list-collapsible-label:has(.list-collapsible-input:checked) {
|
||||
bottom: calc(var(--mobile-navigation-height) + var(--safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.mobile-navigation {
|
||||
transform: translateY(calc(100% - var(--mobile-navigation-height) - var(--safe-area-inset-bottom)));
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
}
|
||||
|
||||
.mobile-navigation-icons {
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
transition: padding-bottom .3s;
|
||||
}
|
||||
|
||||
.mobile-navigation-icons:has(.mobile-navigation-page-links-input:checked) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (display-mode: standalone) {
|
||||
body {
|
||||
padding-top: env(safe-area-inset-top, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 550px) {
|
||||
@@ -1123,22 +1413,30 @@ body {
|
||||
|
||||
.dynamic-columns:has(> :nth-child(1)) { --columns-per-row: 1; }
|
||||
|
||||
.forum-post-list-item {
|
||||
flex-flow: row-reverse;
|
||||
.row-reverse-on-mobile {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.hide-on-mobile {
|
||||
.hide-on-mobile, .thumbnail-container:has(> .hide-on-mobile) {
|
||||
display: none
|
||||
}
|
||||
|
||||
.mobile-reachability-header {
|
||||
display: block;
|
||||
font-size: 3rem;
|
||||
padding: 10dvh 1rem;
|
||||
padding: 10vh 1rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-highlight);
|
||||
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
|
||||
}
|
||||
|
||||
.rss-detailed-thumbnail > * {
|
||||
height: 6rem;
|
||||
}
|
||||
|
||||
.rss-detailed-description {
|
||||
-webkit-line-clamp: 3;
|
||||
}
|
||||
}
|
||||
|
||||
.size-h1 { font-size: var(--font-size-h1); }
|
||||
@@ -1166,7 +1464,10 @@ body {
|
||||
.shrink { flex-shrink: 1; }
|
||||
.shrink-0 { flex-shrink: 0; }
|
||||
.min-width-0 { min-width: 0; }
|
||||
.max-width-100 { max-width: 100%; }
|
||||
.height-100 { height: 100%; }
|
||||
.block { display: block; }
|
||||
.inline-block { display: inline-block; }
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
.relative { position: relative; }
|
||||
.flex { display: flex; }
|
||||
@@ -1174,6 +1475,7 @@ body {
|
||||
.flex-nowrap { flex-wrap: nowrap; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.justify-stretch { justify-content: stretch; }
|
||||
.justify-evenly { justify-content: space-evenly; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-end { justify-content: end; }
|
||||
.uppercase { text-transform: uppercase; }
|
||||
@@ -1185,11 +1487,17 @@ body {
|
||||
.gap-7 { gap: 0.7rem; }
|
||||
.gap-10 { gap: 1rem; }
|
||||
.gap-15 { gap: 1.5rem; }
|
||||
.gap-20 { gap: 2rem; }
|
||||
.gap-25 { gap: 2.5rem; }
|
||||
.gap-35 { gap: 3.5rem; }
|
||||
.gap-45 { gap: 4.5rem; }
|
||||
.gap-55 { gap: 5.5rem; }
|
||||
.margin-top-3 { margin-top: 0.3rem; }
|
||||
.margin-top-5 { margin-top: 0.5rem; }
|
||||
.margin-top-7 { margin-top: 0.7rem; }
|
||||
.margin-top-10 { margin-top: 1rem; }
|
||||
.margin-top-15 { margin-top: 1.5rem; }
|
||||
.margin-top-auto { margin-top: auto; }
|
||||
.margin-block-3 { margin-block: 0.3rem; }
|
||||
.margin-block-5 { margin-block: 0.5rem; }
|
||||
.margin-block-7 { margin-block: 0.7rem; }
|
||||
@@ -1201,3 +1509,4 @@ body {
|
||||
.margin-bottom-10 { margin-bottom: 1rem; }
|
||||
.margin-bottom-15 { margin-bottom: 1.5rem; }
|
||||
.margin-bottom-auto { margin-bottom: auto; }
|
||||
.scale-half { transform: scale(0.5); }
|
||||
|
||||
@@ -21,10 +21,10 @@ function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
|
||||
};
|
||||
|
||||
|
||||
async function fetchPageContents (pageSlug) {
|
||||
async function fetchPageContent(pageData) {
|
||||
// TODO: handle non 200 status codes/time outs
|
||||
// TODO: add retries
|
||||
const response = await fetch(`/api/pages/${pageSlug}/content/`);
|
||||
const response = await fetch(`${pageData.baseURL}/api/pages/${pageData.slug}/content/`);
|
||||
const content = await response.text();
|
||||
|
||||
return content;
|
||||
@@ -33,8 +33,13 @@ async function fetchPageContents (pageSlug) {
|
||||
function setupCarousels() {
|
||||
const carouselElements = document.getElementsByClassName("carousel-container");
|
||||
|
||||
if (carouselElements.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < carouselElements.length; i++) {
|
||||
const carousel = carouselElements[i];
|
||||
carousel.classList.add("show-right-cutoff");
|
||||
const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
|
||||
|
||||
const determineSideCutoffs = () => {
|
||||
@@ -54,9 +59,9 @@ function setupCarousels() {
|
||||
const determineSideCutoffsRateLimited = throttledDebounce(determineSideCutoffs, 20, 100);
|
||||
|
||||
itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
|
||||
document.addEventListener("resize", determineSideCutoffsRateLimited);
|
||||
window.addEventListener("resize", determineSideCutoffsRateLimited);
|
||||
|
||||
determineSideCutoffs();
|
||||
afterContentReady(determineSideCutoffs);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +103,108 @@ function updateRelativeTimeForElements(elements)
|
||||
if (timestamp === undefined)
|
||||
continue
|
||||
|
||||
element.innerText = relativeTimeSince(timestamp);
|
||||
element.textContent = relativeTimeSince(timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
function setupSearchBoxes() {
|
||||
const searchWidgets = document.getElementsByClassName("search");
|
||||
|
||||
if (searchWidgets.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < searchWidgets.length; i++) {
|
||||
const widget = searchWidgets[i];
|
||||
const defaultSearchUrl = widget.dataset.defaultSearchUrl;
|
||||
const newTab = widget.dataset.newTab === "true";
|
||||
const inputElement = widget.getElementsByClassName("search-input")[0];
|
||||
const bangElement = widget.getElementsByClassName("search-bang")[0];
|
||||
const bangs = widget.querySelectorAll(".search-bangs > input");
|
||||
const bangsMap = {};
|
||||
const kbdElement = widget.getElementsByTagName("kbd")[0];
|
||||
let currentBang = null;
|
||||
|
||||
for (let j = 0; j < bangs.length; j++) {
|
||||
const bang = bangs[j];
|
||||
bangsMap[bang.dataset.shortcut] = bang;
|
||||
}
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
if (event.key == "Escape") {
|
||||
inputElement.blur();
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.key == "Enter") {
|
||||
const input = inputElement.value.trim();
|
||||
let query;
|
||||
let searchUrlTemplate;
|
||||
|
||||
if (currentBang != null) {
|
||||
query = input.slice(currentBang.dataset.shortcut.length + 1);
|
||||
searchUrlTemplate = currentBang.dataset.url;
|
||||
} else {
|
||||
query = input;
|
||||
searchUrlTemplate = defaultSearchUrl;
|
||||
}
|
||||
if (query.length == 0 && currentBang == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = searchUrlTemplate.replace("!QUERY!", encodeURIComponent(query));
|
||||
|
||||
if (newTab && !event.ctrlKey || !newTab && event.ctrlKey) {
|
||||
window.open(url, '_blank').focus();
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const changeCurrentBang = (bang) => {
|
||||
currentBang = bang;
|
||||
bangElement.textContent = bang != null ? bang.dataset.title : "";
|
||||
}
|
||||
|
||||
const handleInput = (event) => {
|
||||
const value = event.target.value.trim();
|
||||
if (value in bangsMap) {
|
||||
changeCurrentBang(bangsMap[value]);
|
||||
return;
|
||||
}
|
||||
|
||||
const words = value.split(" ");
|
||||
if (words.length >= 2 && words[0] in bangsMap) {
|
||||
changeCurrentBang(bangsMap[words[0]]);
|
||||
return;
|
||||
}
|
||||
|
||||
changeCurrentBang(null);
|
||||
};
|
||||
|
||||
inputElement.addEventListener("focus", () => {
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
document.addEventListener("input", handleInput);
|
||||
});
|
||||
inputElement.addEventListener("blur", () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
document.removeEventListener("input", handleInput);
|
||||
});
|
||||
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName)) return;
|
||||
if (event.key != "s") return;
|
||||
|
||||
inputElement.focus();
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
kbdElement.addEventListener("mousedown", () => {
|
||||
requestAnimationFrame(() => inputElement.focus());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,6 +213,8 @@ function setupDynamicRelativeTime() {
|
||||
const updateInterval = 60 * 1000;
|
||||
let lastUpdateTime = Date.now();
|
||||
|
||||
updateRelativeTimeForElements(elements);
|
||||
|
||||
const updateElementsAndTimestamp = () => {
|
||||
updateRelativeTimeForElements(elements);
|
||||
lastUpdateTime = Date.now();
|
||||
@@ -142,6 +250,46 @@ function setupDynamicRelativeTime() {
|
||||
});
|
||||
}
|
||||
|
||||
function setupGroups() {
|
||||
const groups = document.getElementsByClassName("widget-type-group");
|
||||
|
||||
if (groups.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let g = 0; g < groups.length; g++) {
|
||||
const group = groups[g];
|
||||
const titles = group.getElementsByClassName("widget-header")[0].children;
|
||||
const tabs = group.getElementsByClassName("widget-group-contents")[0].children;
|
||||
let current = 0;
|
||||
|
||||
for (let t = 0; t < titles.length; t++) {
|
||||
const title = titles[t];
|
||||
title.addEventListener("click", () => {
|
||||
if (t == current) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < titles.length; i++) {
|
||||
titles[i].classList.remove("widget-group-title-current");
|
||||
tabs[i].classList.remove("widget-group-content-current");
|
||||
}
|
||||
|
||||
if (current < t) {
|
||||
tabs[t].dataset.direction = "right";
|
||||
} else {
|
||||
tabs[t].dataset.direction = "left";
|
||||
}
|
||||
|
||||
current = t;
|
||||
|
||||
title.classList.add("widget-group-title-current");
|
||||
tabs[t].classList.add("widget-group-content-current");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupLazyImages() {
|
||||
const images = document.querySelectorAll("img[loading=lazy]");
|
||||
|
||||
@@ -153,39 +301,317 @@ function setupLazyImages() {
|
||||
image.classList.add("finished-transition");
|
||||
}
|
||||
|
||||
for (let i = 0; i < images.length; i++) {
|
||||
const image = images[i];
|
||||
afterContentReady(() => {
|
||||
setTimeout(() => {
|
||||
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);
|
||||
});
|
||||
if (image.complete) {
|
||||
image.classList.add("cached");
|
||||
setTimeout(() => imageFinishedTransition(image), 1);
|
||||
} else {
|
||||
// TODO: also handle error event
|
||||
image.addEventListener("load", () => {
|
||||
image.classList.add("loaded");
|
||||
setTimeout(() => imageFinishedTransition(image), 400);
|
||||
});
|
||||
}
|
||||
}
|
||||
}, 1);
|
||||
});
|
||||
}
|
||||
|
||||
function attachExpandToggleButton(collapsibleContainer) {
|
||||
const showMoreText = "Show more";
|
||||
const showLessText = "Show less";
|
||||
|
||||
let expanded = false;
|
||||
const button = document.createElement("button");
|
||||
const icon = document.createElement("span");
|
||||
icon.classList.add("expand-toggle-button-icon");
|
||||
const textNode = document.createTextNode(showMoreText);
|
||||
button.classList.add("expand-toggle-button");
|
||||
button.append(textNode, icon);
|
||||
button.addEventListener("click", () => {
|
||||
expanded = !expanded;
|
||||
|
||||
if (expanded) {
|
||||
collapsibleContainer.classList.add("container-expanded");
|
||||
button.classList.add("container-expanded");
|
||||
textNode.nodeValue = showLessText;
|
||||
return;
|
||||
}
|
||||
|
||||
const topBefore = button.getClientRects()[0].top;
|
||||
|
||||
collapsibleContainer.classList.remove("container-expanded");
|
||||
button.classList.remove("container-expanded");
|
||||
textNode.nodeValue = showMoreText;
|
||||
|
||||
const topAfter = button.getClientRects()[0].top;
|
||||
|
||||
if (topAfter > 0)
|
||||
return;
|
||||
|
||||
window.scrollBy({
|
||||
top: topAfter - topBefore,
|
||||
behavior: "instant"
|
||||
});
|
||||
});
|
||||
|
||||
collapsibleContainer.after(button);
|
||||
|
||||
return button;
|
||||
};
|
||||
|
||||
|
||||
function setupCollapsibleLists() {
|
||||
const collapsibleLists = document.querySelectorAll(".list.collapsible-container");
|
||||
|
||||
if (collapsibleLists.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < collapsibleLists.length; i++) {
|
||||
const list = collapsibleLists[i];
|
||||
|
||||
if (list.dataset.collapseAfter === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collapseAfter = parseInt(list.dataset.collapseAfter);
|
||||
|
||||
if (collapseAfter == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (list.children.length <= collapseAfter) {
|
||||
continue;
|
||||
}
|
||||
|
||||
attachExpandToggleButton(list);
|
||||
|
||||
for (let c = collapseAfter; c < list.children.length; c++) {
|
||||
const child = list.children[c];
|
||||
child.classList.add("collapsible-item");
|
||||
child.style.animationDelay = ((c - collapseAfter) * 20).toString() + "ms";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupCollapsibleGrids() {
|
||||
const collapsibleGridElements = document.querySelectorAll(".cards-grid.collapsible-container");
|
||||
|
||||
if (collapsibleGridElements.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < collapsibleGridElements.length; i++) {
|
||||
const gridElement = collapsibleGridElements[i];
|
||||
|
||||
if (gridElement.dataset.collapseAfterRows === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const collapseAfterRows = parseInt(gridElement.dataset.collapseAfterRows);
|
||||
|
||||
if (collapseAfterRows == -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const getCardsPerRow = () => {
|
||||
return parseInt(getComputedStyle(gridElement).getPropertyValue('--cards-per-row'));
|
||||
};
|
||||
|
||||
const button = attachExpandToggleButton(gridElement);
|
||||
|
||||
let cardsPerRow = 2;
|
||||
|
||||
const resolveCollapsibleItems = () => {
|
||||
const hideItemsAfterIndex = cardsPerRow * collapseAfterRows;
|
||||
|
||||
if (hideItemsAfterIndex >= gridElement.children.length) {
|
||||
button.style.display = "none";
|
||||
} else {
|
||||
button.style.removeProperty("display");
|
||||
}
|
||||
|
||||
let row = 0;
|
||||
|
||||
for (let i = 0; i < gridElement.children.length; i++) {
|
||||
const child = gridElement.children[i];
|
||||
|
||||
if (i >= hideItemsAfterIndex) {
|
||||
child.classList.add("collapsible-item");
|
||||
child.style.animationDelay = (row * 40).toString() + "ms";
|
||||
|
||||
if (i % cardsPerRow + 1 == cardsPerRow) {
|
||||
row++;
|
||||
}
|
||||
} else {
|
||||
child.classList.remove("collapsible-item");
|
||||
child.style.removeProperty("animation-delay");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
afterContentReady(() => {
|
||||
cardsPerRow = getCardsPerRow();
|
||||
resolveCollapsibleItems();
|
||||
});
|
||||
|
||||
window.addEventListener("resize", () => {
|
||||
const newCardsPerRow = getCardsPerRow();
|
||||
|
||||
if (cardsPerRow == newCardsPerRow) {
|
||||
return;
|
||||
}
|
||||
|
||||
cardsPerRow = newCardsPerRow;
|
||||
resolveCollapsibleItems();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const contentReadyCallbacks = [];
|
||||
|
||||
function afterContentReady(callback) {
|
||||
contentReadyCallbacks.push(callback);
|
||||
}
|
||||
|
||||
const weekDayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
|
||||
function makeSettableTimeElement(element, hourFormat) {
|
||||
const fragment = document.createDocumentFragment();
|
||||
const hour = document.createElement('span');
|
||||
const minute = document.createElement('span');
|
||||
const amPm = document.createElement('span');
|
||||
fragment.append(hour, document.createTextNode(':'), minute);
|
||||
|
||||
if (hourFormat == '12h') {
|
||||
fragment.append(document.createTextNode(' '), amPm);
|
||||
}
|
||||
|
||||
element.append(fragment);
|
||||
|
||||
return (date) => {
|
||||
const hours = date.getHours();
|
||||
|
||||
if (hourFormat == '12h') {
|
||||
amPm.textContent = hours < 12 ? 'AM' : 'PM';
|
||||
hour.textContent = hours % 12 || 12;
|
||||
} else {
|
||||
hour.textContent = hours < 10 ? '0' + hours : hours;
|
||||
}
|
||||
|
||||
const minutes = date.getMinutes();
|
||||
minute.textContent = minutes < 10 ? '0' + minutes : minutes;
|
||||
};
|
||||
};
|
||||
|
||||
function timeInZone(now, zone) {
|
||||
let timeInZone;
|
||||
|
||||
try {
|
||||
timeInZone = new Date(now.toLocaleString('en-US', { timeZone: zone }));
|
||||
} catch (e) {
|
||||
// TODO: indicate to the user that this is an invalid timezone
|
||||
console.error(e);
|
||||
timeInZone = now
|
||||
}
|
||||
|
||||
const diffInHours = Math.round((timeInZone.getTime() - now.getTime()) / 1000 / 60 / 60);
|
||||
|
||||
return { time: timeInZone, diffInHours: diffInHours };
|
||||
}
|
||||
|
||||
function setupClocks() {
|
||||
const clocks = document.getElementsByClassName('clock');
|
||||
|
||||
if (clocks.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateCallbacks = [];
|
||||
|
||||
for (var i = 0; i < clocks.length; i++) {
|
||||
const clock = clocks[i];
|
||||
const hourFormat = clock.dataset.hourFormat;
|
||||
const localTimeContainer = clock.querySelector('[data-local-time]');
|
||||
const localDateElement = localTimeContainer.querySelector('[data-date]');
|
||||
const localWeekdayElement = localTimeContainer.querySelector('[data-weekday]');
|
||||
const localYearElement = localTimeContainer.querySelector('[data-year]');
|
||||
const timeZoneContainers = clock.querySelectorAll('[data-time-in-zone]');
|
||||
|
||||
const setLocalTime = makeSettableTimeElement(
|
||||
localTimeContainer.querySelector('[data-time]'),
|
||||
hourFormat
|
||||
);
|
||||
|
||||
updateCallbacks.push((now) => {
|
||||
setLocalTime(now);
|
||||
localDateElement.textContent = now.getDate() + ' ' + monthNames[now.getMonth()];
|
||||
localWeekdayElement.textContent = weekDayNames[now.getDay()];
|
||||
localYearElement.textContent = now.getFullYear();
|
||||
});
|
||||
|
||||
for (var z = 0; z < timeZoneContainers.length; z++) {
|
||||
const timeZoneContainer = timeZoneContainers[z];
|
||||
const diffElement = timeZoneContainer.querySelector('[data-time-diff]');
|
||||
|
||||
const setZoneTime = makeSettableTimeElement(
|
||||
timeZoneContainer.querySelector('[data-time]'),
|
||||
hourFormat
|
||||
);
|
||||
|
||||
updateCallbacks.push((now) => {
|
||||
const { time, diffInHours } = timeInZone(now, timeZoneContainer.dataset.timeInZone);
|
||||
setZoneTime(time);
|
||||
diffElement.textContent = (diffInHours <= 0 ? diffInHours : '+' + diffInHours) + 'h';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const updateClocks = () => {
|
||||
const now = new Date();
|
||||
|
||||
for (var i = 0; i < updateCallbacks.length; i++)
|
||||
updateCallbacks[i](now);
|
||||
|
||||
setTimeout(updateClocks, (60 - now.getSeconds()) * 1000);
|
||||
};
|
||||
|
||||
updateClocks();
|
||||
}
|
||||
|
||||
async function setupPage() {
|
||||
const pageElement = document.getElementById("page");
|
||||
const pageContents = await fetchPageContents(pageData.slug);
|
||||
const pageContentElement = document.getElementById("page-content");
|
||||
const pageContent = await fetchPageContent(pageData);
|
||||
|
||||
pageElement.innerHTML = pageContents;
|
||||
pageContentElement.innerHTML = pageContent;
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.add("animate-element-transition");
|
||||
}, 150);
|
||||
try {
|
||||
setupClocks()
|
||||
setupCarousels();
|
||||
setupSearchBoxes();
|
||||
setupCollapsibleLists();
|
||||
setupCollapsibleGrids();
|
||||
setupGroups();
|
||||
setupDynamicRelativeTime();
|
||||
setupLazyImages();
|
||||
} finally {
|
||||
pageElement.classList.add("content-ready");
|
||||
|
||||
setTimeout(setupLazyImages, 5);
|
||||
setupCarousels();
|
||||
setupDynamicRelativeTime();
|
||||
for (let i = 0; i < contentReadyCallbacks.length; i++) {
|
||||
contentReadyCallbacks[i]();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.classList.add("page-columns-transitioned");
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", setupPage);
|
||||
} else {
|
||||
setupPage();
|
||||
}
|
||||
setupPage();
|
||||
|
||||
14
internal/assets/static/manifest.json
Normal file
14
internal/assets/static/manifest.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "Glance",
|
||||
"display": "standalone",
|
||||
"background_color": "#151519",
|
||||
"scope": "/",
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "app-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -15,6 +15,7 @@ var (
|
||||
PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl")
|
||||
PageContentTemplate = compileTemplate("content.html")
|
||||
CalendarTemplate = compileTemplate("calendar.html", "widget-base.html")
|
||||
ClockTemplate = compileTemplate("clock.html", "widget-base.html")
|
||||
BookmarksTemplate = compileTemplate("bookmarks.html", "widget-base.html")
|
||||
IFrameTemplate = compileTemplate("iframe.html", "widget-base.html")
|
||||
WeatherTemplate = compileTemplate("weather.html", "widget-base.html")
|
||||
@@ -22,16 +23,21 @@ 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")
|
||||
ChangeDetectionTemplate = compileTemplate("change-detection.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")
|
||||
MarketsTemplate = compileTemplate("markets.html", "widget-base.html")
|
||||
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
|
||||
RSSDetailedListTemplate = compileTemplate("rss-detailed-list.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")
|
||||
SearchTemplate = compileTemplate("search.html", "widget-base.html")
|
||||
ExtensionTemplate = compileTemplate("extension.html", "widget-base.html")
|
||||
GroupTemplate = compileTemplate("group.html", "widget-base.html")
|
||||
)
|
||||
|
||||
var globalTemplateFunctions = template.FuncMap{
|
||||
|
||||
17
internal/assets/templates/change-detection.html
Normal file
17
internal/assets/templates/change-detection.html
Normal file
@@ -0,0 +1,17 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .ChangeDetections }}
|
||||
<li>
|
||||
<a class="size-h4 block text-truncate color-highlight" href="{{ .URL }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li {{ dynamicRelativeTimeAttrs .LastChanged }}></li>
|
||||
<li class="shrink min-width-0"><a class="visited-indicator" href="{{ .DiffURL }}" target="_blank" rel="noreferrer">diff:{{ .PreviousHash }}</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li>No watches configured</li>
|
||||
{{ end}}
|
||||
</ul>
|
||||
{{ end }}
|
||||
30
internal/assets/templates/clock.html
Normal file
30
internal/assets/templates/clock.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="clock" data-hour-format="{{ .HourFormat }}">
|
||||
<div class="flex justify-between items-center" data-local-time>
|
||||
<div>
|
||||
<div class="color-highlight size-h1" data-date></div>
|
||||
<div data-year></div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<div class="clock-time size-h1" data-time></div>
|
||||
<div data-weekday></div>
|
||||
</div>
|
||||
</div>
|
||||
{{ if gt (len .Timezones) 0 }}
|
||||
<hr class="margin-block-10">
|
||||
<ul class="list list-gap-4">
|
||||
{{ range .Timezones }}
|
||||
<li class="flex items-center gap-15" data-time-in-zone="{{ .Timezone }}">
|
||||
<div class="grow min-width-0">
|
||||
<div class="text-truncate">{{ if ne .Label "" }}{{ .Label }}{{ else }}{{ .Timezone }}{{ end }}</div>
|
||||
</div>
|
||||
<div class="color-subdue" data-time-diff></div>
|
||||
<div class="size-h4 clock-time shrink-0 text-right" data-time></div>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -5,10 +5,17 @@
|
||||
<title>{{ block "document-title" . }}{{ end }}</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/png" href="/static/favicon.png" />
|
||||
<link rel="stylesheet" href="/static/main.css?v={{ .App.Config.Server.StartedAt.Unix }}">
|
||||
<script async src="/static/main.js?v={{ .App.Config.Server.StartedAt.Unix }}"></script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="Glance">
|
||||
<meta name="theme-color" content="{{ if ne nil .App.Config.Theme.BackgroundColor }}{{ .App.Config.Theme.BackgroundColor }}{{ else }}hsl(240, 8%, 9%){{ end }}">
|
||||
<link rel="apple-touch-icon" sizes="512x512" href="{{ .App.AssetPath "app-icon.png" }}">
|
||||
<link rel="manifest" href="{{ .App.AssetPath "manifest.json" }}">
|
||||
<link rel="icon" type="image/png" href="{{ .App.Config.Branding.FaviconURL }}" />
|
||||
<link rel="stylesheet" href="{{ .App.AssetPath "main.css" }}">
|
||||
<script type="module" src="{{ .App.AssetPath "main.js" }}"></script>
|
||||
{{ block "document-head-after" . }}{{ end }}
|
||||
</head>
|
||||
<body>
|
||||
|
||||
5
internal/assets/templates/extension.html
Normal file
5
internal/assets/templates/extension.html
Normal file
@@ -0,0 +1,5 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ .Extension.Content }}
|
||||
{{ end }}
|
||||
@@ -1,14 +1,18 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
{{ range $i, $post := .Posts }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<div class="forum-post-list-item thumbnail-container">
|
||||
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Posts }}
|
||||
<li>
|
||||
<div class="flex gap-10 row-reverse-on-mobile thumbnail-parent">
|
||||
{{ if $.ShowThumbnails }}
|
||||
{{ if ne $post.ThumbnailUrl "" }}
|
||||
<img class="forum-post-list-thumbnail thumbnail" src="{{ $post.ThumbnailUrl }}" alt="" loading="lazy">
|
||||
{{ else if $post.HasTargetUrl }}
|
||||
{{ if .IsCrosspost }}
|
||||
<svg class="forum-post-list-thumbnail hide-on-mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-9 -8 40 40" stroke-width="1.5" stroke="var(--color-text-subdue)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 21 3 16.5m0 0L7.5 12M3 16.5h13.5m0-13.5L21 7.5m0 0L16.5 12M21 7.5H7.5" />
|
||||
</svg>
|
||||
{{ else if ne .ThumbnailUrl "" }}
|
||||
<img class="forum-post-list-thumbnail thumbnail" src="{{ .ThumbnailUrl }}" alt="" loading="lazy">
|
||||
{{ else if .HasTargetUrl }}
|
||||
<svg class="forum-post-list-thumbnail hide-on-mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="-9 -8 40 40" stroke-width="1.5" stroke="var(--color-text-subdue)">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 0 1 1.242 7.244l-4.5 4.5a4.5 4.5 0 0 1-6.364-6.364l1.757-1.757m13.35-.622 1.757-1.757a4.5 4.5 0 0 0-6.364-6.364l-4.5 4.5a4.5 4.5 0 0 0 1.242 7.244" />
|
||||
</svg>
|
||||
@@ -18,14 +22,23 @@
|
||||
</svg>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
<div class="grow">
|
||||
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<div class="grow min-width-0">
|
||||
<a href="{{ .DiscussionUrl }}" class="size-title-dynamic color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
{{ if gt (len .Tags) 0 }}
|
||||
<div class="inline-block forum-post-tags-container">
|
||||
<ul class="attachments">
|
||||
{{ range .Tags }}
|
||||
<li>{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ end }}
|
||||
<ul class="list-horizontal-text">
|
||||
<li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li>
|
||||
<li>{{ $post.Score | formatNumber }} points</li>
|
||||
<li>{{ $post.CommentCount | formatNumber }} comments</li>
|
||||
{{ if $post.HasTargetUrl }}
|
||||
<li class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ $post.TargetUrlDomain }}</a></li>
|
||||
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
|
||||
<li>{{ .Score | formatNumber }} points</li>
|
||||
<li>{{ .CommentCount | formatNumber }} comments</li>
|
||||
{{ if .HasTargetUrl }}
|
||||
<li class="min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ .TargetUrlDomain }}</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -33,7 +46,4 @@
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Posts) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
20
internal/assets/templates/group.html
Normal file
20
internal/assets/templates/group.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="widget-group-header">
|
||||
<div class="widget-header gap-20">
|
||||
{{ range $i, $widget := .Widgets }}
|
||||
<button class="widget-group-title{{ if eq $i 0 }} widget-group-title-current{{ end }}">{{ $widget.Title }}</button>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="widget-group-contents">
|
||||
{{ range $i, $widget := .Widgets }}
|
||||
<div class="widget-group-content{{ if eq $i 0 }} widget-group-content-current{{ end }}">{{ .Render }}</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
{{ end }}
|
||||
@@ -3,36 +3,36 @@
|
||||
{{ define "widget-content" }}
|
||||
{{ if ne .Style "dynamic-columns-experimental" }}
|
||||
<ul class="list list-gap-20 list-with-separator">
|
||||
{{ range .Stocks }}
|
||||
{{ range .Markets }}
|
||||
<li class="flex items-center gap-15">
|
||||
{{ template "stock" . }}
|
||||
{{ template "market" . }}
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div class="dynamic-columns">
|
||||
{{ range .Stocks }}
|
||||
{{ range .Markets }}
|
||||
<div class="flex items-center gap-15">
|
||||
{{ template "stock" . }}
|
||||
{{ template "market" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "stock" }}
|
||||
<div class="shrink min-width-0">
|
||||
{{ define "market" }}
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<a class="market-chart" {{ if ne "" .ChartLink }} href="{{ .ChartLink }}" target="_blank" rel="noreferrer"{{ end }}>
|
||||
<svg class="market-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="market-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>
|
||||
@@ -22,13 +22,13 @@
|
||||
|
||||
{{ define "site" }}
|
||||
{{ if .IconUrl }}
|
||||
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
|
||||
<img class="monitor-site-icon{{ if .IsSimpleIcon }} simple-icon{{ end }}" 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>
|
||||
<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 title="{{ .Status.Code }}">{{ .StatusText }}</li>
|
||||
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
|
||||
{{ else if .Status.TimedOut }}
|
||||
<li class="color-negative">Timed Out</li>
|
||||
@@ -37,7 +37,7 @@
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
{{ if eq .StatusStyle "good" }}
|
||||
{{ if eq .StatusStyle "ok" }}
|
||||
<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" />
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
{{ template "document.html" . }}
|
||||
|
||||
{{ define "document-title" }}{{ .Page.Title }} - Glance{{ end }}
|
||||
{{ define "document-title" }}{{ .Page.Title }}{{ end }}
|
||||
|
||||
{{ define "document-head-before" }}
|
||||
<script>
|
||||
const pageData = {
|
||||
slug: "{{ .Page.Slug }}",
|
||||
baseURL: "{{ .App.Config.Server.BaseURL }}",
|
||||
};
|
||||
</script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "document-root-attrs" }}{{ if .App.Config.Theme.Light }}class="light-scheme"{{ end }}{{ end }}
|
||||
{{ define "document-root-attrs" }}class="{{ if .App.Config.Theme.Light }}light-scheme {{ end }}{{ if ne "" .Page.Width }}page-width-{{ .Page.Width }}{{ end }}"{{ end }}
|
||||
|
||||
{{ define "document-head-after" }}
|
||||
{{ template "page-style-overrides.gotmpl" . }}
|
||||
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
|
||||
@@ -20,50 +22,59 @@
|
||||
|
||||
{{ define "navigation-links" }}
|
||||
{{ range .App.Config.Pages }}
|
||||
<a href="/{{ .Slug }}" class="nav-item{{ if eq .Slug $.Page.Slug }} nav-item-current{{ end }}">{{ .Title }}</a>
|
||||
<a href="{{ $.App.Config.Server.BaseURL }}/{{ .Slug }}" class="nav-item{{ if eq .Slug $.Page.Slug }} nav-item-current{{ end }}">{{ .Title }}</a>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
{{ define "document-body" }}
|
||||
<div class="header-container content-bounds">
|
||||
<div class="header flex padding-inline-widget widget-content-frame">
|
||||
<!-- TODO: Replace G with actual logo, first need an actual logo -->
|
||||
<div class="logo">G</div>
|
||||
<div class="nav flex grow">
|
||||
<div class="flex flex-column height-100">
|
||||
{{ if not .Page.HideDesktopNavigation }}
|
||||
<div class="header-container content-bounds">
|
||||
<div class="header flex padding-inline-widget widget-content-frame">
|
||||
<!-- TODO: Replace G with actual logo, first need an actual logo -->
|
||||
<div class="logo">{{ if ne "" .App.Config.Branding.LogoURL }}<img src="{{ .App.Config.Branding.LogoURL }}" alt="">{{ else if ne "" .App.Config.Branding.LogoText }}{{ .App.Config.Branding.LogoText }}{{ else }}G{{ end }}</div>
|
||||
<div class="nav flex grow">
|
||||
{{ template "navigation-links" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
<div class="mobile-navigation">
|
||||
<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>
|
||||
{{ end }}
|
||||
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"><div class="hamburger-icon"></div></label>
|
||||
</div>
|
||||
<div class="mobile-navigation-page-links">
|
||||
{{ template "navigation-links" . }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mobile-navigation">
|
||||
<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>
|
||||
{{ end }}
|
||||
<label class="mobile-navigation-label"><input type="checkbox" class="mobile-navigation-page-links-input" autocomplete="on"><div class="hamburger-icon"></div></label>
|
||||
</div>
|
||||
<div class="mobile-navigation-page-links">
|
||||
{{ template "navigation-links" . }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-bounds">
|
||||
<div class="page" id="page">
|
||||
<div class="page-loading-container">
|
||||
<!-- TODO: add a bigger/better loading indicator -->
|
||||
<div class="loading-icon"></div>
|
||||
<div class="content-bounds grow">
|
||||
<div class="page" id="page">
|
||||
<div class="page-content" id="page-content"></div>
|
||||
<div class="page-loading-container">
|
||||
<!-- TODO: add a bigger/better loading indicator -->
|
||||
<div class="loading-icon"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer flex items-center flex-column">
|
||||
<div>
|
||||
<span class="size-h3">Glance</span> ({{ .App.Version }})
|
||||
{{ if not .App.Config.Branding.HideFooter }}
|
||||
<div class="footer flex items-center flex-column">
|
||||
{{ if eq "" .App.Config.Branding.CustomFooter }}
|
||||
<div>
|
||||
<a class="size-h3" href="https://github.com/glanceapp/glance" target="_blank" rel="noreferrer">Glance</a> {{ if ne "dev" .App.Version }}<a class="visited-indicator" title="Release notes" href="https://github.com/glanceapp/glance/releases/tag/{{ .App.Version }}" target="_blank" rel="noreferrer">{{ .App.Version }}</a>{{ else }}({{ .App.Version }}){{ end }}
|
||||
</div>
|
||||
{{ else }}
|
||||
{{ .App.Config.Branding.CustomFooter }}
|
||||
{{ end }}
|
||||
</div>
|
||||
<ul class="list-horizontal-text margin-top-5 size-h5 color-primary">
|
||||
<li><a href="https://github.com/glanceapp/glance/issues" target="_blank" rel="noreferrer">Report issue</a></li>
|
||||
<li><a href="https://github.com/glanceapp/glance/discussions" target="_blank" rel="noreferrer">Submit feedback</a></li>
|
||||
</ul>
|
||||
{{ end }}
|
||||
|
||||
<div class="mobile-navigation-offset"></div>
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
{{ end }}
|
||||
<a href="{{ .DiscussionUrl }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-7 margin-bottom-auto" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text margin-top-7">
|
||||
<li title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
|
||||
<li>{{ .Score | formatNumber }} points</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
{{ end }}
|
||||
<a href="{{ .DiscussionUrl }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-7" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text margin-top-7">
|
||||
<li title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
|
||||
<li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
|
||||
<li>{{ .Score | formatNumber }} points</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,23 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<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>
|
||||
<ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Releases }}
|
||||
<li>
|
||||
<div class="flex items-center gap-10">
|
||||
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ .NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
||||
{{ if $.ShowSourceIcon }}
|
||||
<img class="simple-icon release-source-icon" src="{{ .SourceIconURL }}" alt="" loading="lazy">
|
||||
{{ end }}
|
||||
</div>
|
||||
<ul class="list-horizontal-text">
|
||||
<li title="{{ $release.TimeReleased | formatTime }}" {{ dynamicRelativeTimeAttrs $release.TimeReleased }}>{{ $release.TimeReleased | relativeTime }}</li>
|
||||
<li>{{ $release.Version }}</li>
|
||||
{{ if gt $release.Downvotes 3 }}
|
||||
<li>{{ $release.Downvotes | formatNumber }} ⚠</li>
|
||||
<li {{ dynamicRelativeTimeAttrs .TimeReleased }}></li>
|
||||
<li>{{ .Version }}</li>
|
||||
{{ if gt .Downvotes 3 }}
|
||||
<li>{{ .Downvotes | formatNumber }} ⚠</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Releases) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<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>
|
||||
<li {{ dynamicRelativeTimeAttrs .CreatedAt }}></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
@@ -47,7 +47,7 @@
|
||||
<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>
|
||||
<li {{ dynamicRelativeTimeAttrs .CreatedAt }}></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list list-gap-2 min-width-0">
|
||||
|
||||
40
internal/assets/templates/rss-detailed-list.html
Normal file
40
internal/assets/templates/rss-detailed-list.html
Normal file
@@ -0,0 +1,40 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-24 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Items }}
|
||||
<li class="flex gap-15 items-start row-reverse-on-mobile thumbnail-parent">
|
||||
<div class="thumbnail-container rss-detailed-thumbnail">
|
||||
{{ if ne "" .ImageURL }}
|
||||
<img class="thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
|
||||
{{ else }}
|
||||
<svg class="scale-half hide-on-mobile" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<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>
|
||||
<div class="grow min-width-0">
|
||||
<a class="size-h3 color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap">
|
||||
<li {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
|
||||
<li class="min-width-0">
|
||||
<a class="block text-truncate" href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
{{ if ne "" .Description }}
|
||||
<p class="rss-detailed-description text-truncate-2-lines margin-top-10">{{ .Description }}</p>
|
||||
{{ end }}
|
||||
{{ if gt (len .Categories) 0 }}
|
||||
<ul class="attachments margin-top-10">
|
||||
{{ range .Categories }}
|
||||
<li>{{ . }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
</div>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li>{{ .NoItemsMessage }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
@@ -3,10 +3,11 @@
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if gt (len .Items) 0 }}
|
||||
<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">
|
||||
<div class="card rss-card-2 widget-content-frame thumbnail-parent">
|
||||
{{ if ne "" .ImageURL }}
|
||||
<img class="rss-card-2-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
|
||||
{{ else }}
|
||||
@@ -17,12 +18,15 @@
|
||||
<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>
|
||||
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
|
||||
<li class="min-width-0 text-truncate">{{ .ChannelName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="widget-content-frame padding-widget">{{ .NoItemsMessage }}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
{{ if gt (len .Items) 0 }}
|
||||
<div class="carousel-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">
|
||||
<div class="card widget-content-frame thumbnail-parent">
|
||||
{{ if ne "" .ImageURL }}
|
||||
<img class="rss-card-image thumbnail" loading="lazy" src="{{ .ImageURL }}" alt="">
|
||||
{{ else }}
|
||||
@@ -17,12 +18,15 @@
|
||||
<div class="margin-bottom-widget padding-inline-widget flex flex-column grow">
|
||||
<a href="{{ .Link }}" title="{{ .Title }}" class="text-truncate-3-lines color-primary-if-not-visited margin-top-10 margin-bottom-auto" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text flex-nowrap margin-top-7">
|
||||
<li class="shrink-0" title="{{ .PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
|
||||
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
|
||||
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
|
||||
<li class="min-width-0 text-truncate">{{ .ChannelName }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ else }}
|
||||
<div class="widget-content-frame padding-widget">{{ .NoItemsMessage }}</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
{{ range $i, $item := .Items }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Items }}
|
||||
<li>
|
||||
<a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li title="{{ $item.PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs $item.PublishedAt }}>{{ .PublishedAt | relativeTime }}</li>
|
||||
{{ if gt (len $.FeedRequests) 1 }}
|
||||
<li><a href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a></li>
|
||||
{{ end }}
|
||||
<ul class="list-horizontal-text flex-nowrap">
|
||||
<li {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
|
||||
<li class="min-width-0">
|
||||
<a class="block text-truncate" href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li>{{ .NoItemsMessage }}</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Items) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
24
internal/assets/templates/search.html
Normal file
24
internal/assets/templates/search.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="search widget-content-frame padding-inline-widget flex gap-15 items-center" data-default-search-url="{{ .SearchEngine }}" data-new-tab="{{ .NewTab }}">
|
||||
<div class="search-bangs">
|
||||
{{ range .Bangs }}
|
||||
<input type="hidden" data-shortcut="{{ .Shortcut }}" data-title="{{ .Title }}" data-url="{{ .URL }}">
|
||||
{{ end }}
|
||||
</div>
|
||||
|
||||
<div class="search-icon-container">
|
||||
<svg class="search-icon" stroke="var(--color-text-subdue)" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<input class="search-input" type="text" placeholder="Type here to search…" autocomplete="off"{{ if .Autofocus }} autofocus{{ end }}>
|
||||
|
||||
<div class="search-bang"></div>
|
||||
<kbd class="hide-on-mobile" title="Press [S] to focus the search input">S</kbd>
|
||||
</div>
|
||||
{{ end }}
|
||||
@@ -1,27 +1,31 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
{{ range $i, $channel := .Channels }}
|
||||
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<div class="{{ if $channel.IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container">
|
||||
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Channels }}
|
||||
<li>
|
||||
<div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-parent">
|
||||
<div class="twitch-channel-avatar-container">
|
||||
{{ if $channel.Exists }}
|
||||
<img class="twitch-channel-avatar thumbnail" src="{{ $channel.AvatarUrl }}" alt="" loading="lazy">
|
||||
{{ if .Exists }}
|
||||
<a href="https://twitch.tv/{{ .Login }}" class="twitch-channel-avatar-link" target="_blank" rel="noreferrer">
|
||||
<img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
|
||||
</a>
|
||||
{{ else }}
|
||||
<svg class="twitch-channel-avatar thumbnail" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
|
||||
</svg>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="shrink min-width-0">
|
||||
<a href="https://twitch.tv/{{ $channel.Login }}" class="size-h3{{ if $channel.IsLive }} color-highlight{{ end }} block text-truncate" target="_blank" rel="noreferrer">{{ $channel.Name }}</a>
|
||||
{{ if $channel.Exists }}
|
||||
{{ if $channel.IsLive }}
|
||||
<a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ $channel.CategorySlug }}" target="_blank" rel="noreferrer">{{ $channel.Category }}</a>
|
||||
<div class="min-width-0">
|
||||
<a href="https://twitch.tv/{{ .Login }}" class="size-h3{{ if .IsLive }} color-highlight{{ end }} block text-truncate" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
||||
{{ if .Exists }}
|
||||
{{ if .IsLive }}
|
||||
{{ if .Category }}
|
||||
<a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ .CategorySlug }}" target="_blank" rel="noreferrer">{{ .Category }}</a>
|
||||
{{ end }}
|
||||
<ul class="list-horizontal-text">
|
||||
<li title="{{ $channel.LiveSince | formatTime }}" {{ dynamicRelativeTimeAttrs $channel.LiveSince }}>{{ $channel.LiveSince | relativeTime }}</li>
|
||||
<li>{{ $channel.ViewersCount | formatViewerCount }} viewers</li>
|
||||
<li {{ dynamicRelativeTimeAttrs .LiveSince }}></li>
|
||||
<li>{{ .ViewersCount | formatViewerCount }} viewers</li>
|
||||
</ul>
|
||||
{{ else }}
|
||||
<div>Offline</div>
|
||||
@@ -34,7 +38,4 @@
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Channels) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
{{ template "widget-base.html" . }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<ul class="list list-gap-14 list-collapsible">
|
||||
{{ range $i, $category := .Categories }}
|
||||
{{ $shouldCollapseItem := shouldCollapse $i $.CollapseAfter }}
|
||||
<li class="twitch-category thumbnail-container{{ if $shouldCollapseItem }} list-collapsible-item{{ end }}" {{ if $shouldCollapseItem }}style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}>
|
||||
<div class="flex gap-10 items-center">
|
||||
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ $category.AvatarUrl }}" alt="">
|
||||
<div class="shrink min-width-0">
|
||||
<a class="size-h3 color-highlight text-truncate block" href="https://www.twitch.tv/directory/category/{{ $category.Slug }}" target="_blank" rel="noreferrer">{{ $category.Name }}</a>
|
||||
<ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
|
||||
{{ range .Categories }}
|
||||
<li class="twitch-category thumbnail-parent">
|
||||
<div class="flex gap-10 items-start">
|
||||
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ .AvatarUrl }}" alt="">
|
||||
<div class="min-width-0">
|
||||
<a class="size-h3 color-highlight text-truncate block" href="https://www.twitch.tv/directory/category/{{ .Slug }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
|
||||
<ul class="list-horizontal-text">
|
||||
<li>{{ $category.ViewersCount | formatViewerCount }} viewers</li>
|
||||
{{ if $category.IsNew }}
|
||||
<li>{{ .ViewersCount | formatViewerCount }} viewers</li>
|
||||
{{ if .IsNew }}
|
||||
<li class="color-primary">NEW</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
<ul class="list-horizontal-text flex-nowrap">
|
||||
{{ range $i, $tag := $category.Tags }}
|
||||
{{ range $i, $tag := .Tags }}
|
||||
{{ if eq $i 0 }}
|
||||
<li class="shrink-0">{{ $tag.Name }}</li>
|
||||
{{ else }}
|
||||
<li class="text-truncate shrink min-width-0">{{ $tag.Name }}</li>
|
||||
<li class="text-truncate min-width-0">{{ $tag.Name }}</li>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</ul>
|
||||
@@ -29,7 +28,4 @@
|
||||
</li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
{{ if gt (len .Categories) $.CollapseAfter }}
|
||||
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
<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">
|
||||
<li class="shrink-0" {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
|
||||
<li class="min-width-0">
|
||||
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
|
||||
|
||||
{{ define "widget-content" }}
|
||||
<div class="cards-grid">
|
||||
<div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
<div class="card widget-content-frame thumbnail-parent">
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="carousel-container">
|
||||
<div class="cards-horizontal carousel-items-container">
|
||||
{{ range .Videos }}
|
||||
<div class="card widget-content-frame thumbnail-container">
|
||||
<div class="card widget-content-frame thumbnail-parent">
|
||||
{{ template "video-card-contents" . }}
|
||||
</div>
|
||||
{{ end }}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<div class="widget widget-type-{{ .GetType }}">
|
||||
<div class="widget widget-type-{{ .GetType }}{{ if ne "" .CSSClass }} {{ .CSSClass }}{{ end }}">
|
||||
{{ if not .HideHeader}}
|
||||
<div class="widget-header">
|
||||
<div class="uppercase">{{ .Title }}</div>
|
||||
{{ if ne "" .TitleURL}}<a href="{{ .TitleURL }}" target="_blank" rel="noreferrer" class="uppercase">{{ .Title }}</a>{{ else }}<div class="uppercase">{{ .Title }}</div>{{ end }}
|
||||
{{ if and .Error .ContentAvailable }}
|
||||
<div class="notice-icon notice-icon-major" title="{{ .Error }}"></div>
|
||||
{{ else if .Notice }}
|
||||
<div class="notice-icon notice-icon-minor" title="{{ .Notice }}"></div>
|
||||
{{ end }}
|
||||
</div>
|
||||
<div class="widget-content {{ if .ContentAvailable }}{{ block "widget-content-classes" . }}{{ end }}{{ end }}">
|
||||
{{ end }}
|
||||
<div class="widget-content{{ if .ContentAvailable }} {{ block "widget-content-classes" . }}{{ end }}{{ end }}">
|
||||
{{ if .ContentAvailable }}
|
||||
{{ block "widget-content" . }}{{ end }}
|
||||
{{ else }}
|
||||
|
||||
Reference in New Issue
Block a user