Compare commits

...
33 Commits
Author SHA1 Message Date
Svilen Markov 21909e0bda Merge pull request #76 from fawni/feat/twitch-channels-sort
Add custom sorting to the twitch channels widget
2024-05-18 11:05:55 +01:00
Svilen Markov 5e9635a76f Change custom order to live 2024-05-18 11:01:34 +01:00
Svilen Markov 6627a635d5 Update build script 🤦‍♂️ 2024-05-18 10:44:52 +01:00
Svilen Markov 20740f33af Update build script 2024-05-18 10:37:14 +01:00
fawn 0681a04607 Add custom sorting to the twitch channels widget
this allows the channels to be sorted as defined in the config while
still keeping the live channels on the top. the wording could probably
be improved, "custom" is too broad but i'm not sure what else to call
it.
2024-05-18 03:07:34 +03:00
Svilen Markov 21d491843f Create SECURITY.md 2024-05-17 19:56:18 +01:00
Svilen Markov 566ac99a53 Move code of conduct 2024-05-17 19:55:34 +01:00
Svilen Markov fca504901f Create CODE_OF_CONDUCT.md 2024-05-17 19:31:03 +01:00
Svilen Markov e03231cbd7 Update time label 2024-05-17 19:28:15 +01:00
Svilen Markov 693051f4e9 Merge pull request #59 from tarikcoskun/main
Add 24-hour time format support for weather
2024-05-17 18:41:04 +01:00
Svilen Markov 06e891f9a9 Update doc 2024-05-17 18:37:35 +01:00
Svilen Markov a2bb1b88ea Remove unrelated units check 2024-05-17 18:37:11 +01:00
Svilen Markov 9602269f27 Merge pull request #49 from medinnna/main
Add web app support
2024-05-17 17:47:18 +01:00
Svilen Markov 27b9af6067 Don't transition all
Otherwise animates image dimensions when resizing from mobile to desktop
2024-05-17 17:45:50 +01:00
Svilen Markov d43c0749c1 Only apply box shadow on collapsible grids 2024-05-17 17:45:50 +01:00
Svilen Markov f4fca7600a Merge pull request #74 from brian-petersen/serve-error
Adds reporting serve errors
2024-05-17 17:31:10 +01:00
Brian Petersen 6134f1c812 Adds reporting serve errors 2024-05-17 10:11:00 -06:00
Svilen Markov 94cf82c3d9 Remove link to discussions 2024-05-17 12:21:44 +01:00
Svilen Markov b0018c3f06 Refactor, again
Make collapsible components reusable
Add ability to collapse video rows in videos-grid style
Lose sanity while dealing with all the intricacies
2024-05-17 12:15:44 +01:00
Svilen Markov 8977114806 Fix viewport offset after collapsing 2024-05-16 21:17:10 +01:00
Svilen Markov 36f8eac3e4 Refactor
Delay showing page content until JS has finished setting up page elements
That then allows the following:
Leave relative time to be rendered on the client
Leave collapsible lists to be rendered on the client
Which massively simplfies the backend templates which were error prone
2024-05-16 20:41:50 +01:00
Svilen Markov de965dace8 Increase delay before adding class 2024-05-14 16:38:54 +01:00
Svilen Markov f0ca2edb25 Reduce max width of weather columns 2024-05-14 16:36:04 +01:00
Svilen Markov 514d5f92d1 Add link to release notes 2024-05-14 14:33:24 +01:00
Svilen Markov 7e05f8a5eb Add carousel right cutoff by default
Prevents it from fading in when the page loads
2024-05-14 14:32:51 +01:00
Svilen Markov 48d4d4e966 Revert back to vh
dvh causes micro jumps when scrolling near the top of the page which looks glitchy
2024-05-14 06:28:42 +01:00
Tarık Coşkun 6a7b50b35f Add 24-hour time format support for weather 2024-05-13 15:08:58 +03:00
Svilen Markov f9ca93e0b0 Account for safe area in label offset 2024-05-12 14:45:13 +01:00
Svilen Markov 09b7d71103 Revert to using the small icon for the favicon
Bit too much whitespace in the app icon to be able to use it as a favicon, makes the G look quite small
2024-05-12 14:27:25 +01:00
Svilen Markov bc06578e53 Change implementation for the safe area 2024-05-12 14:20:10 +01:00
Carlos Medina 91c75518da Change mobile nav styles 2024-05-10 19:54:43 -06:00
Svilen Markov 461c984317 Check if background color is nil before setting 2024-05-09 04:05:25 +01:00
Carlos Medina ecbad7ce6c Add mobile web app support 2024-05-08 16:51:04 -06:00
27 changed files with 603 additions and 145 deletions
+128
View File
@@ -0,0 +1,128 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
glanceapp@duck.com.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
Community Impact Guidelines were inspired by [Mozilla's code of conduct
enforcement ladder](https://github.com/mozilla/diversity).
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
+9
View File
@@ -0,0 +1,9 @@
# Security Policy
## Supported Versions
Security updates will be applied to the latest as well as previous minor version release depending on severity and if applicable.
## Reporting a Vulnerability
Please report any suspected security vulnerabilities to [glanceapp@duck.com](mailto:glanceapp@duck.com) and do not disclose them publicly. You should receive a response within a few days and if confirmed the issue will be resolved as soon as possible.
+9
View File
@@ -647,6 +647,7 @@ Example:
```yaml ```yaml
- type: weather - type: weather
units: metric units: metric
hour-format: 12h
location: London, United Kingdom location: London, United Kingdom
``` ```
@@ -671,6 +672,7 @@ Each bar represents a 2 hour interval. The yellow background represents sunrise
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| location | string | yes | | | location | string | yes | |
| units | string | no | metric | | units | string | no | metric |
| hour-format | string | no | 12h |
| hide-location | boolean | no | false | | hide-location | boolean | no | false |
| show-area-name | boolean | no | false | | show-area-name | boolean | no | false |
@@ -680,6 +682,9 @@ The name of the city and country to fetch weather information for. Attempting to
##### `units` ##### `units`
Whether to show the temperature in celsius or fahrenheit, possible values are `metric` or `imperial`. Whether to show the temperature in celsius or fahrenheit, possible values are `metric` or `imperial`.
#### `hour-format`
Whether to show the hours of the day in 12-hour format or 24-hour format. Possible values are `12h` and `24h`.
##### `hide-location` ##### `hide-location`
Optionally don't display the location name on the widget. Optionally don't display the location name on the widget.
@@ -1063,6 +1068,7 @@ Preview:
| ---- | ---- | -------- | ------- | | ---- | ---- | -------- | ------- |
| channels | array | yes | | | channels | array | yes | |
| collapse-after | integer | no | 5 | | collapse-after | integer | no | 5 |
| sort-by | string | no | viewers |
##### `channels` ##### `channels`
A list of channels to display. A list of channels to display.
@@ -1070,6 +1076,9 @@ A list of channels to display.
##### `collapse-after` ##### `collapse-after`
How many channels are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse. How many channels are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
##### `sort-by`
Can be used to specify the order in which the channels are displayed. Possible values are `viewers` and `live`.
### Twitch top games ### Twitch top games
Display a list of games with the most viewers on Twitch. Display a list of games with the most viewers on Twitch.
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

+90 -40
View File
@@ -57,6 +57,14 @@
font-size: var(--font-size-h4); font-size: var(--font-size-h4);
} }
.page-content, .page.content-ready .page-loading-container {
display: none;
}
.page.content-ready > .page-content {
display: block;
}
.page-column-full .size-title-dynamic { .page-column-full .size-title-dynamic {
font-size: var(--font-size-h3); font-size: var(--font-size-h3);
} }
@@ -117,70 +125,76 @@
padding-top: var(--list-half-gap); 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 { from {
opacity: 0; opacity: 0;
transform: translateY(10px); transform: translateY(10px);
} }
} }
.list-collapsible-item { .expand-toggle-button {
display: none; font: inherit;
animation: listItemReveal 0.3s backwards; border: 0;
animation-delay: var(--animation-delay); cursor: pointer;
} display: block;
width: 100%;
.list-collapsible-label { text-align: left;
display: flex; color: var(--color-text-base);
align-items: center; text-transform: uppercase;
gap: 1rem; font-size: var(--font-size-h4);
padding: var(--widget-content-vertical-padding) 0; padding: var(--widget-content-vertical-padding) 0;
background: var(--color-widget-background); background: var(--color-widget-background);
} }
.list-collapsible-label:has(.list-collapsible-input:checked) { .expand-toggle-button.container-expanded {
position: sticky; 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 { .expand-toggle-button-icon {
display: block; display: inline-block;
margin-left: 1rem;
position: relative;
top: -.2rem;
} }
.list-collapsible-input { .expand-toggle-button-icon::before {
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 {
content: ''; content: '';
font-size: 0.8rem; font-size: 0.8rem;
transform: rotate(90deg); transform: rotate(90deg);
line-height: 1; line-height: 1;
display: inline-block;
transition: transform 0.3s; 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); transform: rotate(-90deg);
} }
.widget-content:has(.list-collapsible-label:last-child) { .widget-content:has(.expand-toggle-button:last-child) {
padding-bottom: 0; padding-bottom: 0;
} }
.cards-grid.collapsible-container + .expand-toggle-button {
text-align: center;
margin-top: 0.5rem;
background-color: var(--color-background);
}
/* required to prevent collapsed lazy images from being loaded while the container is being setup */
.collapsible-container:not(.ready) img[loading=lazy] {
display: none;
}
::selection { ::selection {
background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%))); background-color: hsl(var(--bghs), calc(var(--scheme) (var(--scheme) var(--bgl) + 20%)));
color: var(--color-text-highlight); color: var(--color-text-highlight);
@@ -268,6 +282,7 @@ body {
gap: var(--widget-gap); gap: var(--widget-gap);
margin: var(--widget-gap) 0; margin: var(--widget-gap) 0;
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
animation-delay: 3ms;
} }
@keyframes pageColumnsEntrance { @keyframes pageColumnsEntrance {
@@ -706,7 +721,7 @@ body {
flex-direction: column; flex-direction: column;
width: calc(100% / 12); width: calc(100% / 12);
padding-top: 3px; padding-top: 3px;
max-width: 3.5rem; max-width: 3rem;
} }
.weather-column-value, .weather-columns:hover .weather-column-value { .weather-column-value, .weather-columns:hover .weather-column-value {
@@ -866,8 +881,8 @@ body {
.thumbnail { .thumbnail {
filter: grayscale(0.2) contrast(0.9); filter: grayscale(0.2) contrast(0.9);
transition: all 0.2s;
opacity: 0.8; opacity: 0.8;
transition: filter 0.2s, opacity .2s;
} }
.thumbnail-container:hover .thumbnail { .thumbnail-container:hover .thumbnail {
@@ -1107,9 +1122,44 @@ body {
box-shadow: 0 calc(var(--spacing) * -1) 0 0 currentColor, 0 var(--spacing) 0 0 currentColor; 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); 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);
}
}
@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) { @media (max-width: 550px) {
@@ -1134,7 +1184,7 @@ body {
.mobile-reachability-header { .mobile-reachability-header {
display: block; display: block;
font-size: 3rem; font-size: 3rem;
padding: 10dvh 1rem; padding: 10vh 1rem;
text-align: center; text-align: center;
color: var(--color-text-highlight); color: var(--color-text-highlight);
animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards; animation: pageColumnsEntrance .3s cubic-bezier(0.25, 1, 0.5, 1) backwards;
+190 -19
View File
@@ -21,7 +21,7 @@ function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
}; };
async function fetchPageContents (pageSlug) { async function fetchPageContent(pageSlug) {
// TODO: handle non 200 status codes/time outs // TODO: handle non 200 status codes/time outs
// TODO: add retries // TODO: add retries
const response = await fetch(`/api/pages/${pageSlug}/content/`); const response = await fetch(`/api/pages/${pageSlug}/content/`);
@@ -33,8 +33,13 @@ async function fetchPageContents (pageSlug) {
function setupCarousels() { function setupCarousels() {
const carouselElements = document.getElementsByClassName("carousel-container"); const carouselElements = document.getElementsByClassName("carousel-container");
if (carouselElements.length == 0) {
return;
}
for (let i = 0; i < carouselElements.length; i++) { for (let i = 0; i < carouselElements.length; i++) {
const carousel = carouselElements[i]; const carousel = carouselElements[i];
carousel.classList.add("show-right-cutoff");
const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0]; const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
const determineSideCutoffs = () => { const determineSideCutoffs = () => {
@@ -56,7 +61,7 @@ function setupCarousels() {
itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited); itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
document.addEventListener("resize", determineSideCutoffsRateLimited); document.addEventListener("resize", determineSideCutoffsRateLimited);
determineSideCutoffs(); setTimeout(determineSideCutoffs, 1);
} }
} }
@@ -107,6 +112,8 @@ function setupDynamicRelativeTime() {
const updateInterval = 60 * 1000; const updateInterval = 60 * 1000;
let lastUpdateTime = Date.now(); let lastUpdateTime = Date.now();
updateRelativeTimeForElements(elements);
const updateElementsAndTimestamp = () => { const updateElementsAndTimestamp = () => {
updateRelativeTimeForElements(elements); updateRelativeTimeForElements(elements);
lastUpdateTime = Date.now(); lastUpdateTime = Date.now();
@@ -153,35 +160,199 @@ function setupLazyImages() {
image.classList.add("finished-transition"); image.classList.add("finished-transition");
} }
for (let i = 0; i < images.length; i++) { setTimeout(() => {
const image = images[i]; for (let i = 0; i < images.length; i++) {
const image = images[i];
if (image.complete) { if (image.complete) {
image.classList.add("cached"); image.classList.add("cached");
setTimeout(() => imageFinishedTransition(image), 5); setTimeout(() => imageFinishedTransition(image), 5);
} else { } else {
// TODO: also handle error event // TODO: also handle error event
image.addEventListener("load", () => { image.addEventListener("load", () => {
image.classList.add("loaded"); image.classList.add("loaded");
setTimeout(() => imageFinishedTransition(image), 500); setTimeout(() => imageFinishedTransition(image), 500);
}); });
}
} }
}, 5);
}
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";
}
list.classList.add("ready");
}
}
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");
}
}
};
setTimeout(() => {
cardsPerRow = getCardsPerRow();
resolveCollapsibleItems();
gridElement.classList.add("ready");
}, 1);
window.addEventListener("resize", () => {
const newCardsPerRow = getCardsPerRow();
if (cardsPerRow == newCardsPerRow) {
return;
}
cardsPerRow = newCardsPerRow;
resolveCollapsibleItems();
});
} }
} }
async function setupPage() { async function setupPage() {
const pageElement = document.getElementById("page"); const pageElement = document.getElementById("page");
const pageContents = await fetchPageContents(pageData.slug); const pageContentElement = document.getElementById("page-content");
const pageContent = await fetchPageContent(pageData.slug);
pageElement.innerHTML = pageContents; pageContentElement.innerHTML = pageContent;
setTimeout(() => { setTimeout(() => {
document.body.classList.add("animate-element-transition"); document.body.classList.add("animate-element-transition");
}, 150); }, 200);
setTimeout(setupLazyImages, 5); try {
setupCarousels(); setupLazyImages();
setupDynamicRelativeTime(); setupCarousels();
setupCollapsibleLists();
setupCollapsibleGrids();
setupDynamicRelativeTime();
} finally {
pageElement.classList.add("content-ready");
}
} }
if (document.readyState === "loading") { if (document.readyState === "loading") {
+13
View File
@@ -0,0 +1,13 @@
{
"name": "Glance",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "/static/app-icon.png",
"type": "image/png",
"sizes": "512x512"
}
]
}
+9 -1
View File
@@ -5,7 +5,15 @@
<title>{{ block "document-title" . }}{{ end }}</title> <title>{{ block "document-title" . }}{{ end }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <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="/static/app-icon.png">
<link rel="icon" type="image/png" sizes="50x50" href="/static/favicon.png">
<link rel="manifest" href="/static/manifest.json">
<link rel="icon" type="image/png" href="/static/favicon.png" /> <link rel="icon" type="image/png" href="/static/favicon.png" />
<link rel="stylesheet" href="/static/main.css?v={{ .App.Config.Server.StartedAt.Unix }}"> <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> <script async src="/static/main.js?v={{ .App.Config.Server.StartedAt.Unix }}"></script>
+12 -15
View File
@@ -1,14 +1,14 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $post := .Posts }} {{ range .Posts }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li>
<div class="forum-post-list-item thumbnail-container"> <div class="forum-post-list-item thumbnail-container">
{{ if $.ShowThumbnails }} {{ if $.ShowThumbnails }}
{{ if ne $post.ThumbnailUrl "" }} {{ if ne .ThumbnailUrl "" }}
<img class="forum-post-list-thumbnail thumbnail" src="{{ $post.ThumbnailUrl }}" alt="" loading="lazy"> <img class="forum-post-list-thumbnail thumbnail" src="{{ .ThumbnailUrl }}" alt="" loading="lazy">
{{ else if $post.HasTargetUrl }} {{ 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)"> <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" /> <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> </svg>
@@ -19,13 +19,13 @@
{{ end }} {{ end }}
{{ end }} {{ end }}
<div class="grow"> <div class="grow">
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a> <a href="{{ .DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li title="{{ $post.TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs $post.TimePosted }}>{{ $post.TimePosted | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li>{{ $post.Score | formatNumber }} points</li> <li>{{ .Score | formatNumber }} points</li>
<li>{{ $post.CommentCount | formatNumber }} comments</li> <li>{{ .CommentCount | formatNumber }} comments</li>
{{ if $post.HasTargetUrl }} {{ if .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 class="shrink min-width-0"><a class="visited-indicator text-truncate block" href="{{ .TargetUrl }}" target="_blank" rel="noreferrer">{{ .TargetUrlDomain }}</a></li>
{{ end }} {{ end }}
</ul> </ul>
</div> </div>
@@ -33,7 +33,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Posts) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}
+3 -5
View File
@@ -50,6 +50,7 @@
<div class="content-bounds"> <div class="content-bounds">
<div class="page" id="page"> <div class="page" id="page">
<div class="page-content" id="page-content"></div>
<div class="page-loading-container"> <div class="page-loading-container">
<!-- TODO: add a bigger/better loading indicator --> <!-- TODO: add a bigger/better loading indicator -->
<div class="loading-icon"></div> <div class="loading-icon"></div>
@@ -59,11 +60,8 @@
<div class="footer flex items-center flex-column"> <div class="footer flex items-center flex-column">
<div> <div>
<span class="size-h3">Glance</span> ({{ .App.Version }}) <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> </div>
<ul class="list-horizontal-text margin-top-5 size-h5 color-primary"> <a class="color-primary block margin-top-5 size-h5" href="https://github.com/glanceapp/glance/issues" target="_blank" rel="noreferrer">Report issue</a>
<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>
</div> </div>
{{ end }} {{ end }}
@@ -20,7 +20,7 @@
{{ end }} {{ 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> <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"> <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> <li>{{ .Score | formatNumber }} points</li>
</ul> </ul>
</div> </div>
@@ -19,7 +19,7 @@
{{ end }} {{ 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> <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"> <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> <li>{{ .Score | formatNumber }} points</li>
</ul> </ul>
</div> </div>
+3 -6
View File
@@ -1,12 +1,12 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-10 list-collapsible"> <ul class="list list-gap-10 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $release := .Releases }} {{ range $i, $release := .Releases }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li>
<a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a> <a class="size-h4 block text-truncate color-primary-if-not-visited" href="{{ $release.NotesUrl }}" target="_blank" rel="noreferrer">{{ .Name }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li title="{{ $release.TimeReleased | formatTime }}" {{ dynamicRelativeTimeAttrs $release.TimeReleased }}>{{ $release.TimeReleased | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs $release.TimeReleased }}></li>
<li>{{ $release.Version }}</li> <li>{{ $release.Version }}</li>
{{ if gt $release.Downvotes 3 }} {{ if gt $release.Downvotes 3 }}
<li>{{ $release.Downvotes | formatNumber }} ⚠</li> <li>{{ $release.Downvotes | formatNumber }} ⚠</li>
@@ -15,7 +15,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Releases) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}
+2 -2
View File
@@ -13,7 +13,7 @@
<div class="flex gap-7 size-h5 margin-top-3"> <div class="flex gap-7 size-h5 margin-top-3">
<ul class="list list-gap-2"> <ul class="list list-gap-2">
{{ range .RepositoryDetails.PullRequests }} {{ range .RepositoryDetails.PullRequests }}
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs .CreatedAt }}></li>
{{ end }} {{ end }}
</ul> </ul>
<ul class="list list-gap-2 min-width-0"> <ul class="list list-gap-2 min-width-0">
@@ -30,7 +30,7 @@
<div class="flex gap-7 size-h5 margin-top-3"> <div class="flex gap-7 size-h5 margin-top-3">
<ul class="list list-gap-2"> <ul class="list list-gap-2">
{{ range .RepositoryDetails.Issues }} {{ range .RepositoryDetails.Issues }}
<li title="{{ .CreatedAt | formatTime }}" {{ dynamicRelativeTimeAttrs .CreatedAt }}>{{ .CreatedAt | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs .CreatedAt }}></li>
{{ end }} {{ end }}
</ul> </ul>
<ul class="list list-gap-2 min-width-0"> <ul class="list list-gap-2 min-width-0">
@@ -17,7 +17,7 @@
<div class="rss-card-2-content padding-inline-widget"> <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> <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"> <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-0" {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li> <li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
</ul> </ul>
</div> </div>
@@ -17,7 +17,7 @@
<div class="margin-bottom-widget padding-inline-widget flex flex-column grow"> <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> <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"> <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-0" {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
<li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li> <li class="shrink min-width-0 text-truncate">{{ .ChannelName }}</li>
</ul> </ul>
</div> </div>
+4 -7
View File
@@ -1,12 +1,12 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $item := .Items }} {{ range .Items }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li>
<a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a> <a class="size-title-dynamic color-primary-if-not-visited" href="{{ .Link }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li title="{{ $item.PublishedAt | formatTime }}" {{ dynamicRelativeTimeAttrs $item.PublishedAt }}>{{ .PublishedAt | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs .PublishedAt }}></li>
{{ if gt (len $.FeedRequests) 1 }} {{ if gt (len $.FeedRequests) 1 }}
<li><a href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a></li> <li><a href="{{ .ChannelURL }}" target="_blank" rel="noreferrer">{{ .ChannelName }}</a></li>
{{ end }} {{ end }}
@@ -14,7 +14,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Items) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}
+12 -15
View File
@@ -1,13 +1,13 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $channel := .Channels }} {{ range .Channels }}
<li {{ if shouldCollapse $i $.CollapseAfter }}class="list-collapsible-item" style="--animation-delay: {{ itemAnimationDelay $i $.CollapseAfter }};"{{ end }}> <li>
<div class="{{ if $channel.IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container"> <div class="{{ if .IsLive }}twitch-channel-live {{ end }}flex gap-10 items-start thumbnail-container">
<div class="twitch-channel-avatar-container"> <div class="twitch-channel-avatar-container">
{{ if $channel.Exists }} {{ if .Exists }}
<img class="twitch-channel-avatar thumbnail" src="{{ $channel.AvatarUrl }}" alt="" loading="lazy"> <img class="twitch-channel-avatar thumbnail" src="{{ .AvatarUrl }}" alt="" loading="lazy">
{{ else }} {{ 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"> <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" /> <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" />
@@ -15,13 +15,13 @@
{{ end }} {{ end }}
</div> </div>
<div class="shrink min-width-0"> <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> <a href="https://twitch.tv/{{ .Login }}" class="size-h3{{ if .IsLive }} color-highlight{{ end }} block text-truncate" target="_blank" rel="noreferrer">{{ .Name }}</a>
{{ if $channel.Exists }} {{ if .Exists }}
{{ if $channel.IsLive }} {{ if .IsLive }}
<a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ $channel.CategorySlug }}" target="_blank" rel="noreferrer">{{ $channel.Category }}</a> <a class="text-truncate block" href="https://www.twitch.tv/directory/category/{{ .CategorySlug }}" target="_blank" rel="noreferrer">{{ .Category }}</a>
<ul class="list-horizontal-text"> <ul class="list-horizontal-text">
<li title="{{ $channel.LiveSince | formatTime }}" {{ dynamicRelativeTimeAttrs $channel.LiveSince }}>{{ $channel.LiveSince | relativeTime }}</li> <li {{ dynamicRelativeTimeAttrs .LiveSince }}></li>
<li>{{ $channel.ViewersCount | formatViewerCount }} viewers</li> <li>{{ .ViewersCount | formatViewerCount }} viewers</li>
</ul> </ul>
{{ else }} {{ else }}
<div>Offline</div> <div>Offline</div>
@@ -34,7 +34,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Channels) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}
@@ -1,22 +1,21 @@
{{ template "widget-base.html" . }} {{ template "widget-base.html" . }}
{{ define "widget-content" }} {{ define "widget-content" }}
<ul class="list list-gap-14 list-collapsible"> <ul class="list list-gap-14 collapsible-container" data-collapse-after="{{ .CollapseAfter }}">
{{ range $i, $category := .Categories }} {{ range .Categories }}
{{ $shouldCollapseItem := shouldCollapse $i $.CollapseAfter }} <li class="twitch-category thumbnail-container">
<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"> <div class="flex gap-10 items-center">
<img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ $category.AvatarUrl }}" alt=""> <img class="twitch-category-thumbnail thumbnail" loading="lazy" src="{{ .AvatarUrl }}" alt="">
<div class="shrink min-width-0"> <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> <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"> <ul class="list-horizontal-text">
<li>{{ $category.ViewersCount | formatViewerCount }} viewers</li> <li>{{ .ViewersCount | formatViewerCount }} viewers</li>
{{ if $category.IsNew }} {{ if .IsNew }}
<li class="color-primary">NEW</li> <li class="color-primary">NEW</li>
{{ end }} {{ end }}
</ul> </ul>
<ul class="list-horizontal-text flex-nowrap"> <ul class="list-horizontal-text flex-nowrap">
{{ range $i, $tag := $category.Tags }} {{ range $i, $tag := .Tags }}
{{ if eq $i 0 }} {{ if eq $i 0 }}
<li class="shrink-0">{{ $tag.Name }}</li> <li class="shrink-0">{{ $tag.Name }}</li>
{{ else }} {{ else }}
@@ -29,7 +28,4 @@
</li> </li>
{{ end }} {{ end }}
</ul> </ul>
{{ if gt (len .Categories) $.CollapseAfter }}
<label class="list-collapsible-label"><input type="checkbox" autocomplete="off" class="list-collapsible-input"></label>
{{ end }}
{{ end }} {{ end }}
@@ -3,7 +3,7 @@
<div class="margin-top-10 margin-bottom-widget flex flex-column grow padding-inline-widget"> <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> <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"> <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-0" {{ dynamicRelativeTimeAttrs .TimePosted }}></li>
<li class="shrink min-width-0"> <li class="shrink min-width-0">
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a> <a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
</li> </li>
+1 -1
View File
@@ -3,7 +3,7 @@
{{ define "widget-content-classes" }}widget-content-frameless{{ end }} {{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }} {{ define "widget-content" }}
<div class="cards-grid"> <div class="cards-grid collapsible-container" data-collapse-after-rows="{{ .CollapseAfterRows }}">
{{ range .Videos }} {{ range .Videos }}
<div class="card widget-content-frame thumbnail-container"> <div class="card widget-content-frame thumbnail-container">
{{ template "video-card-contents" . }} {{ template "video-card-contents" . }}
+6
View File
@@ -44,6 +44,12 @@ func (channels TwitchChannels) SortByViewers() {
}) })
} }
func (channels TwitchChannels) SortByLive() {
sort.SliceStable(channels, func(i, j int) bool {
return channels[i].IsLive && !channels[j].IsLive
})
}
type twitchOperationResponse struct { type twitchOperationResponse struct {
Data json.RawMessage Data json.RawMessage
Extensions struct { Extensions struct {
+1 -1
View File
@@ -36,7 +36,7 @@ func Main() int {
return 1 return 1
} }
if app.Serve() != nil { if err := app.Serve(); err != nil {
fmt.Printf("http server error: %v\n", err) fmt.Printf("http server error: %v\n", err)
return 1 return 1
} }
+11 -1
View File
@@ -14,6 +14,7 @@ type TwitchChannels struct {
ChannelsRequest []string `yaml:"channels"` ChannelsRequest []string `yaml:"channels"`
Channels []feed.TwitchChannel `yaml:"-"` Channels []feed.TwitchChannel `yaml:"-"`
CollapseAfter int `yaml:"collapse-after"` CollapseAfter int `yaml:"collapse-after"`
SortBy string `yaml:"sort-by"`
} }
func (widget *TwitchChannels) Initialize() error { func (widget *TwitchChannels) Initialize() error {
@@ -23,6 +24,10 @@ func (widget *TwitchChannels) Initialize() error {
widget.CollapseAfter = 5 widget.CollapseAfter = 5
} }
if widget.SortBy != "viewers" && widget.SortBy != "live" {
widget.SortBy = "viewers"
}
return nil return nil
} }
@@ -33,7 +38,12 @@ func (widget *TwitchChannels) Update(ctx context.Context) {
return return
} }
channels.SortByViewers() if widget.SortBy == "viewers" {
channels.SortByViewers()
} else if widget.SortBy == "live" {
channels.SortByLive()
}
widget.Channels = channels widget.Channels = channels
} }
+11 -6
View File
@@ -10,12 +10,13 @@ import (
) )
type Videos struct { type Videos struct {
widgetBase `yaml:",inline"` widgetBase `yaml:",inline"`
Videos feed.Videos `yaml:"-"` Videos feed.Videos `yaml:"-"`
VideoUrlTemplate string `yaml:"video-url-template"` VideoUrlTemplate string `yaml:"video-url-template"`
Style string `yaml:"style"` Style string `yaml:"style"`
Channels []string `yaml:"channels"` CollapseAfterRows int `yaml:"collapse-after-rows"`
Limit int `yaml:"limit"` Channels []string `yaml:"channels"`
Limit int `yaml:"limit"`
} }
func (widget *Videos) Initialize() error { func (widget *Videos) Initialize() error {
@@ -25,6 +26,10 @@ func (widget *Videos) Initialize() error {
widget.Limit = 25 widget.Limit = 25
} }
if widget.CollapseAfterRows == 0 || widget.CollapseAfterRows < -1 {
widget.CollapseAfterRows = 4
}
return nil return nil
} }
+11 -2
View File
@@ -14,17 +14,26 @@ type Weather struct {
Location string `yaml:"location"` Location string `yaml:"location"`
ShowAreaName bool `yaml:"show-area-name"` ShowAreaName bool `yaml:"show-area-name"`
HideLocation bool `yaml:"hide-location"` HideLocation bool `yaml:"hide-location"`
HourFormat string `yaml:"hour-format"`
Units string `yaml:"units"` Units string `yaml:"units"`
Place *feed.PlaceJson `yaml:"-"` Place *feed.PlaceJson `yaml:"-"`
Weather *feed.Weather `yaml:"-"` Weather *feed.Weather `yaml:"-"`
TimeLabels [12]string `yaml:"-"` TimeLabels [12]string `yaml:"-"`
} }
var timeLabels = [12]string{"2am", "4am", "6am", "8am", "10am", "12pm", "2pm", "4pm", "6pm", "8pm", "10pm", "12am"} var timeLabels12h = [12]string{"2am", "4am", "6am", "8am", "10am", "12pm", "2pm", "4pm", "6pm", "8pm", "10pm", "12am"}
var timeLabels24h = [12]string{"02:00", "04:00", "06:00", "08:00", "10:00", "12:00", "14:00", "16:00", "18:00", "20:00", "22:00", "00:00"}
func (widget *Weather) Initialize() error { func (widget *Weather) Initialize() error {
widget.withTitle("Weather").withCacheOnTheHour() widget.withTitle("Weather").withCacheOnTheHour()
widget.TimeLabels = timeLabels
if widget.HourFormat == "" || widget.HourFormat == "12h" {
widget.TimeLabels = timeLabels12h
} else if widget.HourFormat == "24h" {
widget.TimeLabels = timeLabels24h
} else {
return fmt.Errorf("invalid hour format '%s' for weather widget, must be either 12h or 24h", widget.HourFormat)
}
if widget.Units == "" { if widget.Units == "" {
widget.Units = "metric" widget.Units = "metric"
+65 -7
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"os" "os"
"os/exec" "os/exec"
@@ -77,7 +78,40 @@ var buildTargets = []buildTarget{
}, },
} }
func hasUncommitedChanges() (bool, error) {
output, err := exec.Command("git", "status", "--porcelain").CombinedOutput()
if err != nil {
return false, err
}
return len(output) > 0, nil
}
func main() { func main() {
flags := flag.NewFlagSet("", flag.ExitOnError)
specificTag := flags.String("tag", "", "Which tagged version to build")
err := flags.Parse(os.Args[1:])
if err != nil {
fmt.Println(err)
os.Exit(1)
}
uncommitedChanges, err := hasUncommitedChanges()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
if uncommitedChanges {
fmt.Println("There are uncommited changes - commit, stash or discard them first")
os.Exit(1)
}
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
@@ -95,10 +129,24 @@ func main() {
os.Mkdir(buildPath, 0755) os.Mkdir(buildPath, 0755)
os.Mkdir(archivesPath, 0755) os.Mkdir(archivesPath, 0755)
version, err := getVersionFromGit() var version string
if *specificTag == "" {
version, err := getVersionFromGit()
if err != nil {
fmt.Println(version, err)
os.Exit(1)
}
} else {
version = *specificTag
}
output, err := exec.Command("git", "checkout", "tags/"+version).CombinedOutput()
if err != nil { if err != nil {
fmt.Println(version, err) fmt.Println(string(output))
fmt.Println(err)
os.Exit(1) os.Exit(1)
} }
@@ -119,13 +167,19 @@ func main() {
fmt.Println("Building docker image") fmt.Println("Building docker image")
output, err := exec.Command( var dockerBuildOptions = []string{
"sudo", "docker", "build", "docker", "build",
"--platform=linux/amd64,linux/arm64,linux/arm/v7", "--platform=linux/amd64,linux/arm64,linux/arm/v7",
"-t", versionTag, "-t", versionTag,
"-t", latestTag, }
".",
).CombinedOutput() if !strings.Contains(version, "beta") {
dockerBuildOptions = append(dockerBuildOptions, "-t", latestTag)
}
dockerBuildOptions = append(dockerBuildOptions, ".")
output, err = exec.Command("sudo", dockerBuildOptions...).CombinedOutput()
if err != nil { if err != nil {
fmt.Println(string(output)) fmt.Println(string(output))
@@ -152,6 +206,10 @@ func main() {
os.Exit(1) os.Exit(1)
} }
if strings.Contains(version, "beta") {
return
}
output, err = exec.Command( output, err = exec.Command(
"sudo", "docker", "push", latestTag, "sudo", "docker", "push", latestTag,
).CombinedOutput() ).CombinedOutput()