Compare commits

...

637 commits
v0.1.0 ... main

Author SHA1 Message Date
Svilen Markov
03b616622e Fix bug when making single request in job 2025-04-14 01:07:04 +01:00
Svilen Markov
50e1da01fe Add newline 2025-04-13 17:40:30 +01:00
Svilen Markov
f8f50b26d8 Add mountpoint:info CLI command 2025-04-13 17:09:51 +01:00
Svilen Markov
a3bc133bcb Maybe fix for missing mountpoint info 2025-04-13 16:52:17 +01:00
Svilen Markov
50dae22ff4 Add restart: unless-stopped 2025-04-13 16:15:59 +01:00
Svilen Markov
5a91154eab Document function 2025-04-13 16:08:04 +01:00
Ralph Ocdol
d5b89d512a added unique to filter arrays with unique items in custom-api 2025-04-13 16:07:54 +01:00
Svilen Markov
97f43f88eb Only set title if attribute isn't set 2025-04-11 23:14:06 +01:00
Svilen Markov
3177af9524 Add rounded class 2025-04-11 22:34:53 +01:00
Svilen Markov
f99d22738c Add replaceMatches 2025-04-11 20:37:45 +01:00
Ralph Ocdol
ea7124db52 feat: custom-api's replaceAll with regex support 2025-04-11 20:37:36 +01:00
Lucas L.
b1247e5de6 Add filter on already seen links for RSS feeds 2025-04-11 20:34:47 +01:00
Svilen Markov
5ab962634e Fix sorting bug in twitch channels widget 2025-04-11 20:34:03 +01:00
Svilen Markov
d99944dcff Avoid spinning up unnecessary goroutines for single data jobs 2025-04-11 20:33:24 +01:00
Svilen Markov
920bcb06dc Print warnings when failing to get sensors 2025-04-11 19:34:49 +01:00
Svilen Markov
3679cc21ab Increase scale of everything on mobile 2025-04-10 00:44:55 +01:00
Svilen Markov
07e5626469 Add version & sensors:print cli commands 2025-04-10 00:39:08 +01:00
Svilen Markov
6bb6f87800 Fix diagnose command 2025-04-10 00:35:27 +01:00
Svilen Markov
cb6b52ff4c Bump go version 2025-04-10 00:34:53 +01:00
Svilen Markov
03c377dff3 Update dependencies 2025-04-10 00:14:40 +01:00
Svilen Markov
0e3f172032
Merge pull request #553 from Panonim/patch-1
Added Community Widgets Link to Readme.
2025-04-09 16:40:36 +01:00
Svilen Markov
fd704419d9 Update readme links 2025-04-09 16:39:53 +01:00
Artur Flis
32ac72f592
Update README.md 2025-04-02 19:57:01 +02:00
Svilen Markov
9c77b87435
Merge pull request #486 from daot/main
Make clickable area for docker-containers widget items bigger
2025-03-30 14:57:16 +01:00
Svilen Markov
d4d9e94f44 Make link hitbox take up full width 2025-03-30 14:53:59 +01:00
Svilen Markov
47cef2abf0
Merge pull request #499 from FranklyFuzzy/main
Added Dracula theme code and screenshot
2025-03-30 14:40:15 +01:00
Svilen Markov
0a5cdf380f
Merge pull request #528 from diceroll123/patch-1
Fix typo
2025-03-30 14:39:39 +01:00
Svilen Markov
9da7967158 Update docs 2025-03-29 18:00:36 +00:00
Svilen Markov
964744a9ae Add offsetNow function 2025-03-29 17:57:43 +00:00
Svilen Markov
779304d035 Allow skipping JSON check and add JSONLines 2025-03-29 13:36:22 +00:00
Svilen Markov
f15d7445bd Add concat function 2025-03-29 13:20:53 +00:00
Svilen Markov
26d68ba3fc Allow extension widget to specify title-url 2025-03-29 10:56:11 +00:00
Svilen Markov
958805a1fd Increase z-index of mobile nav
This fixes the carousel gradient side
being above it since it also has z-index 10
2025-03-29 10:49:37 +00:00
Svilen Markov
dd74c173a5 More custom-api additions/tweaks
* Remove need to convert to int for math stuff
* Add `now` and `duration` functions
2025-03-28 23:34:49 +00:00
Svilen Markov
bd020c93f5 Fix markets price precision 2025-03-28 14:11:17 +00:00
Steve C
75555f0426
Fix typo 2025-03-27 23:45:27 -04:00
Svilen Markov
233de7fc37 Add new functions to custom-api widget 2025-03-26 19:44:35 +00:00
Svilen Markov
c6e0230e5d Allow sending headers in extension widget 2025-03-26 17:59:41 +00:00
Svilen Markov
ca2732668e Update docs 2025-03-26 17:58:52 +00:00
Svilen Markov
456b76ac96 Allow specifying method and request body 2025-03-26 17:58:35 +00:00
Svilen Markov
a2c58bea82 Add parseRelativeTime function 2025-03-26 11:28:05 +00:00
Svilen Markov
7276b3d5ef Add allow-insecure to custom-api widget 2025-03-26 11:27:47 +00:00
Svilen Markov
1ef8315462 Only override extension url query if parameters property present 2025-03-26 11:27:12 +00:00
Svilen Markov
2f88ac419b Add parseTime to custom-api 2025-03-26 11:27:07 +00:00
Ralph Ocdol
cee7c85780 Add subrequests to custom-api (#385)
* feat: custom-api multiple API queries

* fix template check

* refactor

* Update implementation & docs

* Swap statement

* Update docs

---------

Co-authored-by: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com>
2025-03-26 11:26:16 +00:00
Svilen Markov
b2c5205035 Add support for bool in query params fields 2025-03-26 11:25:47 +00:00
Svilen Markov
2be59c3eb6 Fix failing to parse empty response body in custom api widget 2025-03-26 11:25:34 +00:00
Svilen Markov
176fc896ae Also apply to extension widget 2025-03-26 11:24:43 +00:00
Svilen Markov
a943555d86 Add note to docs 2025-03-26 11:24:27 +00:00
Svilen Markov
58bbfbbc42 Make query parameters field reusable 2025-03-26 11:24:15 +00:00
Ralph Ocdol
026b644630 feat: add parameters and array parameters support 2025-03-26 11:24:08 +00:00
FranklyFuzzy
1483b355af
Update themes.md
Added Dracula theme with screenshot
2025-03-23 14:16:09 -04:00
FranklyFuzzy
6a5bb635bb
Add files via upload 2025-03-23 14:14:54 -04:00
‮daot
0e0aca3844 Update docker-containers.html 2025-03-21 18:08:15 -05:00
Svilen Markov
1512718d41 Update readme
Added "Common issues"
2025-03-20 19:27:29 +00:00
Svilen Markov
4731b90899
Merge pull request #456 from slashformotion/slashformotion/refactor
refacto: remove duplicate
2025-03-17 20:49:10 +00:00
slashformotion
844e32a11e refacto: remove duplicate 2025-03-17 16:26:43 +01:00
Svilen Markov
0b2cfc45c1 Docker containers accessibility improvements 2025-03-17 02:06:32 +00:00
Svilen Markov
c8be41ce6c Reduce contrast of calendar dates 2025-03-17 00:45:23 +00:00
Svilen Markov
ec2f710d06 Use constant for the prefix 2025-03-16 23:17:07 +00:00
Svilen Markov
71112173b9 Fix title link not opening in new tab 2025-03-16 23:15:19 +00:00
Svilen Markov
1615c20e66 Change custom api int64 types to int
Some of Go's native template functions return int and
having to juggle between int and int64 might get messy
so we'll try to stick to having a single int type
2025-03-16 01:32:35 +00:00
Svilen Markov
fbcea12786 Add more info to logged message 2025-03-16 01:25:13 +00:00
Svilen Markov
047d13afd1 Fix summary triangle showing on Safari 2025-03-16 01:24:56 +00:00
Svilen Markov
4c1165533c Define service strings as consts to avoid forgetting to change them in all places 2025-03-15 19:41:08 +00:00
Svilen Markov
14bdcf71fe Remove debug code 2025-03-15 19:10:21 +00:00
Svilen Markov
1b3f022b2d Rename service to reduce ambiguity 2025-03-15 19:03:48 +00:00
Svilen Markov
b6d93a7f09
Merge pull request #371 from KallanX/enhancement/add-support-pihole-v6
Enhancement/add support pihole v6
2025-03-15 18:45:22 +00:00
Svilen Markov
4b2438c298 Refactor DNS stats widget 2025-03-15 18:42:58 +00:00
Svilen Markov
3b79c8e09f Remove symbol-link-template 2025-03-15 10:27:39 +00:00
Svilen Markov
d7bbf2b8e2 Accessibility improvements 2025-03-12 18:01:35 +00:00
Svilen Markov
b8df34309f Add note to docs about page width 2025-03-12 18:01:35 +00:00
Svilen Markov
278fa3c397 Add gap between domain and percent 2025-03-12 18:01:35 +00:00
Svilen Markov
6f48ee98e5 Uppercase currency before looking for matching symbol #365 2025-03-12 18:01:35 +00:00
Svilen Markov
774b0c104b
Merge pull request #399 from KadoBOT/patch-1
Update configuration.md
2025-03-08 09:46:37 +00:00
Ricardo Ambrogi
fbd4d9a74e
Update configuration.md
Correct `title` to `name`.

I'm unsure if `title` is also accepted, I haven't tested it. Please close this PR if this fix is not valid.
2025-03-07 09:07:10 +01:00
Svilen Markov
3f2fefe4f7
Merge pull request #382 from legoraft/main
Added gruvbox theme
2025-03-01 22:44:22 +00:00
Legoraft
fb8513bc9f
Added gruvbox theme code and screenshot 2025-03-01 14:19:36 +01:00
Legoraft
e4ec958edb
Added gruvbox screenshot 2025-03-01 14:17:17 +01:00
Keith Carichner Jr
78ad88a245
Merge pull request #1 from ralphocdol/fix-graph
Fix Graph and move SID to header
2025-02-27 10:33:20 -05:00
Ralph Ocdol
99660aeee8 Fix Graph and move SID to header 2025-02-27 23:02:10 +08:00
Keith Carichner Jr
65a412eb59 Pushing latest. Add ability to fetch series data from new api. 2025-02-26 16:20:43 -05:00
Keith Carichner Jr
2002ed1c9c Pushing latest changes. 2025-02-24 13:52:27 -05:00
Svilen Markov
488a1f6070 Update link to point to repo instead of pkg.go.dev 2025-02-22 13:32:50 +00:00
Svilen Markov
1e12d937aa
Merge pull request #372 from DavisYe/main
Correct document
2025-02-22 12:58:56 +00:00
Keith Carichner Jr
e643a44b59 Pushing latest fixes to handle the two different JSON responses. 2025-02-20 14:54:11 -05:00
DavisYe
7319870289 Correct the docker compose environment format of GITHUB_TOKEN in the configuration document. 2025-02-20 21:30:34 +08:00
Keith Carichner Jr
72c1ebf66d Minor README adjustment. 2025-02-19 23:23:22 -05:00
Keith Carichner Jr
008c4a3ab7 Removing SID check func. 2025-02-19 23:04:45 -05:00
Keith Carichner Jr
38d3d11571 Attempting to add support for Pi-hole v6. 2025-02-19 17:30:14 -05:00
Svilen Markov
d4565acfe7 Markets widget rate limit fix 2025-02-19 02:25:07 +00:00
Svilen Markov
16129c53bd
Merge pull request #358 from rubiojr/rubiojr/reload-on-rename2
Auto-reload config file on RENAME
2025-02-19 01:25:08 +00:00
Svilen Markov
cbf1961510 Don't try to get sensor info on openbsd 2025-02-17 23:48:16 +00:00
Svilen Markov
c76a4d4be7 Increase docker containers widget timeout 2025-02-17 23:45:57 +00:00
Svilen Markov
27af0400c0 Tweak impl for handling config renames 2025-02-17 22:28:10 +00:00
Sergio Rubio
76a80ff034 Add clarifying comment 2025-02-17 19:17:49 +01:00
Sergio Rubio
f7f333ad52 Auto-reload config file on RENAME
Some editors (like Vim), create a temp file when saving, then replace
the file being edited with the temp file. This causes the FS notify
event to be RENAME, not WRITE, so auto-reload misses this.

In addition to that, the file is removed from the watcher and the
auto-reload functionality stops working entirely after the first RENAME.

https://github.com/fsnotify/fsnotify/issues/255 describes this.
2025-02-17 19:08:54 +01:00
Svilen Markov
0ce45e32aa Update dashboard icons repo 2025-02-15 14:59:20 +00:00
Svilen Markov
abeb11c8a6 Add hide-mountpoints-by-default prop for server-stats 2025-02-15 14:43:25 +00:00
Svilen Markov
e01af4adec Delay changing popover display
Previously would make the popover visible and then reposition
it on the next frame in order to avoid getting called recursively
due to the observer, however this causes the scrollbar to appear
if it wasn't already visible for a single frame which is janky.

This change fixes that.
2025-02-15 13:53:13 +00:00
Svilen Markov
3043a0bd15 Rework getting host info (#340) 2025-02-15 13:46:58 +00:00
Svilen Markov
232cab01f8 Fix typo 2025-02-10 23:15:12 +00:00
Svilen Markov
b301953249 Update docs 2025-02-10 20:38:50 +00:00
Svilen Markov
bb9cb03c8a Fix index out of range 2025-02-10 11:22:48 +00:00
Svilen Markov
d8a4d39849
Merge pull request #330 from glanceapp/release/v0.7.0
Release/v0.7.0
2025-02-09 19:07:48 +00:00
Svilen Markov
0d6966726e Update pull request template 2025-02-09 18:45:23 +00:00
Svilen Markov
bb26725c67 Include pkg dir when building docker image 2025-02-09 18:21:23 +00:00
Svilen Markov
103fe5718b Tidy mod 2025-02-09 18:15:50 +00:00
Svilen Markov
a0d1bf1788 Bump versions 2025-02-09 18:04:25 +00:00
Svilen Markov
37f35281b4 Add server-stats widget 2025-02-09 17:52:22 +00:00
Svilen Markov
306fb3cb33 Add new calendar and deprecate old 2025-02-09 17:47:20 +00:00
Svilen Markov
24a6107171 Update config docs 2025-02-09 17:33:20 +00:00
Svilen Markov
e495fbfd99 Remove comment 2025-02-09 17:33:07 +00:00
Svilen Markov
da6fbc4995 Update custom-api doc 2025-02-09 17:32:55 +00:00
Svilen Markov
8d673e1229 Update readme 2025-02-09 17:32:43 +00:00
Svilen Markov
d8c7d02f5e Add custom-api examples 2025-02-09 05:33:08 +00:00
Svilen Markov
8331c4b305 Add Exists function 2025-02-09 05:32:30 +00:00
Svilen Markov
5a06f4788b Return NaN instead of 0 when dividing by 0 2025-02-09 05:32:05 +00:00
Svilen Markov
827171e206 Add upgrade guide 2025-02-09 03:04:29 +00:00
Svilen Markov
c3f3fa9876 Update readme 2025-02-09 00:40:19 +00:00
Svilen Markov
825ef84da5 Add comment 2025-02-09 00:39:51 +00:00
Svilen Markov
4445d46f61 Add example glance.yml to docs 2025-02-09 00:39:36 +00:00
Svilen Markov
c583057d45 Allow specifying playlists separately 2025-02-09 00:31:53 +00:00
Svilen Markov
982f3ee2a1 Add issue templates 2025-02-07 11:11:58 +00:00
Svilen Markov
cd1e4e9f91 Update update warn link 2025-02-07 11:06:44 +00:00
Svilen Markov
33f8e6d144 Update notice page 2025-02-07 11:06:20 +00:00
Svilen Markov
af70cc83ba Add preserve-order and limit props to RSS widget 2025-02-07 11:05:53 +00:00
Svilen Markov
8b731fd9e5 Invert colors in docs image 2025-02-07 11:01:46 +00:00
Svilen Markov
a0c697b982 Only ignore glance*.yml in root dir 2025-02-07 11:00:58 +00:00
Svilen Markov
21a6fe407b Remove println 2025-02-04 03:55:41 +00:00
Svilen Markov
1c1a7b3e27 Change label from title to name 2025-02-02 13:02:59 +00:00
Svilen Markov
61fd5481cc Update custom API 2025-02-02 13:02:46 +00:00
Svilen Markov
d400b27545 Fix observer console errors 2025-01-22 16:59:10 +00:00
Svilen Markov
ec9d455d18
Merge pull request #307 from Mystically11/add-prereleases-to-releases
feat: Add option to include GitHub repository pre-releases in releases widget
2025-01-17 20:18:47 +00:00
Svilen Markov
8a75f303bb Remove extra whitespace 2025-01-17 20:16:55 +00:00
Svilen Markov
0ce706e02e Update releases widget 2025-01-17 20:14:07 +00:00
Svilen Markov
3076593021 Merge remote-tracking branch 'upstream/release/v0.7.0' into add-prereleases-to-releases 2025-01-17 19:35:46 +00:00
Svilen Markov
a83389bb7d
Merge pull request #314 from JeckDev/release/v0.7.0
feat: add alternate link (error-url) for monitor widget sites
2025-01-17 19:10:41 +00:00
Svilen Markov
ee1cde8a04 Update monitor widget 2025-01-17 19:09:49 +00:00
Svilen Markov
a9efe88461 Revert points abbreviation 2025-01-15 07:49:08 +00:00
Jeck
c8200f2972 Added error-url parameter to monitor sites 2025-01-14 21:32:04 -08:00
Svilen Markov
80b43d3e0c Further simplify points abbreviation implementation 2025-01-14 11:58:26 +00:00
Svilen Markov
a7e235441b Simplify points abbreviation implementation 2025-01-14 11:55:52 +00:00
Svilen Markov
bb80637e72 Only hide comment count if there's a target URL 2025-01-14 11:35:02 +00:00
Svilen Markov
8fb5de4e87 Remove whitespace in template 2025-01-14 10:48:35 +00:00
Svilen Markov
1c03f0a07a Update forum post templates
* Use approx number for points & comments
* Hide comments when limited on horizontal space
* Abbreviate "points" to "pts" when limited on horizontal space
* Don't let domain wrap
2025-01-14 10:41:46 +00:00
Svilen Markov
b4ac96ccaf Remove whitespace in template & increase popover max width 2025-01-13 10:47:55 +00:00
Svilen Markov
5723fbdea5 Add markets widget link templates & automatic name 2025-01-12 04:21:47 +00:00
Svilen Markov
260bc15577 Remove reddit widget title forward slash prefix 2025-01-11 22:31:45 +00:00
Svilen Markov
59bfe3e835 Add vertical-list style for videos widget 2025-01-11 19:48:06 +00:00
Svilen Markov
086fac4120 Fix monitor widget showing incorrect status icon 2025-01-11 17:59:07 +00:00
Mystically
46bc36ad4a oops 2025-01-09 21:20:11 +01:00
Mystically
ed477e8cc6 add option to include github prereleases 2025-01-09 21:19:13 +01:00
Svilen Markov
8b878149d4 Update docs 2025-01-09 17:17:56 +00:00
Svilen Markov
db512419ea Add new text color class 2025-01-07 15:34:58 +00:00
Svilen Markov
491bc65a38 Add target property on bookmarks widget 2025-01-07 10:45:54 +00:00
Svilen Markov
94ec286de6 Add codeblock highlighting 2025-01-06 21:28:53 +00:00
Svilen Markov
108c83588c Allow using a standard HTTP proxy in reddit widget 2025-01-06 20:40:46 +00:00
Svilen Markov
abbb4950a5 Fix HTML entities in RSS item titles 2025-01-04 07:49:29 +00:00
Svilen Markov
0fe0b34898
Merge pull request #298 from hecht-a/add_search_placeholder
feat: add possibility to set search input placeholder
2025-01-04 03:47:51 +00:00
HECHT Axel
8ad7d4be0a
feat: add possibility to set search input placeholder 2025-01-03 19:36:43 +01:00
Svilen Markov
ac7f3805d4 Add custom unmarshalling for pihole queries stats
+ hide-graph and hide-top-domains options for DNS stats widget
2024-12-28 14:17:48 +00:00
Svilen Markov
e524dd111e
Merge pull request #286 from DallasHoff/mobile-style-fixes-0.7.0
Mobile style fixes
2024-12-26 17:04:21 +00:00
Svilen Markov
4a14709f13 Tweak iOS fixes 2024-12-26 17:02:15 +00:00
Svilen Markov
85b8ee493e Update dependencies 2024-12-26 13:24:40 +00:00
Svilen Markov
e546488aeb Fix broken reddit video & nsfw post thumbnails 2024-12-26 13:20:03 +00:00
Svilen Markov
0702e0abd3 Fix broken reddit video thumbnail URLs 2024-12-26 13:16:38 +00:00
Dallas Hoffman
29318005b2 Mobile style fixes 2024-12-24 14:42:28 -05:00
Svilen Markov
b4b61c94d7 Merge branch 'main' into release/v0.7.0 2024-12-22 14:34:11 +00:00
Svilen Markov
f18620d890 Add note about extension URL query parameters 2024-12-22 14:32:56 +00:00
Svilen Markov
eb86c3fb79 Allow setting same-tab and hide-arrow on bookmark groups 2024-12-22 14:22:11 +00:00
Svilen Markov
f89fc2ee1d Preserve formatting of non-html extension widget content 2024-12-22 13:44:41 +00:00
Svilen Markov
495eaa0a37 Add Widget-Content-Frameless option to extension widget 2024-12-22 13:31:26 +00:00
Svilen Markov
b44b7b80ff Don't use sprintf for relative time attribute 2024-12-20 11:41:48 +00:00
Svilen Markov
c8ff5362a3 Update dependencies 2024-12-19 23:05:47 +00:00
Svilen Markov
d575a0a29f Remove console.log 2024-12-18 12:47:30 +00:00
Svilen Markov
2e1134fdfb Properly handle future timestamps for relative time 2024-12-18 11:30:17 +00:00
Svilen Markov
ab6ae15836 Add automatic native titles to all truncateable elements 2024-12-17 23:34:32 +00:00
Svilen Markov
a4840c7365 Remove unnecessary variables 2024-12-17 00:30:34 +00:00
Svilen Markov
dbcc13a5cf Allow inserting env variables anywhere in the config 2024-12-16 23:59:25 +00:00
Svilen Markov
8d2639b349 Allow RSS feed URLs to be specified by env variables 2024-12-16 14:08:47 +00:00
Svilen Markov
889b034a79 Rename struct 2024-12-16 14:07:37 +00:00
Svilen Markov
e49145023f Don't escape user-defined URLs 2024-12-16 14:05:52 +00:00
Svilen Markov
ae1fb05607 Better convention for safe template types 2024-12-16 13:58:58 +00:00
Svilen Markov
80a957bc5b Change extension user-defined title to take precedence 2024-12-15 15:04:33 +00:00
Svilen Markov
24f93d8258
Merge pull request #275 from glanceapp/release/v0.6.3
Release/v0.6.3
2024-12-11 18:56:48 +00:00
Svilen Markov
01016e08d2 Merge branch 'main' into release/v0.6.3 2024-12-11 18:53:40 +00:00
Svilen Markov
949fde1517 Fix edge case in weather widget graph 2024-12-11 18:50:34 +00:00
Svilen Markov
804cf9916b Change simple icons provider and always use latest version 2024-12-11 18:47:30 +00:00
Svilen Markov
ef24680baf Fix overlap issues in weather and dns-stats widgets 2024-12-11 18:38:09 +00:00
Svilen Markov
d4b1d240b9 Fix releasing page mutex during template execution 2024-12-08 14:55:53 +00:00
Svilen Markov
448c795982 Properly close code block in docs 2024-12-07 16:47:58 +00:00
Svilen Markov
7f667e2d1c Allow using env variables in monitor and bookmarks widget URLs 2024-12-07 16:40:09 +00:00
Svilen Markov
a1afdb0c63 Allow digits in env variable names 2024-12-07 16:17:09 +00:00
Svilen Markov
ee1d7599d5 Add note to preconfigured pages doc 2024-12-07 09:28:36 +00:00
Svilen Markov
8adad18198 Move variable definition outside of closure 2024-12-07 09:20:01 +00:00
Svilen Markov
49a0758aae Fix trying to load incorrect thumbnail if post marked nsfw 2024-12-07 08:57:51 +00:00
Svilen Markov
e3bdc73013 Allow specifying playlist in videos widget
Co-authored-by: vishalkadam47 <vishal@jeevops.com>
2024-12-06 10:54:14 +00:00
Svilen Markov
1922e1e895 Increase RSS widget workers 2024-12-06 10:54:14 +00:00
Svilen Markov
7ee8e7c8d2
Merge pull request #242 from CremaLuca/main
feat: HEALTHCHECK in Dockerfile
2024-12-05 13:33:01 +00:00
Svilen Markov
e5106c0704
Merge branch 'release/v0.7.0' into main 2024-12-05 13:32:16 +00:00
Svilen Markov
80331aa217 Also add healthcheck to the other Dockerfile 2024-12-05 13:31:25 +00:00
Svilen Markov
84537e6884 Update status icons 2024-12-04 08:19:24 +00:00
Svilen Markov
f7062115bc Add support for selfhst icons 2024-12-04 08:11:18 +00:00
Svilen Markov
24f9f20eb7 Reduce list gap 2024-12-03 22:11:51 +00:00
Svilen Markov
217c6f2ed0 Remove container created time 2024-12-03 20:13:23 +00:00
Svilen Markov
82490ef5dd
Merge pull request #261 from baranovskis/release/v0.7.0
Docker widget
2024-12-03 19:10:15 +00:00
Svilen Markov
fcda017c39 Update docker containers widget 2024-12-03 19:04:43 +00:00
Svilen Markov
0c36925783 Fix off by 1px popover triangle 2024-12-03 16:52:28 +00:00
Svilen Markov
fcb67e62c5 Change widget error header icon to SVG 2024-12-03 16:51:33 +00:00
Svilen Markov
0651a2886e Merge latest changes 2024-12-03 13:59:20 +00:00
Svilen Markov
d876f4cb39 Add allow-insecure for dns-stats widget 2024-11-30 14:24:56 +00:00
Svilen Markov
b5259d1a98 Rename variables & interface 2024-11-30 14:24:19 +00:00
Svilen Markov
ffe053ffc5 Even more config watcher fixes 2024-11-30 12:54:19 +00:00
Svilen Markov
1785af4749 More config watcher fixes 2024-11-30 12:37:56 +00:00
Svilen Markov
98b4b7330e Further fixes for config file watcher 2024-11-30 12:23:57 +00:00
Svilen Markov
f68e5ae9ef Optimize cache control header assignment in file server 2024-11-30 11:16:07 +00:00
Svilen Markov
b3e73ce86a Move log outside of func 2024-11-30 10:59:58 +00:00
Svilen Markov
02cbb5f812 Fix CS 2024-11-30 10:48:33 +00:00
Svilen Markov
1d9ae72c81 Rename variables 2024-11-29 22:58:05 +00:00
Svilen Markov
d6470ae814 Somewhat working fix for config watcher 2024-11-29 21:45:50 +00:00
Svilen Markov
a816d1a913 Update error messages 2024-11-29 21:12:10 +00:00
Svilen Markov
2c03316f86 Update error 2024-11-29 21:06:58 +00:00
Svilen Markov
d19214a390 Update error messages 2024-11-29 20:52:09 +00:00
Svilen Markov
03035d1a2d Move template vars 2024-11-29 20:45:33 +00:00
Svilen Markov
6886716e67 Add comment 2024-11-29 18:28:10 +00:00
Svilen Markov
6165308c23 Fix random overlapping issues after browser update... wtf? 2024-11-29 17:34:25 +00:00
Svilen Markov
77a9469ff8 Add console message about new config location 2024-11-29 17:11:07 +00:00
Svilen Markov
2dce9b4c48 Listen on all interfaces 2024-11-29 17:03:52 +00:00
Svilen Markov
74e05763f7 Re-add tzdata 2024-11-29 16:45:18 +00:00
Svilen Markov
a4185fde07 Rename file 2024-11-29 16:41:15 +00:00
Svilen Markov
ebb519e6d8 Capitalize console messages 2024-11-29 16:38:16 +00:00
Svilen Markov
90fbba600f Restructure & refactor codebase 2024-11-29 16:34:15 +00:00
Svilen Markov
4bd4921131
Merge pull request #267 from xendke/calendar-start-day
Optionally start calendar weeks on Sunday
2024-11-26 17:02:26 +00:00
Juan Xavier Gomez
44ee813c6f simplify cal weekday index 2024-11-25 15:49:28 -05:00
Juan Xavier Gomez
3e467c5021 optionally start calendar weeks on sunday 2024-11-25 13:04:52 -05:00
Svilen Markov
2b0dd3ab99 Refactor 1/2 + new stuff
* Refactor CLI
* Add config:print command
* Add diagnose command
* Allow including other files in config
* Watch for file changes and automatically restart server
2024-11-24 15:39:14 +00:00
Andrejs Baranovskis
2618346a32 Rewrote docker client to reduce dependencies ♻️ 2024-11-20 17:03:37 +01:00
Andrejs Baranovskis
eacbb14279 Added docker widget with documentation 2024-11-19 17:58:55 +01:00
Andrejs Baranovskis
c8570d07ef Added debugging Dockerfile 2024-11-19 17:31:42 +01:00
Svilen Markov
7e345dd1f9 Add comment 2024-11-17 10:18:11 +00:00
Svilen Markov
afe6ad6bfc Don't focus new tab on middle click
This is more in line with how the browser behaves on middle click
2024-11-17 10:06:54 +00:00
Svilen Markov
af4c1e8514 Prevent default on middle click 2024-11-17 09:57:46 +00:00
Svilen Markov
d4fecd1dd8 Fix middle mouse click 2024-11-17 09:49:56 +00:00
Svilen Markov
79779eb721 Restore title-url functionality on group titles 2024-11-17 09:36:46 +00:00
Svilen Markov
6af666b58c Reduce margin 2024-11-16 15:00:05 +00:00
Svilen Markov
42872136a0 Reduce gap 2024-11-16 11:52:09 +00:00
Svilen Markov
aca7b2ac5f Reduce status icon size 2024-11-16 11:45:23 +00:00
Svilen Markov
a646f19b3f Make monitor widget compact style title size dynamic 2024-11-16 11:25:29 +00:00
Svilen Markov
9c5298aebf Add compact style for monitor widget 2024-11-16 11:10:46 +00:00
Svilen Markov
c6b07852fe Clear search input on submission 2024-11-16 08:29:58 +00:00
Svilen Markov
40d3053df9
Merge pull request #249 from ralphocdol/default-expand-mobile-navigation
Default expand mobile navigation
2024-11-16 07:21:25 +00:00
Svilen Markov
5e576a58e9
Merge branch 'release/v0.7.0' into default-expand-mobile-navigation 2024-11-16 07:20:49 +00:00
Svilen Markov
4d63b5dda1 Update docs 2024-11-16 07:17:06 +00:00
Svilen Markov
c1efd3f68b Fix formatting 2024-11-16 07:15:26 +00:00
Svilen Markov
2841cc67e8
Merge pull request #255 from 2q2code/main
Add Dashboard Icons support
2024-11-16 07:00:00 +00:00
Svilen Markov
becf34b0d9
Merge branch 'release/v0.7.0' into main 2024-11-16 06:49:11 +00:00
Svilen Markov
1bba915aef Update docs 2024-11-16 06:37:09 +00:00
Svilen Markov
f7239137d6 Update icons implementation to use custom type 2024-11-16 06:37:05 +00:00
Doc Em
d656986413 Add DashboardIcon (PNG) Support
- Refactored to make better naming sense
- Added capability to select between the PNG and SVG offerings of DashboardIcons
2024-11-14 20:06:54 +11:00
2Q2C0DE
d0c4e9d846 Add Dashboard Icons prefix support
- defined new type IconSource and constants, presently supporting:
  - LocalFile
  - SimpleIcon
  - DashboardIcon
- added new field to bookmarks and monitors to hold IconSource
- adjusted IsSimpleIcon to get truthiness from IconSource field
- generalised `toSimpleIconIfPrefixed` into `toRemoteResourceIconIfPrefixed`,
  adding support for `walkxcode`'s dashboard icons via CDN (svg)
2024-11-14 18:30:41 +11:00
Svilen Markov
bacb607d90 Allow using env variables in custom API URL 2024-11-03 04:46:24 +00:00
Svilen Markov
3914e24b3d Update twitch channel stream preview popover 2024-11-01 22:57:34 +00:00
Svilen Markov
4582be1da5 Allow changing horizontal offset of popover 2024-11-01 22:57:06 +00:00
Svilen Markov
c3c7f8b14f Allow changing max columns on split columns widget 2024-10-31 18:23:56 +00:00
Svilen Markov
863d4f117b
Merge pull request #251 from ehaughee/market-name-hovertext
Add title prop to Market Name for full text of truncated names on hover
2024-10-30 23:57:11 +00:00
Eric Haughee
35038ed56b Add title prop to Market Name for full text of truncated names on hover 2024-10-30 15:25:19 -07:00
Svilen Markov
c4149f5d94
Merge pull request #248 from oliver-mitchell/fix-non-whole-hour-timezone-display-on-clock
fix: non-whole-hour timezones now correctly shown on Clock widget
2024-10-30 15:48:24 +00:00
Svilen Markov
de2db71e54 Simplify clock zone diff text and add title for more details 2024-10-30 15:30:34 +00:00
Svilen Markov
2e3ed896cc Give clock time min width to avoid inconsistent layout for 12 hour format 2024-10-30 15:25:30 +00:00
Ralph Ocdol
f6f352e9e0 fix: indention 2024-10-29 18:15:28 +08:00
Ralph Ocdol
33dede8abc feat: added auto expand mobile page navigation 2024-10-29 18:13:24 +08:00
Oliver Mitchell
1e2e66ecf7 fix: non-whole-hour timezones now correctly shown on Clock
Timezones that used non-whole-hour definitions (ie. Australia/ACDT) not correctly shown on the Clock widget. This fix changes how the difference is calculated to account for both minutes, and hours.

Due to the change, the text shown for relative timezone differences also had to be changed.
2024-10-29 00:00:37 +10:30
Svilen Markov
6e5140d859 Allow specifying headers in RSS feeds 2024-10-21 23:49:09 +01:00
Svilen Markov
84a7f90129 Add custom API widget 2024-10-21 23:27:25 +01:00
Luca Crema
aff02c2def
Added HEALTHCHECK to Dockerfile 2024-10-18 14:07:24 +02:00
Svilen Markov
2dd5b29303 Remove unused class 2024-10-18 11:10:00 +01:00
Svilen Markov
bc8f17393a Fix simple icons URL 2024-10-18 10:54:34 +01:00
Svilen Markov
72f78f1c8d
Merge pull request #240 from cmeadowstech/alt-status-codes
Add alternative status code option to monitor widget
2024-10-18 10:53:18 +01:00
Svilen Markov
201b4ca1e8 Use AltStatusCodes for HasFailing check 2024-10-18 10:51:46 +01:00
Cody Meadows
04811a0658 Merge remote-tracking branch 'upstream/release/v0.7.0' into alt-status-codes
Update to current release
2024-10-17 22:01:56 +00:00
Cody Meadows
ee94d6aa89 Add alternative status code prameter 2024-10-17 21:58:18 +00:00
Svilen Markov
3382cd6208 Show twitch stream preview and title on avatar hover 2024-10-17 21:53:07 +01:00
Svilen Markov
56f9ec1d10 Include title in twitch channel data 2024-10-17 21:37:32 +01:00
Svilen Markov
1b42312d02 Fix edge case in popover 2024-10-17 21:36:41 +01:00
Svilen Markov
8a41a9f506 Improve formatting of request body 2024-10-17 21:36:22 +01:00
Svilen Markov
18bb2d7501 Change simple icons provider 2024-10-16 10:58:09 +01:00
Svilen Markov
6f8e576c9b Add fallback-content-type property 2024-10-16 10:52:57 +01:00
Svilen Markov
e5bb102ab1 Add split-column widget 2024-10-15 18:05:29 +01:00
Svilen Markov
13700fe2b2 Change masonry options source 2024-10-15 16:04:22 +01:00
Svilen Markov
3cfbe65855 Make the first full column the primary on mobile rather than the last 2024-10-15 15:58:30 +01:00
Svilen Markov
ab0b11cc92 Add masonry layout functionality 2024-10-13 18:33:26 +01:00
Svilen Markov
17c8071de9 Extract group widget functionality into generic widget container 2024-10-13 18:30:47 +01:00
Svilen Markov
1fe7f61ec8 Change environment variables behavior
* Can now be inserted anywhere in the string
* Can now insert multiple environment variables in a single string
* Can now be escaped if prefixed with \
2024-10-13 17:12:59 +01:00
Svilen Markov
ea3b8124fc Remove unused class 2024-10-13 17:08:52 +01:00
Svilen Markov
c41cfadb19
Merge pull request #231 from ehaughee/markets-sort-by-change
Add Markets sort-by: change to sort by percent change
2024-10-04 11:09:59 +01:00
Eric Haughee
e434fe0847 Add Markets sort-by: change to sort by percent change 2024-10-03 11:26:33 -07:00
Svilen Markov
8a8aaa752e Fix edge case in weather widget
If the temperature for the entire day is the same the range is 0, divide by 0 no bueno
2024-10-02 10:40:25 +01:00
Svilen Markov
4420d1df2c Bump versions & update dependencies 2024-10-01 15:47:56 +01:00
Svilen Markov
d90d39933a
Merge pull request #228 from Dakhtara/patch-1
Fix typo on configuration for search autofocus property description
2024-10-01 15:28:42 +01:00
Anthony
7e38ab624a
Update configuration.md 2024-10-01 08:59:11 +02:00
Svilen Markov
9a36187333 Add /api/healthz endpoint 2024-09-30 00:42:19 +01:00
Svilen Markov
b0c4d70628 Fix bug with collapsible grids inside of group widget
Also move utils to new file
2024-09-30 00:42:19 +01:00
Svilen Markov
d5fa6424c1 Fix bookmark icons shrinking when text wraps 2024-09-30 00:42:19 +01:00
Svilen Markov
07fe5b3cb1
Merge pull request #224 from SimJunYou/piholePrivacy
DNS Stats widget - fix Pihole privacy edge case
2024-09-30 00:31:00 +01:00
Svilen Markov
672547cd07 Switch to using custom type for pihole's top blocked domains 2024-09-30 00:24:44 +01:00
SimJunYou
28167403a4 Cover edge case when Pihole's privacy settings are enabled 2024-09-19 17:43:43 +08:00
Svilen Markov
eaf08d42dd
Merge pull request #220 from paricbat/main
Add important details about the server.base-url property
2024-09-16 08:35:33 +01:00
Veronika Bušová
bae95a5e07
Add important details about the server.base-url property 2024-09-15 00:29:46 +02:00
Svilen Markov
db1ed9e257 Fix & refactor adguard stats 2024-09-10 05:38:14 +01:00
Svilen Markov
adef35049f Add warning when pihole doesn't return expected data points 2024-09-10 05:38:14 +01:00
Svilen Markov
bd9abf2e52 Fix mobile nav links wrapping 2024-09-10 05:38:14 +01:00
Svilen Markov
aa74c00fd4
Merge pull request #210 from micash545/main
releases: Add support for Codeberg
2024-09-08 19:36:20 +01:00
Svilen Markov
a68a907fa7 Update SVG 2024-09-08 19:35:01 +01:00
Svilen Markov
1a05d6d03a Don't export function 2024-09-08 19:34:43 +01:00
micash
60f4183057 add codeberg releases 2024-09-08 16:59:24 +02:00
Svilen Markov
04c3afc850 Document center-vertically property 2024-09-08 06:25:06 +01:00
Svilen Markov
066e512844
Merge pull request #206 from glanceapp/release/v0.6.0
Release/v0.6.0
2024-09-08 04:18:11 +01:00
Svilen Markov
d60457afaf Merge branch 'main' into release/v0.6.0 2024-09-08 04:17:23 +01:00
Svilen Markov
f5a788220e Add link to Discord server 2024-09-08 01:13:26 +01:00
Svilen Markov
2fd5edc6e4 Update CSS 2024-09-08 00:43:26 +01:00
Svilen Markov
26716d0b89 Add preconfigured pages 2024-09-08 00:39:48 +01:00
Svilen Markov
3e0f57d0c7 Update README images 2024-09-07 23:13:15 +01:00
Svilen Markov
5347717f3d Ignore all yml files starting with glance 2024-09-07 23:12:12 +01:00
Svilen Markov
6b0a569998 Limit width of widgets 2024-09-06 21:45:03 +01:00
Svilen Markov
4476391287 Fix missing class 2024-09-06 20:33:04 +01:00
Svilen Markov
16a9d8c244 Update group widget styling 2024-09-06 20:32:20 +01:00
Svilen Markov
bbda9a0ee8 Add single-line-titles property to RSS widget 2024-09-04 23:02:16 +01:00
Svilen Markov
776fdcc6ce Merge branch 'release/v0.6.0' into features 2024-09-03 00:26:33 +01:00
Svilen Markov
f76e06ec57 Fix videos widget channel URL 2024-09-01 02:27:09 +01:00
Svilen Markov
3a9cff697b Add and fix monitor widget class 2024-08-31 18:51:59 +01:00
Svilen Markov
e6979c77e3 Update dynamic columns & remove the need to specify style 2024-08-31 18:37:08 +01:00
Svilen Markov
99866507f5 Fix overflowing text in monitor widget 2024-08-31 17:35:53 +01:00
Svilen Markov
cf0dd07c21 Add center-vertically property 2024-08-31 17:35:38 +01:00
Svilen Markov
b9bf8c6c96 Fix version formatting in releases widget 2024-08-30 16:19:20 +01:00
Svilen Markov
725d0da15d Add show-failing-only property to the monitor widget 2024-08-29 21:14:50 +01:00
Svilen Markov
37164070d2 Merge branch 'release/v0.6.0' into features 2024-08-29 20:06:40 +01:00
Svilen Markov
d7bd34531f
Merge pull request #77 from aharivel/github-commit
Add last Github commits
2024-08-29 18:19:19 +01:00
Svilen Markov
71ca1753ef Update repository widget last commits 2024-08-29 18:18:38 +01:00
Svilen Markov
c6bfbf4ac1
Merge branch 'release/v0.6.0' into github-commit 2024-08-29 17:40:14 +01:00
Svilen Markov
a27fde72ee Fix group widget 2024-08-27 21:41:18 +01:00
Svilen Markov
785f6a36bf Increase assets cache time 2024-08-27 17:29:31 +01:00
Svilen Markov
1a4c12d851 Fix hardcoded release icons 2024-08-27 17:17:41 +01:00
Svilen Markov
b484e32b08 Improve dockerhub releases 2024-08-27 05:19:35 +01:00
Svilen Markov
303438834b
Merge pull request #165 from Fumesover/release/v0.6.0
releases: Add support for gitlab
2024-08-27 03:37:45 +01:00
Svilen Markov
70b7b7615a Fix capitalization 2024-08-27 03:36:36 +01:00
Svilen Markov
5c42e60faf Update docs 2024-08-27 03:35:35 +01:00
Svilen Markov
48ef60e0eb Add todo 2024-08-27 03:33:37 +01:00
Svilen Markov
01af97ddab Allow fetching releases from multiple sources 2024-08-27 03:26:16 +01:00
Svilen Markov
038794fa1c
Merge pull request #75 from realdavidops/main
Allow some branding customization.
2024-08-27 00:18:04 +01:00
Svilen Markov
ffb1bccb10 Move default logo text to the template 2024-08-27 00:15:55 +01:00
Svilen Markov
371eb3bee6 Change styling of custom logo 2024-08-27 00:14:31 +01:00
Svilen Markov
466d935d02 Update docs 2024-08-27 00:09:35 +01:00
Svilen Markov
7ffca9c3f1 Allow setting custom footer 2024-08-27 00:09:26 +01:00
Svilen Markov
a5981b3106 Fix RSS thumbnails where feed image is absolute 2024-08-26 23:50:37 +01:00
Svilen Markov
1df080983a Add DNS Stats widget 2024-08-22 23:11:45 +01:00
Svilen Markov
822b72eee4 Delay showing next popover by 2 frames
This resets the CSS animation so it can be played again, otherwise it gets skipped
2024-08-22 22:44:27 +01:00
Svilen Markov
014abdcc00 Allow setting text-align on popovers 2024-08-22 22:43:41 +01:00
Svilen Markov
b35cc437d3 Remove unused functions 2024-08-22 22:43:10 +01:00
Svilen Markov
7ce87c7168
Merge pull request #197 from ccjjxl/bugfix_rss
Fix RSS item link empty <link/>
2024-08-22 11:16:33 +01:00
ccjjxl
2a3f8eedf6 Fix RSS item link empty <link/> 2024-08-22 17:24:00 +08:00
Svilen Markov
895c3f2f97 Allow changing popover anchor 2024-08-20 16:19:21 +01:00
Svilen Markov
8860f4591b Allow showing popover above & tweaks 2024-08-20 13:29:44 +01:00
Svilen Markov
984f26807e Remove duplicate rule & add non-prefixed line-clamp 2024-08-19 14:24:05 +01:00
Svilen Markov
88c58e6108 Add <details> styling 2024-08-19 14:03:48 +01:00
Svilen Markov
83c7c4a14a Add popover functionality 2024-08-19 02:15:22 +01:00
Svilen Markov
b4a4df480e Add utility classes 2024-08-18 18:26:56 +01:00
Svilen Markov
9843b4d218 Fix armv7 docker image 2024-08-15 13:33:42 +01:00
Svilen Markov
c1251af597 Tidy up 2024-08-11 14:13:00 +01:00
Svilen Markov
7d09b8bf1b Fix reddit crossposts & add show-flairs property 2024-08-11 14:04:18 +01:00
Svilen Markov
f57bdeec12 Update branding config 2024-08-10 19:43:50 +01:00
Svilen Markov
9899f6b761
Merge branch 'release/v0.6.0' into main 2024-08-10 18:47:39 +01:00
Svilen Markov
328e10b89f Don't show error if RSS feeds didn't return any items 2024-08-10 18:01:54 +01:00
Svilen Markov
b37f8a8375
Merge pull request #194 from glanceapp/backport-fixes
Backport fixes from v0.6.0 to v0.5.0
2024-08-09 16:08:30 +01:00
Svilen Markov
139937f887 Fix missing RSS thumbnail images 2024-08-09 16:03:24 +01:00
Wyatt Gill
b8b90451b6 Use GitHub's latest release API endpoint
The current releases widget uses the releases endpoint to pull the 10
most recent releases and filter them to find the latest release. This
causes a problem when a repository's latest release is outside of the 10
most recent (e.g. 10 prereleases):

ERROR No live release found repository=cross-seed/cross-seed url="https://api.github.com/repos/cross-seed/cross-seed/releases?per_page=10"

This is no longer a problem when using the latest release endpoint which
grabs the latest release, ignoring draft releases and prereleases.
2024-08-09 16:02:15 +01:00
Svilen Markov
c9a2aff6a6 Fix search input color 2024-08-09 16:01:15 +01:00
Svilen Markov
b25b117717 Reduce mb on widget header 2024-08-09 11:25:11 +01:00
Svilen Markov
e174fd731c Fix size-title-dynamic class 2024-08-08 16:55:46 +01:00
Svilen Markov
34e6d463be Make forum post title size dynamic 2024-08-08 16:37:41 +01:00
Svilen Markov
808169f43c Make horizontal scrollbars thinner 2024-08-08 16:00:41 +01:00
Svilen Markov
82b6531a07 Add funding 2024-08-07 19:18:17 +01:00
Svilen Markov
917e2cabfc Merge branch 'main' into release/v0.6.0 2024-08-07 19:03:12 +01:00
Svilen Markov
a981025679
Merge pull request #108 from wfg/github-actions
Create release workflow
2024-08-07 18:58:13 +01:00
Svilen Markov
f9ea2d73e1 Tidy up previous build files 2024-08-07 18:50:06 +01:00
Svilen Markov
a5728e9407 Update release workflow 2024-08-07 18:47:50 +01:00
Svilen Markov
404a2acc8f Remove extra margin from mobile nav offset 2024-08-07 11:16:30 +01:00
Svilen Markov
7bf61ee8ed Remove app name from title 2024-08-07 04:05:10 +01:00
Svilen Markov
bf1cc47525 Fix & increase offset of loading indicator 2024-08-05 23:16:32 +01:00
Svilen Markov
74ad27d5fb Rejig CSS 2024-08-05 22:50:44 +01:00
Svilen Markov
60b73aff24
Merge pull request #167 from CremaLuca/main
feat: serving behind reverse proxy
2024-08-05 14:12:20 +01:00
Svilen Markov
4de465e544 Add some foolproofery 2024-08-05 14:11:19 +01:00
Svilen Markov
e1161b9227 Refactor base-url & add documentation 2024-08-05 14:08:16 +01:00
Svilen Markov
c2cdd0fa08
Merge branch 'release/v0.6.0' into main 2024-08-05 13:26:46 +01:00
Svilen Markov
b365a03e71 Allow hiding desktop navigation 2024-08-05 12:42:41 +01:00
Svilen Markov
2e629efc7f Remove link from footer 2024-08-05 12:28:08 +01:00
Svilen Markov
fcaa2bef6e Vertically center loading indicator 2024-08-05 12:15:55 +01:00
Svilen Markov
3b2a9fedef Move footer to bottom of viewport 2024-08-05 12:00:20 +01:00
Svilen Markov
64256f2638 Fix value 2024-08-05 11:40:32 +01:00
Svilen Markov
5c8122d93b Fix extra margin at bottom of page 2024-08-05 11:17:46 +01:00
Svilen Markov
6ffb95779d Don't force scrollbar when not needed 2024-08-05 11:12:25 +01:00
Svilen Markov
af975d0e7f Bump versions 2024-08-05 00:00:11 +01:00
Svilen Markov
34b2b88cf3 Merge branch 'main' into release/v0.6.0 2024-08-04 23:46:28 +01:00
Svilen Markov
3fea166274 Add page width customizability 2024-08-04 23:40:58 +01:00
Svilen Markov
233b905492
Merge pull request #177 from MrExplode/monitor-urls
Different monitor urls
2024-08-04 20:55:02 +01:00
Svilen Markov
7afad765cb Use hyphen instead of underscore 2024-08-04 20:54:30 +01:00
MrExplode
39663cb594
Document monitor URL changes 2024-08-03 14:14:42 +02:00
MrExplode
457e0539fa
Allow empty check URL 2024-08-03 14:14:24 +02:00
MrExplode
871ba619a3
Add separate URL for monitor check 2024-08-03 14:06:52 +02:00
Svilen Markov
f5427310e8 Document include-shorts 2024-08-03 04:25:07 +01:00
Svilen Markov
e4435252f4
Merge pull request #176 from 3rd/release/v0.6.0
feat: add no_shorts option for YouTube feeds
2024-08-03 04:05:56 +01:00
Svilen Markov
a66aa74c7f Invert property 2024-08-03 04:04:02 +01:00
Svilen Markov
3949e15f77 Remove previous scuffed attempt at filtering shorts 2024-08-03 04:03:16 +01:00
Svilen Markov
acaf50bf8a Use author > name instead of title
This is because the playlist response returns the name of the playlist in the title,
which meant that the channel name was always "Videos"
2024-08-03 04:02:23 +01:00
3rd
09eedc08c1 feat: add no_shorts option for YouTube feeds 2024-08-02 23:39:27 +03:00
Svilen Markov
738bcf8bcb Add group widget 2024-08-01 21:34:07 +01:00
Svilen Markov
795caa5d9d Allow widgets to handle HTTP requests 2024-08-01 18:26:38 +01:00
Svilen Markov
c041197f3f
Merge pull request #173 from DVDAndroid/dvd/assets-fixes
Fix crash when using a custom CSS file; fix error 404 for manifest.json
2024-07-26 16:30:55 +01:00
dvdandroid
0730789cb3 Fix crash when using a custom CSS file; fix error 404 for manifest.json 2024-07-25 16:57:10 +02:00
Svilen Markov
1eb0fbd2c9
Merge pull request #171 from DVDAndroid/dvd/twich-nocategory-fix
Fix incorrect LiveSince if API returned no game
2024-07-25 00:42:59 +01:00
Svilen Markov
0484122b7f
Merge pull request #172 from DVDAndroid/dvd/clickable-twitch-avatar
Make Twitch avatar clickable
2024-07-25 00:42:46 +01:00
dvdandroid
7bc9c704d1 Don't render "empty" game category 2024-07-24 21:50:00 +02:00
dvdandroid
c9efdc2c16 Fix incorrect LiveSince if API returned no game
(streamer can stream without a category/game associated)
2024-07-24 21:38:35 +02:00
dvdandroid
e0dee998d4 Make Twitch avatar clickable 2024-07-23 23:17:28 +02:00
Svilen Markov
e09ea60616 Update dependencies 2024-07-15 21:59:51 +01:00
Svilen Markov
7cab07fa01 Bump alpine version 2024-07-15 21:59:51 +01:00
Svilen Markov
5a62a8afe9 Add autofocus property to search widget 2024-07-15 21:59:51 +01:00
CremaLuca
23d6d41148 fix(serve): using paths without base url 2024-07-10 15:40:35 +02:00
Luca Crema
a07fbfd7b7 Added baseurl somewhere 2024-07-10 15:35:25 +02:00
Svilen Markov
1411268e35
Merge pull request #80 from c0smicdev/main
Add Kanagawa Dark theme
2024-07-03 19:51:42 +01:00
Svilen Markov
9bda88edbd
Merge pull request #145 from ssrtw/rss-feed-enhance
Rss feed enhance
2024-07-03 19:49:32 +01:00
Svilen Markov
16da25898f Prevent users from modifying internal property 2024-07-03 19:48:58 +01:00
Svilen Markov
88ae2b8e83
Merge pull request #164 from dracarys18/main
fix: allow custom bangs without an argument
2024-07-03 19:26:54 +01:00
Albin Parou
7d1ede8c91 releases: Add support for gitlab 2024-07-03 08:20:02 +02:00
dracarys18
3805f8b156
fix: allow custom bangs without an argument 2024-07-03 02:46:16 +05:30
Svilen Markov
39f40670d0 Fix missing RSS thumbnail images 2024-06-29 17:17:27 +01:00
Svilen Markov
514cf2b81c Allow setting widget title URL 2024-06-29 16:10:43 +01:00
Svilen Markov
4dbb5975c0 Allow setting custom CSS classes for individual widgets 2024-06-29 15:55:17 +01:00
Svilen Markov
b327e59ab1 Allow specifying a custom lobsters URL or instance 2024-06-29 15:43:07 +01:00
Svilen Markov
917d51e54b Make script a module 2024-06-29 15:20:26 +01:00
Svilen Markov
122d05346b Change caching mechanism 2024-06-29 15:20:26 +01:00
c0smic
645802981e
Update theme preview 2024-06-14 16:56:33 +00:00
c0smic
0348a2475c
Delete preview of old version 2024-06-14 16:55:44 +00:00
ssrtw
a2b7f59dc9 fix: correct title handling for RSS items without titles 2024-06-13 03:13:57 +00:00
ssrtw
c23dc93c7a feat: use description as fallback for RSS items without titles 2024-06-12 15:36:29 +00:00
ssrtw
d3e3667405 fix: add detailed mode flag to fix RSS logic conflict 2024-06-12 03:28:43 +00:00
Svilen Markov
d4ed5afe93
Merge pull request #134 from wfg/github-latest-release-fix
Use GitHub's latest release API endpoint
2024-06-11 20:43:35 +01:00
Svilen Markov
790ecef1c1
Merge pull request #137 from ralphocdol/search-new-tab-property
feat: added property new-tab to type: search
2024-06-11 20:34:05 +01:00
Svilen Markov
edb59ffdc6 Small change to implementation for new tab 2024-06-11 20:30:10 +01:00
Svilen Markov
e137038586 Fix overflowing text 2024-06-11 01:16:39 +01:00
Svilen Markov
761eb343f9 Fix search input color 2024-06-11 00:35:26 +01:00
Svilen Markov
6c15c907bf Remove clock from preconfigured page 2024-06-11 00:17:25 +01:00
ralphocdol
06b21e3d27 feat: added property new-tab to type: search 2024-06-09 21:18:56 +08:00
Wyatt Gill
263d2e6f30 Use GitHub's latest release API endpoint
The current releases widget uses the releases endpoint to pull the 10
most recent releases and filter them to find the latest release. This
causes a problem when a repository's latest release is outside of the 10
most recent (e.g. 10 prereleases):

ERROR No live release found repository=cross-seed/cross-seed url="https://api.github.com/repos/cross-seed/cross-seed/releases?per_page=10"

This is no longer a problem when using the latest release endpoint which
grabs the latest release, ignoring draft releases and prereleases.
2024-06-07 16:27:13 -05:00
Svilen Markov
c97fec66f4 Update dependencies 2024-06-04 23:42:40 +01:00
Wyatt Gill
d5cf881d7a Produce example 2024-06-03 16:16:43 -05:00
Wyatt Gill
050d3e6cd2 Add release workflow using GoReleaser 2024-06-03 16:16:43 -05:00
Svilen Markov
757f68c872 Increase lobsters widget cache time 2024-06-03 00:46:26 +01:00
Svilen Markov
b83bf9bc09
Merge pull request #121 from glanceapp/release/v0.5.0
Release v0.5.0
2024-06-02 20:17:50 +01:00
Svilen Markov
8d96e883d6 Update readme 2024-06-02 20:14:35 +01:00
Svilen Markov
75079a981b Add HTML widget 2024-06-02 19:01:20 +01:00
Svilen Markov
e8043be107 Add lobsters widget preview 2024-06-02 18:44:27 +01:00
Svilen Markov
c276f66017 Add tags to forum posts template 2024-06-02 18:44:17 +01:00
Svilen Markov
db7df33b27 Bump versions 2024-06-02 18:24:54 +01:00
Svilen Markov
1b8e355944 Document extension widget 2024-06-02 18:24:54 +01:00
Svilen Markov
342ef90cbe Add extension widget 2024-06-02 18:21:03 +01:00
Svilen Markov
b3be60acf6 Document collapse-after-rows property 2024-06-02 18:21:03 +01:00
Svilen Markov
b58d3e2849 Document change detection widget 2024-06-02 18:21:03 +01:00
Svilen Markov
4206fb943d Document simple icons usage for monitor icons 2024-06-02 18:21:03 +01:00
Svilen Markov
ee58ff40f1 Document allow-insecure property 2024-06-02 18:21:02 +01:00
Svilen Markov
21fd842bde Refactor monitor: allow insecure requests & use of simple icons 2024-06-02 18:21:02 +01:00
Svilen Markov
b63b5eb262 Make converting string to simple icon url reusable 2024-06-02 18:21:02 +01:00
Svilen Markov
9c69509a19 Fix markets class 2024-06-02 18:21:02 +01:00
Svilen Markov
511147fb90 Document detailed-list RSS style 2024-06-02 18:21:02 +01:00
Svilen Markov
9ce0030482 Don't offset attachments 2024-06-02 18:21:02 +01:00
Svilen Markov
4164263639 Fix RSS item links without base domain 2024-06-02 18:21:02 +01:00
Svilen Markov
1bebb88d0e Rename Stocks to Markets
Also fix bug that would remove markets if a network request failed
and not show them again until Glance was restarted
2024-06-02 18:21:02 +01:00
Svilen Markov
d2f5dbbc26 Fix weather widget making HTTP request as part of validation
Which would prevent app startup if there was no internet connection
2024-06-02 18:21:02 +01:00
Svilen Markov
2af6500028 Add utility class 2024-06-02 18:21:02 +01:00
Svilen Markov
a7cf8be8c4 Lower gap between timezones 2024-06-02 18:21:02 +01:00
Svilen Markov
69b3e544bd Fix layout shift on image load 2024-06-02 18:21:02 +01:00
Svilen Markov
0c63c61db5 Position category details to top when tags missing 2024-06-02 18:21:02 +01:00
Svilen Markov
8dc34ddfa7
Merge pull request #56 from jonasknobloch/lobsters-widget
Add Lobsters widget
2024-06-02 18:20:36 +01:00
Svilen Markov
8efda0047c
Merge branch 'release/v0.5.0' into lobsters-widget 2024-06-02 18:20:07 +01:00
Svilen Markov
e3c17e0347
Merge branch 'main' into lobsters-widget 2024-06-02 18:19:06 +01:00
Svilen Markov
038c43bc09 Document lobsters widget 2024-06-02 18:17:20 +01:00
Svilen Markov
720549ec7e Allow specifying sort order and tags through config 2024-06-02 18:08:29 +01:00
Svilen Markov
3aa1f273b6 Add tags to dedicated field rather than to title 2024-06-02 17:51:49 +01:00
Svilen Markov
5105ebb870 Don't parse unused fields 2024-06-02 17:51:07 +01:00
Svilen Markov
0a55c12f4f Add tags to forum posts 2024-06-02 17:50:46 +01:00
Svilen Markov
67c20c3a5f
Merge pull request #46 from knhash/change-detection
feat: add change detection module
2024-05-30 22:55:22 +01:00
Svilen Markov
f4f7892123
Merge branch 'release/v0.5.0' into change-detection 2024-05-30 22:55:01 +01:00
Svilen Markov
00a93e466d Update change detection 2024-05-30 22:53:59 +01:00
c0smic
d27993d2c9
fix: use negative color with higher contrast 2024-05-29 16:53:31 +00:00
c0smic
bfc65d51e3
Merge branch 'glanceapp:main' into main 2024-05-29 16:50:18 +00:00
c0smic
3c29d38599
fix: Fix typo
Co-authored-by: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com>
2024-05-27 18:11:35 +00:00
Svilen Markov
f8816ff227
Merge pull request #89 from NoxesP/patch-1
docs: update configuration.md
2024-05-25 04:23:06 +01:00
Svilen Markov
a5be460c2e
Merge pull request #69 from chand1012/search
Add Search Bar Widget
2024-05-25 04:00:20 +01:00
Svilen Markov
e1e7853c34 Update search widget 2024-05-25 03:58:11 +01:00
Svilen Markov
3f862f67ab
Merge branch 'release/v0.5.0' into search 2024-05-25 02:51:32 +01:00
Svilen Markov
4bb5b2641b Further attempts at making the rain look less scuffed 2024-05-25 02:38:49 +01:00
Svilen Markov
f044fdf5dc Add utility classes 2024-05-25 02:38:49 +01:00
Svilen Markov
d1be4637ad Fix registering resize event on wrong object 2024-05-25 02:38:49 +01:00
Svilen Markov
d18f645c18 Add detailed-list style for RSS 2024-05-25 02:38:49 +01:00
Svilen Markov
6663360caa Don't add classes that are no longer needed 2024-05-25 02:38:48 +01:00
Svilen Markov
c2d24ee4d9 Set background color in manifest 2024-05-25 02:38:48 +01:00
Svilen Markov
37471fcdc1 Remove extra new line 2024-05-25 02:38:48 +01:00
Svilen Markov
0f42567d98
Merge pull request #82 from yardenshoham/add-clock-widget
Add a clock widget
2024-05-25 02:32:18 +01:00
Svilen Markov
8148f09b9c Add more features to clock 2024-05-25 02:28:45 +01:00
NoxesP
c270afb5e5
docs: update configuration.md
Won't admit how long it took for me to notice this in my glance.yml file after wondering why it wasn't working ahahah.

Small fix :)
2024-05-20 04:47:57 +03:00
Svilen Markov
6de0f73ec1 Fix build breaking because of missing build dir 2024-05-19 21:33:51 +01:00
Svilen Markov
a80a34afb1
Merge pull request #83 from wfg/image-build-rework
Image build rework
2024-05-19 18:06:03 +01:00
Svilen Markov
584f813fc4
Merge pull request #87 from hex-developer/main
Fix column overflow
2024-05-19 18:01:21 +01:00
Svilen Markov
a44056dc43 Remove other unnecessary shrinks 2024-05-19 18:00:08 +01:00
Svilen Markov
c941c78ebf Truncate RSS channel name 2024-05-19 17:52:17 +01:00
Svilen Markov
9e9f001fdf Add newline at end of file 2024-05-19 17:51:59 +01:00
Svilen Markov
9060887694 Remove unnecessary shrink 2024-05-19 17:51:40 +01:00
Svilen Markov
6d9ea27e9f
Merge branch 'release/v0.5.0' into main 2024-05-19 17:48:14 +01:00
Svilen Markov
4f4973157c Merge branch 'main' into release/v0.5.0 2024-05-19 17:46:21 +01:00
hex-developer
859a6d2f27 Fix column overflow 2024-05-19 15:52:13 +00:00
Wyatt Gill
e3ad646bae Add .dockerignore to keep build context small 2024-05-18 17:42:02 -05:00
Wyatt Gill
53a661861a Build app in Dockerfile
This change makes it simpler and more convenient to build the app.
2024-05-18 17:41:21 -05:00
Yarden Shoham
4a27ec3271 Add a clock widget
It's a simple clock that shows the current time

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2024-05-18 22:40:33 +03:00
Svilen Markov
926de71274 Add PR template 2024-05-18 19:49:26 +01:00
Svilen Markov
b0e57bae9f
Merge pull request #81 from yardenshoham/correct-method-in-docs
Specify the correct method in the Monitor widget documentation
2024-05-18 19:19:14 +01:00
Svilen Markov
0bbb4573b2 Revert setTimeout for images
Reduces instances of cached images fading in by giving
the browser a bit more time to load them
2024-05-18 18:52:54 +01:00
Svilen Markov
eeda28a287 Reduce reliance on setTimeout shenanigans 2024-05-18 18:43:11 +01:00
Yarden Shoham
59c55b96be Specify the correct method in the Monitor widget documentation
After 189b8895b0 we use `GET` instead of `HEAD`

Signed-off-by: Yarden Shoham <git@yardenshoham.com>
2024-05-18 18:02:12 +03:00
c0smic
060609e2cb
Add Kanagawa Dark to themes 2024-05-18 14:25:08 +00:00
c0smic
a1c48bf012
Use new Kanagawa Dark Image 2024-05-18 14:24:15 +00:00
c0smic
a67fb66d2e
Delete old Kanagawa Dark image 2024-05-18 14:23:32 +00:00
c0smic
3001f4845a
Add new Kanagawa Dark image 2024-05-18 14:20:17 +00:00
c0smic
192c1ad669
Rename kanagawa dark preview image 2024-05-18 14:15:35 +00:00
c0smic
89ae8a3607
Add Kanagawa Dark Theme preview 2024-05-18 14:13:56 +00:00
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
Anthony Harivel
90d718c186
Add last Github commits
Signed-off-by: Anthony Harivel <aharivel@redhat.com>
2024-05-18 11:51:44 +02: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
David Leonard
3cfbe0c89b Allow some branding customization. 2024-05-17 14:37:05 -07: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
chand1012
8392b7a606
Fix some styling issues and query bug 2024-05-17 10:18:58 -04: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
chand1012
0f01a8eda5
Add search box 2024-05-15 20:04:08 -04: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
Shashank S
6cad5a8efb Merge remote-tracking branch 'origin' into change-detection 2024-05-13 03:32:52 +02:00
Svilen Markov
7743664527
Merge pull request #57 from glanceapp/release/v0.4.0
Release v0.4.0
2024-05-12 22:12:00 +01:00
Svilen Markov
714e1b6d29 Add dynamic columns style for bookmarks, stocks & monitor sites 2024-05-12 22:07:58 +01:00
Svilen Markov
a0585e097f Update codeblocks highlighting 2024-05-12 20:45:57 +01:00
Svilen Markov
6363b76d33 Update codeblocks highlighting 2024-05-12 20:44:35 +01:00
Svilen Markov
503562f2e5 Update readme 2024-05-12 20:42:36 +01:00
Svilen Markov
01a7331354 Update readme with docker image build guide 2024-05-12 20:33:36 +01:00
Jonas Knobloch
c07a1ab636 Update default feed 2024-05-12 17:37:04 +02:00
Jonas Knobloch
eb9f92f04a Update tag delimiter 2024-05-12 17:14:40 +02:00
Jonas Knobloch
c05607934b Fetch posts via JSON endpoint 2024-05-12 17:14:04 +02: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
Jonas Knobloch
344f518991 Add Lobsters widget 2024-05-12 13:20:34 +02:00
Svilen Markov
6bb44bef20
Merge pull request #54 from eltociear/patch-1
docs: update configuration.md
2024-05-12 11:03:18 +01:00
Svilen Markov
aef0c84286 Move engagement calculation inside widget 2024-05-12 11:00:45 +01:00
Svilen Markov
0fbc8dcb41 Allow specifying links for stock symbol & chart 2024-05-12 04:13:38 +01:00
Svilen Markov
818138340c Rename widget to just repository 2024-05-12 03:35:25 +01:00
Svilen Markov
4beab6bcec Make images fade in as they load 2024-05-12 03:18:55 +01:00
Ikko Eltociear Ashimine
b9b7ae1cac
docs: update configuration.md
minor fix
2024-05-12 03:09:12 +09:00
Carlos Medina
91c75518da Change mobile nav styles 2024-05-10 19:54:43 -06:00
Svilen Markov
82f51293df Add more refinement options for HN widget 2024-05-10 14:08:01 +01:00
Svilen Markov
840ba0f240 Add more refinement options for Reddit widget 2024-05-10 13:23:57 +01:00
Svilen Markov
c5e3eed64b Add Repository Overview widget 2024-05-10 11:05:42 +01:00
Svilen Markov
7dd1cca65d Add horizontal-cards-2 style for RSS widget 2024-05-10 10:10:13 +01:00
Svilen Markov
57414bdb67 So. Many. Units. 2024-05-10 09:52:05 +01:00
Svilen Markov
646be63895 Fix 1px black border in video thumbnails 2024-05-10 08:26:55 +01:00
Svilen Markov
3c1fb97c12 Add grid-cards style for videos widget 2024-05-10 07:41:11 +01:00
Svilen Markov
e78619f5e1 Update build to include openbsd and armv7 2024-05-09 13:00:42 +01:00
Shashank S
aa04904a72 feat: add diff, fix: configurable url 2024-05-09 08:32:27 +02:00
Svilen Markov
54102ae35e Make RSS thumbnail height configurable 2024-05-09 06:01:16 +01:00
Svilen Markov
7c4c1798b6 Lowercase domains 2024-05-09 04:49:42 +01:00
Svilen Markov
f30006093d Tweak breakpoints of horizontal cards 2024-05-09 04:46:22 +01:00
Svilen Markov
c8ef47064a Merge branch 'main' into release/v0.4.0 2024-05-09 04:22:51 +01: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
Shashank S
7adf624e95 feat: add change detection module 2024-05-08 11:47:25 +02:00
Svilen Markov
56cb97706b Document show-mobile-header property 2024-05-08 01:38:49 +01:00
Svilen Markov
dd5bec799f Allow specifying Url from env 2024-05-08 01:30:02 +01:00
Svilen Markov
2389415071 Add mobile reachability header 2024-05-08 01:21:55 +01:00
Svilen Markov
976fb02bd3 Lower gap between releases 2024-05-06 16:53:24 +01:00
Svilen Markov
007fe18d27 Add utility classes 2024-05-06 16:53:13 +01:00
Svilen Markov
11f2999b52 Update readme 2024-05-06 16:52:56 +01:00
Svilen Markov
e204029d3c
Merge pull request #41 from chev2/catppuccin-themes
Add the other Catppuccin themes
2024-05-03 22:09:39 +01:00
Chev
d3eaae2fb0
Update docs/themes.md with new Catppuccin themes 2024-05-03 12:52:02 -07:00
Chev
251c22a14f
Rename catppuccin.png to catppuccin-mocha.png
For consistency
2024-05-03 12:50:53 -07:00
Chev
7d0a7186e8
Add new Catppuccin theme screenshots 2024-05-03 12:49:45 -07:00
Svilen Markov
7b444b88e3
Merge pull request #35 from glanceapp/v0.3.0
V0.3.0
2024-05-03 05:46:39 +01:00
Svilen Markov
f317b1b189 Adjust the height and position of reddit thumbnails 2024-05-03 05:43:08 +01:00
Svilen Markov
3523562c3a Additions to monitor and bookmarks widgets 2024-05-03 05:38:40 +01:00
Svilen Markov
5fdc3e3f10
Merge pull request #23 from jarylc/proxy-option-for-reddit
Added a request-url-template option for Reddit requests
2024-05-03 02:48:32 +01:00
Jaryl Chng
24ef8f6e2a
Added a request-url-template option for Reddit requests
Reddit blocks known datacenter IPs from accessing their endpoints unless you request them to unblock it. Even so it has a low chance of success.

The new option will allow users to specify a request URL to handle Reddit requests.
2024-05-03 09:42:47 +08:00
Svilen Markov
d8d6625478 Allow specifying state in weather location 2024-05-02 19:54:20 +01:00
Svilen Markov
ad06146784 Add hide-on-mobile utility class 2024-05-02 18:45:58 +01:00
Svilen Markov
c97171883c Rename thumbnails to show-thumbnails 2024-05-02 18:45:15 +01:00
Svilen Markov
9c5b6045af Don't request unused wind speed data 2024-05-02 17:14:27 +01:00
Svilen Markov
eb85a7656f Allow showing thumbnails for reddit posts 2024-05-02 00:34:25 +01:00
Svilen Markov
25d45b309d Update doc 2024-05-02 00:33:18 +01:00
Svilen Markov
7d0b4535bb Change bool to boolean for consistency 2024-05-01 23:55:48 +01:00
Svilen Markov
f8af2025e8 Rename flex-grow to grow 2024-05-01 23:47:14 +01:00
Svilen Markov
948c117f5b Retain stocks order by default 2024-05-01 22:28:59 +01:00
Svilen Markov
58967ab758 Update doc 2024-05-01 22:06:40 +01:00
Svilen Markov
44a153d30a Allow using alternative links for YT, HN and reddit 2024-05-01 19:08:25 +01:00
Svilen Markov
d5d6103327 Fix currency symbol being hardcoded to $ 2024-04-29 22:32:46 +01:00
Svilen Markov
189b8895b0 Change monitor widget to make GET requests 2024-04-29 17:04:39 +01:00
Svilen Markov
748ad1ff8d Allow setting path to custom CSS 2024-04-29 16:47:18 +01:00
Svilen Markov
ccdafcdfab Allow changing between C and F for weather temp 2024-04-29 16:29:11 +01:00
Svilen Markov
78572a534b Increase max body length 2024-04-29 16:18:32 +01:00
Svilen Markov
2eb8e71e89 Update configuration docs 2024-04-29 01:15:28 +01:00
Svilen Markov
5375103c4b Update dependencies 2024-04-28 23:53:32 +01:00
Svilen Markov
faeeb7c899 Truncate response body in error message 2024-04-28 14:53:58 +01:00
Svilen Markov
99fd77feac Update incorrect link in configuration doc 2024-04-27 22:48:42 +01:00
212 changed files with 16041 additions and 4597 deletions

11
.dockerignore Normal file
View file

@ -0,0 +1,11 @@
# https://docs.docker.com/build/building/context/#dockerignore-files
# Ignore all files by default
*
# Only add necessary files to the Docker build context (Dockerfiles are always included implicitly)
!/build/
!/internal/
!/pkg/
!/go.mod
!/go.sum
!main.go

128
.github/CODE_OF_CONDUCT.md vendored Normal file
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.

1
.github/FUNDING.yml vendored Normal file
View file

@ -0,0 +1 @@
github: [glanceapp]

37
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View file

@ -0,0 +1,37 @@
name: Bug report
description: Let us know if something isn't working as expected
labels: ["bug report"]
body:
- type: markdown
attributes:
value: |
> [!NOTE]
>
> Do not prefix your title with "[BUG]", "[Bug report]", etc., a label will be added automatically.
If you're unsure whether you're experiencing a bug or not, consider using the [Discussions](https://github.com/glanceapp/glance/discussions) or [Discord](https://discord.com/invite/7KQ7Xa9kJd) to ask for help.
Please include only the information you think is relevant to the bug:
* How did you install Glance? (Docker container, manual binary install, etc)
* Which version of Glance are you using?
* Include the relevant parts of your `glance.yml` if applicable (widget, data source, properties used, etc)
* Include any relevant logs or screenshots if applicable
* Is the issue specific to a certain browser or OS?
* Steps to reliably reproduce the issue
* Are you hosting Glance on a VPS?
* Anything else you think might be relevant
**No need to copy the above list into your description, it's just a guide to help you provide the most useful information.**
- type: textarea
id: description
validations:
required: true
attributes:
label: Description
- type: markdown
attributes:
value: |
Thank you for taking the time to submit a bug report.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Discussions
url: https://github.com/glanceapp/glance/discussions
about: For help, feedback, guides, resources and more
- name: Discord
url: https://discord.com/invite/7KQ7Xa9kJd
about: Much like the discussions but more chatty

View file

@ -0,0 +1,33 @@
name: Feature request
description: Share your ideas for new features or improvements
labels: ["feature request"]
body:
- type: markdown
attributes:
value: |
> [!NOTE]
>
> Do not prefix your title with "[REQUEST]", "[Feature request]", etc., a label will be added automatically.
Please provide a detailed description of what the feature would do and what it would look like:
* What problem would this feature solve?
* Are there any potential downsides to this feature?
* If applicable, what would the configuration for this feature look like?
* Are there any existing examples of this feature in other software?
* If applicable, include any external documentation required to implement this feature
* Anything else you think might be relevant
**No need to copy the above list into your description, it's just a guide to help you provide the most useful information.**
- type: textarea
id: description
validations:
required: true
attributes:
label: Description
- type: markdown
attributes:
value: |
Thank you for taking the time to submit your idea.

1
.github/PULL_REQUEST_TEMPLATE.md vendored Normal file
View file

@ -0,0 +1 @@
<!-- If your pull request adds new features, changes existing ones or fixes any bugs, please use the dev branch as the base, otherwise use the main branch -->

9
.github/SECURITY.md vendored Normal file
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.

39
.github/workflows/release.yaml vendored Normal file
View file

@ -0,0 +1,39 @@
name: Create release
permissions:
contents: write
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout the target Git reference
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Golang
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Set up Docker buildx
uses: docker/setup-buildx-action@v3
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
args: release

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
/assets
/build
/playground
glance.yml
/.idea
/glance*.yml

69
.goreleaser.yaml Normal file
View file

@ -0,0 +1,69 @@
project_name: glanceapp/glance
checksum:
disable: true
builds:
- binary: glance
env:
- CGO_ENABLED=0
goos:
- linux
- openbsd
- freebsd
- windows
- darwin
goarch:
- amd64
- arm64
- arm
- 386
goarm:
- 7
ldflags:
- -s -w -X github.com/glanceapp/glance/internal/glance.buildVersion={{ .Tag }}
archives:
-
name_template: "glance-{{ .Os }}-{{ .Arch }}{{ with .Arm }}v{{ . }}{{ end }}"
files:
- nothing*
format_overrides:
- goos: windows
format: zip
dockers:
- image_templates:
- &amd64_image "{{ .ProjectName }}:{{ .Tag }}-amd64"
build_flag_templates:
- --platform=linux/amd64
goarch: amd64
use: buildx
dockerfile: Dockerfile.goreleaser
- image_templates:
- &arm64v8_image "{{ .ProjectName }}:{{ .Tag }}-arm64"
build_flag_templates:
- --platform=linux/arm64
goarch: arm64
use: buildx
dockerfile: Dockerfile.goreleaser
- image_templates:
- &armv7_image "{{ .ProjectName }}:{{ .Tag }}-armv7"
build_flag_templates:
- --platform=linux/arm/v7
goarch: arm
goarm: 7
use: buildx
dockerfile: Dockerfile.goreleaser
docker_manifests:
- name_template: "{{ .ProjectName }}:{{ .Tag }}"
image_templates: &multiarch_images
- *amd64_image
- *arm64v8_image
- *armv7_image
- name_template: "{{ .ProjectName }}:latest"
skip_push: auto
image_templates: *multiarch_images

View file

@ -1,10 +1,16 @@
FROM alpine:3.19
ARG TARGETOS
ARG TARGETARCH
FROM golang:1.24.2-alpine3.21 AS builder
WORKDIR /app
COPY build/glance-$TARGETOS-$TARGETARCH /app/glance
COPY . /app
RUN CGO_ENABLED=0 go build .
FROM alpine:3.21
WORKDIR /app
COPY --from=builder /app/glance .
HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
CMD wget --spider -q http://localhost:8080/api/healthz
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance"]
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]

10
Dockerfile.goreleaser Normal file
View file

@ -0,0 +1,10 @@
FROM alpine:3.21
WORKDIR /app
COPY glance .
HEALTHCHECK --timeout=10s --start-period=60s --interval=60s \
CMD wget --spider -q http://localhost:8080/api/healthz
EXPOSE 8080/tcp
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]

454
README.md
View file

@ -1,90 +1,436 @@
<p align="center"><em>What if you could see everything at a...</em></p>
<h1 align="center">Glance</h1>
<p align="center"><a href="#installation">Install</a><a href="docs/configuration.md">Configuration</a><a href="docs/themes.md">Themes</a></p>
<p align="center"><a href="#installation">Install</a><a href="docs/configuration.md">Configuration</a><a href="https://discord.com/invite/7KQ7Xa9kJd">Discord</a><a href="https://github.com/sponsors/glanceapp">Sponsor</a></p>
<p align="center"><a href="https://github.com/glanceapp/community-widgets">Community widgets</a><a href="docs/preconfigured-pages.md">Preconfigured pages</a><a href="docs/themes.md">Themes</a></p>
![example homepage](docs/images/readme-main-image.png)
![](docs/images/readme-main-image.png)
### Features
#### Various widgets
## Features
### Various widgets
* RSS feeds
* Subreddit posts
* Weather
* Bookmarks
* Latest YouTube videos from specific channels
* Calendar
* Stocks
* iframe
* Twitch channels & top games
* GitHub releases
* Site monitor
* Hacker News posts
* Weather forecasts
* YouTube channel uploads
* Twitch channels
* Market prices
* Docker containers status
* Server stats
* Custom widgets
* [and many more...](docs/configuration.md)
#### Themeable
![multiple color schemes example](docs/images/themes-example.png)
### Fast and lightweight
* Low memory usage
* Few dependencies
* Minimal vanilla JS
* Single <20mb binary available for multiple OSs & architectures and just as small Docker container
* Uncached pages usually load within ~1s (depending on internet speed and number of widgets)
#### Optimized for mobile devices
![mobile device previews](docs/images/mobile-preview.png)
### Tons of customizability
* Different layouts
* As many pages/tabs as you need
* Numerous configuration options for each widget
* Multiple styles for some widgets
* Custom CSS
#### Fast and lightweight
* Minimal JS, no bloated frameworks
* Very few dependencies
* Single, easily distributed <15mb binary and just as small docker container
* All requests are parallelized, uncached pages usually load within ~1s (depending on internet speed and number of widgets)
### Optimized for mobile devices
Because you'll want to take it with you on the go.
### Configuration
Checkout the [configuration docs](docs/configuration.md) to learn more. A [preconfigured page](docs/configuration.md#preconfigured-page) is also available to get you started quickly.
![](docs/images/mobile-preview.png)
### Installation
> [!CAUTION]
>
> The project is under active development, expect things to break every once in a while.
### Themeable
Easily create your own theme by tweaking a few numbers or choose from one of the [already available themes](docs/themes.md).
#### Manual
Checkout the [releases page](https://github.com/glanceapp/glance/releases) for available binaries. You can place the binary inside `/opt/glance/` and have it start with your server via a [systemd service](https://linuxhandbook.com/create-systemd-services/). To specify a different path for the config file use the `--config` option:
![](docs/images/themes-example.png)
<br>
## Configuration
Configuration is done through YAML files, to learn more about how the layout works, how to add more pages and how to configure widgets, visit the [configuration documentation](docs/configuration.md).
<details>
<summary><strong>Preview example configuration file</strong></summary>
<br>
```yaml
pages:
- name: Home
columns:
- size: small
widgets:
- type: calendar
first-day-of-week: monday
- type: rss
limit: 10
collapse-after: 3
cache: 12h
feeds:
- url: https://selfh.st/rss/
title: selfh.st
limit: 4
- url: https://ciechanow.ski/atom.xml
- url: https://www.joshwcomeau.com/rss.xml
title: Josh Comeau
- url: https://samwho.dev/rss.xml
- url: https://ishadeed.com/feed.xml
title: Ahmad Shadeed
- type: twitch-channels
channels:
- theprimeagen
- j_blow
- piratesoftware
- cohhcarnage
- christitustech
- EJ_SA
- size: full
widgets:
- type: group
widgets:
- type: hacker-news
- type: lobsters
- type: videos
channels:
- UCXuqSBlHAE6Xw-yeJA0Tunw # Linus Tech Tips
- UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
- UCsBjURrPoezykLs9EqgamOA # Fireship
- UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee
- UCHnyfMqiRRG1u-2MsSQLbXA # Veritasium
- type: group
widgets:
- type: reddit
subreddit: technology
show-thumbnails: true
- type: reddit
subreddit: selfhosted
show-thumbnails: true
- size: small
widgets:
- type: weather
location: London, United Kingdom
units: metric
hour-format: 12h
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
name: Bitcoin
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
repositories:
- glanceapp/glance
- go-gitea/gitea
- immich-app/immich
- syncthing/syncthing
```
/opt/glance/glance --config /etc/glance.yml
</details>
<br>
## Installation
Choose one of the following methods:
<details>
<summary><strong>Docker compose using provided directory structure (recommended)</strong></summary>
<br>
Create a new directory called `glance` as well as the template files within it by running:
```bash
mkdir glance && cd glance && curl -sL https://github.com/glanceapp/docker-compose-template/archive/refs/heads/main.tar.gz | tar -xzf - --strip-components 2
```
#### Docker
> [!IMPORTANT]
>
> Make sure you have a valid `glance.yml` file before running the container.
*[click here to view the files that will be created](https://github.com/glanceapp/docker-compose-template/tree/main/root)*
```console
docker run -d -p 8080:8080 \
-v ./glance.yml:/app/glance.yml \
-v /etc/timezone:/etc/timezone:ro \
-v /etc/localtime:/etc/localtime:ro \
glanceapp/glance
Then, edit the following files as desired:
* `docker-compose.yml` to configure the port, volumes and other containery things
* `config/home.yml` to configure the widgets or layout of the home page
* `config/glance.yml` if you want to change the theme or add more pages
<details>
<summary>Other files you may want to edit</summary>
* `.env` to configure environment variables that will be available inside configuration files
* `assets/user.css` to add custom CSS
</details>
When ready, run:
```bash
docker compose up -d
```
Or if you prefer docker compose:
If you encounter any issues, you can check the logs by running:
```bash
docker compose logs
```
<hr>
</details>
<details>
<summary><strong>Docker compose manual</strong></summary>
<br>
Create a `docker-compose.yml` file with the following contents:
```yaml
services:
glance:
container_name: glance
image: glanceapp/glance
restart: unless-stopped
volumes:
- ./glance.yml:/app/glance.yml
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
- ./config:/app/config
ports:
- 8080:8080
restart: unless-stopped
```
### Building from source
Then, create a new directory called `config` and download the example starting [`glance.yml`](https://github.com/glanceapp/glance/blob/main/docs/glance.yml) file into it by running:
Requirements: [Go](https://go.dev/dl/) >= v1.22
To build:
```
go build .
```bash
mkdir config && wget -O config/glance.yml https://raw.githubusercontent.com/glanceapp/glance/refs/heads/main/docs/glance.yml
```
To run:
Feel free to edit the `glance.yml` file to your liking, and when ready run:
```bash
docker compose up -d
```
If you encounter any issues, you can check the logs by running:
```bash
docker logs glance
```
<hr>
</details>
<details>
<summary><strong>Manual binary installation</strong></summary>
<br>
Precompiled binaries are available for Linux, Windows and macOS (x86, x86_64, ARM and ARM64 architectures).
### Linux
Visit the [latest release page](https://github.com/glanceapp/glance/releases/latest) for available binaries. You can place the binary in `/opt/glance/` and have it start with your server via a [systemd service](https://linuxhandbook.com/create-systemd-services/). By default, when running the binary, it will look for a `glance.yml` file in the directory it's placed in. To specify a different path for the config file, use the `--config` option:
```bash
/opt/glance/glance --config /etc/glance.yml
```
To grab a starting template for the config file, run:
```bash
wget https://raw.githubusercontent.com/glanceapp/glance/refs/heads/main/docs/glance.yml
```
### Windows
Download and extract the executable from the [latest release](https://github.com/glanceapp/glance/releases/latest) (most likely the file called `glance-windows-amd64.zip` if you're on a 64-bit system) and place it in a folder of your choice. Then, create a new text file called `glance.yml` in the same folder and paste the content from [here](https://raw.githubusercontent.com/glanceapp/glance/refs/heads/main/docs/glance.yml) in it. You should then be able to run the executable and access the dashboard by visiting `http://localhost:8080` in your browser.
<hr>
</details>
<details>
<summary><strong>Other</strong></summary>
<br>
Glance can also be installed through the following 3rd party channels:
* [Proxmox VE Helper Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=glance)
* [NixOS package](https://search.nixos.org/packages?channel=unstable&show=glance)
* [Coolify.io](https://coolify.io/docs/services/glance/)
<hr>
</details>
<br>
## Common issues
<details>
<summary><strong>Requests timing out</strong></summary>
The most common cause of this is when using Pi-Hole, AdGuard Home or other ad-blocking DNS services, which by default have a fairly low rate limit. Depending on the number of widgets you have in a single page, this limit can very easily be exceeded. To fix this, increase the rate limit in the settings of your DNS service.
If using Podman, in some rare cases the timeout can be caused by an unknown issue, in which case it may be resolved by adding the following to the bottom of your `docker-compose.yml` file:
```yaml
networks:
podman:
external: true
```
</details>
<details>
<summary><strong>Broken layout for markets, bookmarks or other widgets</strong></summary>
This is almost always caused by the browser extension Dark Reader. To fix this, disable dark mode for the domain where Glance is hosted.
</details>
<details>
<summary><strong>cannot unmarshal !!map into []glance.page</strong></summary>
The most common cause of this is having a `pages` key in your `glance.yml` and then also having a `pages` key inside one of your included pages. To fix this, remove the `pages` key from the top of your included pages.
</details>
<br>
## FAQ
<details>
<summary><strong>Does the information on the page update automatically?</strong></summary>
No, a page refresh is required to update the information. Some things do dynamically update where it makes sense, like the clock widget and the relative time showing how long ago something happened.
</details>
<details>
<summary><strong>How frequently do widgets update?</strong></summary>
No requests are made periodically in the background, information is only fetched upon loading the page and then cached. The default cache lifetime is different for each widget and can be configured.
</details>
<details>
<summary><strong>Can I create my own widgets?</strong></summary>
Yes, there are multiple ways to create custom widgets:
* `iframe` widget - allows you to embed things from other websites
* `html` widget - allows you to insert your own static HTML
* `extension` widget - fetch HTML from a URL
* `custom-api` widget - fetch JSON from a URL and render it using custom HTML
</details>
<details>
<summary><strong>Can I change the title of a widget?</strong></summary>
Yes, the title of all widgets can be changed by specifying the `title` property in the widget's configuration:
```yaml
- type: rss
title: My custom title
- type: markets
title: My custom title
- type: videos
title: My custom title
# and so on for all widgets...
```
</details>
<br>
## Feature requests
New feature suggestions are always welcome and will be considered, though please keep in mind that some of them may be out of scope for what the project is trying to achieve (or is reasonably capable of). If you have an idea for a new feature and would like to share it, you can do so [here](https://github.com/glanceapp/glance/issues/new?template=feature_request.yml).
Feature requests are tagged with one of the following:
* [Roadmap](https://github.com/glanceapp/glance/labels/roadmap) - will be implemented in a future release
* [Backlog](https://github.com/glanceapp/glance/labels/backlog) - may be implemented in the future but needs further feedback or interest from the community
* [Icebox](https://github.com/glanceapp/glance/labels/icebox) - no plans to implement as it doesn't currently align with the project's goals or capabilities, may be revised at a later date
<br>
## Building from source
Choose one of the following methods:
<details>
<summary><strong>Build binary with Go</strong></summary>
<br>
Requirements: [Go](https://go.dev/dl/) >= v1.23
To build the project for your current OS and architecture, run:
```bash
go build -o build/glance .
```
To build for a specific OS and architecture, run:
```bash
GOOS=linux GOARCH=amd64 go build -o build/glance .
```
[*click here for a full list of GOOS and GOARCH combinations*](https://go.dev/doc/install/source#:~:text=$GOOS%20and%20$GOARCH)
Alternatively, if you just want to run the app without creating a binary, like when you're testing out changes, you can run:
```bash
go run .
```
<hr>
</details>
<details>
<summary><strong>Build project and Docker image with Docker</strong></summary>
<br>
Requirements: [Docker](https://docs.docker.com/engine/install/)
To build the project and image using just Docker, run:
*(replace `owner` with your name or organization)*
```bash
docker build -t owner/glance:latest .
```
If you wish to push the image to a registry (by default Docker Hub), run:
```bash
docker push owner/glance:latest
```
<hr>
</details>
<br>
## Contributing guidelines
* Before working on a new feature it's preferable to submit a feature request first and state that you'd like to implement it yourself
* Please don't submit PRs for feature requests that are either in the roadmap<sup>[1]</sup>, backlog<sup>[2]</sup> or icebox<sup>[3]</sup>
* Use `dev` for the base branch if you're adding new features or fixing bugs, otherwise use `main`
* Avoid introducing new dependencies
* Avoid making backwards-incompatible configuration changes
* Avoid introducing new colors or hard-coding colors, use the standard `primary`, `positive` and `negative`
* For icons, try to use [heroicons](https://heroicons.com/) where applicable
* Provide a screenshot of the changes if UI related where possible
* No `package.json`
<details>
<summary><strong><sup>[1] [2] [3]</sup></strong></summary>
[1] The feature likely already has work put into it that may conflict with your implementation
[2] The demand, implementation or functionality for this feature is not yet clear
[3] No plans to add this feature for the time being
</details>
<br>
## Thank you
To all the people who were generous enough to [sponsor](https://github.com/sponsors/glanceapp) the project and to everyone who has contributed in any way, be it PRs, submitting issues, helping others in the discussions or Discord server, creating guides and tools or just mentioning Glance on social media. Your support is greatly appreciated and helps keep the project going.

File diff suppressed because it is too large Load diff

415
docs/custom-api.md Normal file
View file

@ -0,0 +1,415 @@
[Jump to function definitions](#functions)
## Examples
The best way to get an idea of how the templates work would be with a bunch examples. Here are the most common use cases:
JSON response:
```json
{
"title": "My Title",
"content": "My Content",
}
```
To access the two fields in the JSON response, you would use the following:
```html
<div>{{ .JSON.String "title" }}</div>
<div>{{ .JSON.String "content" }}</div>
```
Output:
```html
<div>My Title</div>
<div>My Content</div>
```
<hr>
JSON response:
```json
{
"author": "John Doe",
"posts": [
{
"title": "My Title",
"content": "My Content"
},
{
"title": "My Title 2",
"content": "My Content 2"
}
]
}
```
To loop through the array of posts, you would use the following:
```html
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div>{{ .String "content" }}</div>
{{ end }}
```
Output:
```html
<div>My Title</div>
<div>My Content</div>
<div>My Title 2</div>
<div>My Content 2</div>
```
Notice the missing `.JSON` when accessing the title and content, this is because the range function sets the context to the current array element.
If you want to access the top-level context within the range, you can use the following:
```html
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div>{{ .String "content" }}</div>
<div>{{ $.JSON.String "author" }}</div>
{{ end }}
```
Output:
```html
<div>My Title</div>
<div>My Content</div>
<div>John Doe</div>
<div>My Title 2</div>
<div>My Content 2</div>
<div>John Doe</div>
```
<hr>
JSON response:
```json
[
"Apple",
"Banana",
"Cherry",
"Watermelon"
]
```
Somewhat awkwardly, when the current context is a basic type that isn't an object, the way you specify its type is to use an empty string as the key. So, to loop through the array of strings, you would use the following:
```html
{{ range .JSON.Array "" }}
<div>{{ .String "" }}</div>
{{ end }}
```
Output:
```html
<div>Apple</div>
<div>Banana</div>
<div>Cherry</div>
<div>Watermelon</div>
```
To access an item at a specific index, you could use the following:
```html
<div>{{ .JSON.String "0" }}</div>
```
Output:
```html
<div>Apple</div>
```
<hr>
JSON response:
```json
{
"user": {
"address": {
"city": "New York",
"state": "NY"
}
}
}
```
To easily access deeply nested objects, you can use the following dot notation:
```html
<div>{{ .JSON.String "user.address.city" }}</div>
<div>{{ .JSON.String "user.address.state" }}</div>
```
Output:
```html
<div>New York</div>
<div>NY</div>
```
Using indexes anywhere in the path is also supported:
```json
{
"users": [
{
"name": "John Doe"
},
{
"name": "Jane Doe"
}
]
}
```
```html
<div>{{ .JSON.String "users.0.name" }}</div>
<div>{{ .JSON.String "users.1.name" }}</div>
```
Output:
```html
<div>John Doe</div>
<div>Jane Doe</div>
```
<hr>
JSON response:
```json
{
"user": {
"name": "John Doe",
"age": 30
}
}
```
To check if a field exists, you can use the following:
```html
{{ if .JSON.Exists "user.age" }}
<div>{{ .JSON.Int "user.age" }}</div>
{{ else }}
<div>Age not provided</div>
{{ end }}
```
Output:
```html
<div>30</div>
```
<hr>
JSON response:
```json
{
"price": 100,
"discount": 10
}
```
Calculations can be performed on either ints or floats. If both numbers are ints, an int will be returned, otherwise a float will be returned. If you try to divide by zero, 0 will be returned. If you provide non-numeric values, `NaN` will be returned.
```html
<div>{{ sub (.JSON.Int "price") (.JSON.Int "discount") }}</div>
```
Output:
```html
<div>90</div>
```
Other operations include `add`, `mul`, and `div`.
<hr>
JSON response:
```json
{
"posts": [
{
"title": "Exploring the Depths of Quantum Computing",
"date": "2023-10-27T10:00:00Z"
},
{
"title": "A Beginner's Guide to Sustainable Living",
"date": "2023-11-15T14:30:00+01:00"
},
{
"title": "The Art of Baking Sourdough Bread",
"date": "2023-12-03T08:45:22-08:00"
}
]
}
```
To parse the date and display the relative time (e.g. 2h, 1d, etc), you would use the following:
```
{{ range .JSON.Array "posts" }}
<div>{{ .String "title" }}</div>
<div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
{{ end }}
```
The `parseTime` function takes two arguments: the layout of the date string and the date string itself. The layout can be one of the following: "RFC3339", "RFC3339Nano", "DateTime", "DateOnly", "TimeOnly" or a custom layout in Go's [date format](https://pkg.go.dev/time#pkg-constants).
Output:
```html
<div>Exploring the Depths of Quantum Computing</div>
<div data-dynamic-relative-time="1698400800"></div>
<div>A Beginner's Guide to Sustainable Living</div>
<div data-dynamic-relative-time="1700055000"></div>
<div>The Art of Baking Sourdough Bread</div>
<div data-dynamic-relative-time="1701621922"></div>
```
You don't have to worry about the internal implementation, this will then be dynamically populated by Glance on the client side to show the correct relative time.
The important thing to notice here is that the return value of `toRelativeTime` must be used as an attribute in an HTML tag, be it a `div`, `li`, `span`, etc.
<hr>
In some instances, you may want to know the status code of the response. This can be done using the following:
```html
{{ if eq .Response.StatusCode 200 }}
<p>Success!</p>
{{ else }}
<p>Failed to fetch data</p>
{{ end }}
```
You can also access the response headers:
```html
<div>{{ .Response.Header.Get "Content-Type" }}</div>
```
<hr>
JSON response:
```json
{"name": "Steve", "age": 30}
{"name": "Alex", "age": 25}
{"name": "John", "age": 35}
```
The above format is "[ndjson](https://docs.mulesoft.com/dataweave/latest/dataweave-formats-ndjson)" or "[JSON Lines](https://jsonlines.org/)", where each line is a separate JSON object. To parse this format, you must first disable the JSON validation check in your config, since by default the response is expected to be a single valid JSON object:
```yaml
- type: custom-api
skip-json-validation: true
```
Then, to iterate over each object you can use `.JSONLines`:
```html
{{ range .JSONLines }}
<p>{{ .String "name" }} is {{ .Int "age" }} years old</p>
{{ end }}
```
Output:
```html
<p>Steve is 30 years old</p>
<p>Alex is 25 years old</p>
<p>John is 35 years old</p>
```
For other ways of selecting data from a JSON Lines response, have a look at the docs for [tidwall/gjson](https://github.com/tidwall/gjson/tree/master?tab=readme-ov-file#json-lines). For example, to get an array of all names, you can use the following:
```html
{{ range .JSON.Array "..#.name" }}
<p>{{ .String "" }}</p>
{{ end }}
```
Output:
```html
<p>Steve</p>
<p>Alex</p>
<p>John</p>
```
## Functions
The following functions are available on the `JSON` object:
- `String(key string) string`: Returns the value of the key as a string.
- `Int(key string) int`: Returns the value of the key as an integer.
- `Float(key string) float`: Returns the value of the key as a float.
- `Bool(key string) bool`: Returns the value of the key as a boolean.
- `Array(key string) []JSON`: Returns the value of the key as an array of `JSON` objects.
- `Exists(key string) bool`: Returns true if the key exists in the JSON object.
The following helper functions provided by Glance are available:
- `toFloat(i int) float`: Converts an integer to a float.
- `toInt(f float) int`: Converts a float to an integer.
- `toRelativeTime(t time.Time) template.HTMLAttr`: Converts Time to a relative time such as 2h, 1d, etc which dynamically updates. **NOTE:** the value of this function should be used as an attribute in an HTML tag, e.g. `<span {{ toRelativeTime .Time }}></span>`.
- `now() time.Time`: Returns the current time.
- `offsetNow(offset string) time.Time`: Returns the current time with an offset. The offset can be positive or negative and must be in the format "3h" "-1h" or "2h30m10s".
- `duration(str string) time.Duration`: Parses a string such as `1h`, `24h`, `5h30m`, etc into a `time.Duration`.
- `parseTime(layout string, s string) time.Time`: Parses a string into time.Time. The layout must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants). You can alternatively use these values instead of the literal format: "unix", "RFC3339", "RFC3339Nano", "DateTime", "DateOnly".
- `parseRelativeTime(layout string, s string) time.Time`: A shorthand for `{{ .String "date" | parseTime "rfc3339" | toRelativeTime }}`.
- `add(a, b float) float`: Adds two numbers.
- `sub(a, b float) float`: Subtracts two numbers.
- `mul(a, b float) float`: Multiplies two numbers.
- `div(a, b float) float`: Divides two numbers.
- `formatApproxNumber(n int) string`: Formats a number to be more human-readable, e.g. 1000 -> 1k.
- `formatNumber(n float|int) string`: Formats a number with commas, e.g. 1000 -> 1,000.
- `trimPrefix(prefix string, str string) string`: Trims the prefix from a string.
- `trimSuffix(suffix string, str string) string`: Trims the suffix from a string.
- `trimSpace(str string) string`: Trims whitespace from a string on both ends.
- `replaceAll(old string, new string, str string) string`: Replaces all occurrences of a string in a string.
- `replaceMatches(pattern string, replacement string, str string) string`: Replaces all occurrences of a regular expression in a string.
- `findMatch(pattern string, str string) string`: Finds the first match of a regular expression in a string.
- `findSubmatch(pattern string, str string) string`: Finds the first submatch of a regular expression in a string.
- `sortByString(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a string key in either ascending or descending order.
- `sortByInt(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by an integer key in either ascending or descending order.
- `sortByFloat(key string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a float key in either ascending or descending order.
- `sortByTime(key string, layout string, order string, arr []JSON): []JSON`: Sorts an array of JSON objects by a time key in either ascending or descending order. The format must be provided in Go's [date format](https://pkg.go.dev/time#pkg-constants).
- `concat(strings ...string) string`: Concatenates multiple strings together.
- `unique(key string, arr []JSON) []JSON`: Returns a unique array of JSON objects based on the given key.
The following helper functions provided by Go's `text/template` are available:
- `eq(a, b any) bool`: Compares two values for equality.
- `ne(a, b any) bool`: Compares two values for inequality.
- `lt(a, b any) bool`: Compares two values for less than.
- `lte(a, b any) bool`: Compares two values for less than or equal to.
- `gt(a, b any) bool`: Compares two values for greater than.
- `gte(a, b any) bool`: Compares two values for greater than or equal to.
- `and(a, b bool) bool`: Returns true if both values are true.
- `or(a, b bool) bool`: Returns true if either value is true.
- `not(a bool) bool`: Returns the opposite of the value.
- `index(a any, b int) any`: Returns the value at the specified index of an array.
- `len(a any) int`: Returns the length of an array.
- `printf(format string, a ...any) string`: Returns a formatted string.

164
docs/extensions.md Normal file
View file

@ -0,0 +1,164 @@
# Extensions
> [!IMPORTANT]
>
> **This document as well as the extensions feature are a work in progress. The API may change in the future. You are responsible for maintaining your own extensions.**
## Overview
With the intention of requiring minimal knowledge in order to develop extensions, rather than being a convoluted protocol they are nothing more than an HTTP request to a server that returns a few special headers. The exchange between Glance and extensions can be seen in the following diagram:
![](images/extension-overview.png)
If you know how to setup an HTTP server and a bit of HTML and CSS you're ready to start building your own extensions.
> [!TIP]
>
> By default, the extension widget has a cache time of 30 minutes. To avoid having to restart Glance after every extension change you can set the cache time of the widget to 1 second:
> ```yaml
> - type: extension
> url: http://localhost:8081
> cache: 1s
> ```
## Headers
### `Widget-Title`
Used to specify the title of the widget. If not provided, the widget's title will be "Extension".
### `Widget-Title-URL`
Used to specify the URL that will be opened when the widget's title is clicked. If the user has specified a `title-url` in their config, it will take precedence over this header.
### `Widget-Content-Type`
Used to specify the content type that will be returned by the extension. If not provided, the content will be shown as plain text.
### `Widget-Content-Frameless`
When set to `true`, the widget's content will be displayed without the default background or "frame".
## Content Types
> [!NOTE]
>
> Currently, `html` is the only supported content type. The long-term goal is to have generic content types such as `videos`, `forum-posts`, `markets`, `streams`, etc. which will be returned in JSON format and displayed by Glance using existing styles and functionality, allowing extension developers to achieve a native look while only focusing on providing data from their preferred source.
### `html`
Displays the content as HTML. This requires the user to have the `allow-potentially-dangerous-html` property set to `true`, otherwise the content will be shown as plain text.
#### Using existing classes and functionality
Most of the features seen throughout Glance can easily be used in your custom HTML extensions. Below is an example of some of these features:
```html
<p class="color-subdue">Text with subdued color</p>
<p>Text with base color</p>
<p class="color-highlight">Text with highlighted color</p>
<p class="color-primary">Text with primary color</p>
<p class="color-positive">Text with positive color</p>
<p class="color-negative">Text with negative color</p>
<hr class="margin-block-15">
<p class="size-h1">Font size 1</p>
<p class="size-h2">Font size 2</p>
<p class="size-h3">Font size 3</p>
<p class="size-h4">Font size 4</p>
<p class="size-base">Font size base</p>
<p class="size-h5">Font size 5</p>
<p class="size-h6">Font size 6</p>
<hr class="margin-block-15">
<a class="visited-indicator" href="#notvisitedprobably">Link with visited indicator</a>
<hr class="margin-block-15">
<a class="color-primary-if-not-visited" href="#notvisitedprobably">Link with primary color if not visited</a>
<hr class="margin-block-15">
<p>Event happened <span data-dynamic-relative-time="<unix timestamp>"></span> ago</p>
<hr class="margin-block-15">
<ul class="list-horizontal-text">
<li>horizontal</li>
<li>list</li>
<li>with</li>
<li>multiple</li>
<li>text</li>
<li>items</li>
</ul>
<hr class="margin-block-15">
<ul class="list list-gap-10 list-with-separator">
<li>list</li>
<li>with</li>
<li>gap</li>
<li>and</li>
<li>horizontal</li>
<li>lines</li>
</ul>
<hr class="margin-block-15">
<ul class="list collapsible-container" data-collapse-after="3">
<li>collapsible</li>
<li>list</li>
<li>with</li>
<li>many</li>
<li>items</li>
<li>that</li>
<li>will</li>
<li>appear</li>
<li>when</li>
<li>you</li>
<li>click</li>
<li>the</li>
<li>button</li>
<li>below</li>
</ul>
<hr class="margin-bottom-15">
<p class="margin-bottom-10">Lazily loaded image:</p>
<img src="https://picsum.photos/200" alt="" loading="lazy">
<hr class="margin-block-15">
<p class="margin-bottom-10">List of posts:</p>
<ul class="list list-gap-14 collapsible-container" data-collapse-after="5">
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
<li>
<a class="size-h3 color-primary-if-not-visited" href="#link">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Voluptatum, ipsa?</a>
<ul class="list-horizontal-text">
<li data-dynamic-relative-time="<unix timestamp>"></li>
<li>3,321 points</li>
<li>139 comments</li>
</ul>
</li>
</ul>
```
All of that will result in the following:
![](images/extension-html-reusing-existing-features-preview.png)
**Class names or features may change, once again, you are responsible for maintaining your own extensions.**

105
docs/glance.yml Normal file
View file

@ -0,0 +1,105 @@
pages:
- name: Home
# Optionally, if you only have a single page you can hide the desktop navigation for a cleaner look
# hide-desktop-navigation: true
columns:
- size: small
widgets:
- type: calendar
first-day-of-week: monday
- type: rss
limit: 10
collapse-after: 3
cache: 12h
feeds:
- url: https://selfh.st/rss/
title: selfh.st
limit: 4
- url: https://ciechanow.ski/atom.xml
- url: https://www.joshwcomeau.com/rss.xml
title: Josh Comeau
- url: https://samwho.dev/rss.xml
- url: https://ishadeed.com/feed.xml
title: Ahmad Shadeed
- type: twitch-channels
channels:
- theprimeagen
- j_blow
- piratesoftware
- cohhcarnage
- christitustech
- EJ_SA
- size: full
widgets:
- type: group
widgets:
- type: hacker-news
- type: lobsters
- type: videos
channels:
- UCXuqSBlHAE6Xw-yeJA0Tunw # Linus Tech Tips
- UCR-DXc1voovS8nhAvccRZhg # Jeff Geerling
- UCsBjURrPoezykLs9EqgamOA # Fireship
- UCBJycsmduvYEL83R_U4JriQ # Marques Brownlee
- UCHnyfMqiRRG1u-2MsSQLbXA # Veritasium
- type: group
widgets:
- type: reddit
subreddit: technology
show-thumbnails: true
- type: reddit
subreddit: selfhosted
show-thumbnails: true
- size: small
widgets:
- type: weather
location: London, United Kingdom
units: metric # alternatively "imperial"
hour-format: 12h # alternatively "24h"
# Optionally hide the location from being displayed in the widget
# hide-location: true
- type: markets
markets:
- symbol: SPY
name: S&P 500
- symbol: BTC-USD
name: Bitcoin
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- type: releases
cache: 1d
# Without authentication the Github API allows for up to 60 requests per hour. You can create a
# read-only token from your Github account settings and use it here to increase the limit.
# token: ...
repositories:
- glanceapp/glance
- go-gitea/gitea
- immich-app/immich
- syncthing/syncthing
# Add more pages here:
# - name: Your page name
# columns:
# - size: small
# widgets:
# # Add widgets here
# - size: full
# widgets:
# # Add widgets here
# - size: small
# widgets:
# # Add widgets here

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 946 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 637 KiB

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 261 KiB

After

Width:  |  Height:  |  Size: 310 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 370 KiB

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

View file

Before

Width:  |  Height:  |  Size: 341 KiB

After

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 549 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

226
docs/preconfigured-pages.md Normal file
View file

@ -0,0 +1,226 @@
# Preconfigured pages
Don't want to spend time configuring pages from scratch? No problem! Simply copy the config from the ones below.
Pull requests with your page configurations are welcome!
> [!NOTE]
>
> Pages must be placed under a top level `pages:` key, you can read more about that [here](configuration.md#pages).
## Startpage
![](images/startpage-preview.png)
<details>
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
```yaml
- name: Startpage
width: slim
hide-desktop-navigation: true
center-vertically: true
columns:
- size: full
widgets:
- type: search
autofocus: true
- type: monitor
cache: 1m
title: Services
sites:
- title: Jellyfin
url: https://yourdomain.com/
icon: si:jellyfin
- title: Gitea
url: https://yourdomain.com/
icon: si:gitea
- title: qBittorrent # only for Linux ISOs, of course
url: https://yourdomain.com/
icon: si:qbittorrent
- title: Immich
url: https://yourdomain.com/
icon: si:immich
- title: AdGuard Home
url: https://yourdomain.com/
icon: si:adguard
- title: Vaultwarden
url: https://yourdomain.com/
icon: si:vaultwarden
- type: bookmarks
groups:
- title: General
links:
- title: Gmail
url: https://mail.google.com/mail/u/0/
- title: Amazon
url: https://www.amazon.com/
- title: Github
url: https://github.com/
- title: Entertainment
links:
- title: YouTube
url: https://www.youtube.com/
- title: Prime Video
url: https://www.primevideo.com/
- title: Disney+
url: https://www.disneyplus.com/
- title: Social
links:
- title: Reddit
url: https://www.reddit.com/
- title: Twitter
url: https://twitter.com/
- title: Instagram
url: https://www.instagram.com/
```
</details>
## Markets
![](images/markets-page-preview.png)
<details>
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
```yaml
- name: Markets
columns:
- size: small
widgets:
- type: markets
title: Indices
markets:
- symbol: SPY
name: S&P 500
- symbol: DX-Y.NYB
name: Dollar Index
- type: markets
title: Crypto
markets:
- symbol: BTC-USD
name: Bitcoin
- symbol: ETH-USD
name: Ethereum
- type: markets
title: Stocks
sort-by: absolute-change
markets:
- symbol: NVDA
name: NVIDIA
- symbol: AAPL
name: Apple
- symbol: MSFT
name: Microsoft
- symbol: GOOGL
name: Google
- symbol: AMD
name: AMD
- symbol: RDDT
name: Reddit
- symbol: AMZN
name: Amazon
- symbol: TSLA
name: Tesla
- symbol: INTC
name: Intel
- symbol: META
name: Meta
- size: full
widgets:
- type: rss
title: News
style: horizontal-cards
feeds:
- url: https://feeds.bloomberg.com/markets/news.rss
title: Bloomberg
- url: https://moxie.foxbusiness.com/google-publisher/markets.xml
title: Fox Business
- url: https://moxie.foxbusiness.com/google-publisher/technology.xml
title: Fox Business
- type: group
widgets:
- type: reddit
show-thumbnails: true
subreddit: technology
- type: reddit
show-thumbnails: true
subreddit: wallstreetbets
- type: videos
style: grid-cards
collapse-after-rows: 3
channels:
- UCvSXMi2LebwJEM1s4bz5IBA # New Money
- UCV6KDgJskWaEckne5aPA0aQ # Graham Stephan
- UCAzhpt9DmG6PnHXjmJTvRGQ # Federal Reserve
- size: small
widgets:
- type: rss
title: News
limit: 30
collapse-after: 13
feeds:
- url: https://www.ft.com/technology?format=rss
title: Financial Times
- url: https://feeds.a.dj.com/rss/RSSMarketsMain.xml
title: Wall Street Journal
```
</details>
## Gaming
![](images/gaming-page-preview.png)
<details>
<summary>View config (requires Glance <code>v0.6.0</code> or higher)</summary>
```yaml
- name: Gaming
columns:
- size: small
widgets:
- type: twitch-top-games
limit: 20
collapse-after: 13
exclude:
- just-chatting
- pools-hot-tubs-and-beaches
- music
- art
- asmr
- size: full
widgets:
- type: group
widgets:
- type: reddit
show-thumbnails: true
subreddit: pcgaming
- type: reddit
subreddit: games
- type: videos
style: grid-cards
collapse-after-rows: 3
channels:
- UCNvzD7Z-g64bPXxGzaQaa4g # gameranx
- UCZ7AeeVbyslLM_8-nVy2B8Q # Skill Up
- UCHDxYLv8iovIbhrfl16CNyg # GameLinked
- UC9PBzalIcEQCsiIkq36PyUA # Digital Foundry
- size: small
widgets:
- type: reddit
subreddit: gamingnews
limit: 7
style: vertical-cards
```
</details>

View file

@ -11,8 +11,30 @@ theme:
contrast-multiplier: 1.1
```
### Catppuccin
![screenshot](images/themes/catppuccin.png)
### Catppuccin Frappe
![screenshot](images/themes/catppuccin-frappe.png)
```yaml
theme:
background-color: 229 19 23
contrast-multiplier: 1.2
primary-color: 222 74 74
positive-color: 96 44 68
negative-color: 359 68 71
```
### Catppuccin Macchiato
![screenshot](images/themes/catppuccin-macchiato.png)
```yaml
theme:
background-color: 232 23 18
contrast-multiplier: 1.2
primary-color: 220 83 75
positive-color: 105 48 72
negative-color: 351 74 73
```
### Catppuccin Mocha
![screenshot](images/themes/catppuccin-mocha.png)
```yaml
theme:
background-color: 240 21 15
@ -31,6 +53,26 @@ theme:
primary-color: 97 13 80
```
### Gruvbox Dark
![screenshot](images/themes/gruvbox.png)
```yaml
theme:
background-color: 0 0 16
primary-color: 43 59 81
positive-color: 61 66 44
negative-color: 6 96 59
```
### Kanagawa Dark
![screenshot](images/themes/kanagawa-dark.png)
```yaml
theme:
background-color: 240 13 14
primary-color: 51 33 68
negative-color: 358 100 68
contrast-multiplier: 1.2
```
### Tucan
![screenshot](images/themes/tucan.png)
```yaml
@ -40,8 +82,31 @@ theme:
negative-color: 209 88 54
```
### Dracula
![screenshot](images/themes/dracula.png)
```yaml
theme:
background-color: 231 15 21
primary-color: 265 89 79
contrast-multiplier: 1.2
positive-color: 135 94 66
negative-color: 0 100 67
```
## Light
### Catppuccin Latte
![screenshot](images/themes/catppuccin-latte.png)
```yaml
theme:
light: true
background-color: 220 23 95
contrast-multiplier: 1.0
primary-color: 220 91 54
positive-color: 109 58 40
negative-color: 347 87 44
```
### Peachy
![screenshot](images/themes/peachy.png)
```yaml

57
docs/v0.7.0-upgrade.md Normal file
View file

@ -0,0 +1,57 @@
## Upgrading to v0.7.0 from previous versions
In essence, the `glance.yml` file has been moved from the root of the project to a `config/` directory and you now need to mount that directory to `/app/config` in the container.
### Before
Versions before v0.7.0 used a `docker-compose.yml` that looked like the following:
```yaml
services:
glance:
image: glanceapp/glance
volumes:
- ./glance.yml:/app/glance.yml
ports:
- 8080:8080
```
And expected you to have the following directory structure:
```plaintext
glance/
docker-compose.yml
glance.yml
```
### After
With the release of v0.7.0, the recommended `docker-compose.yml` looks like the following:
```yaml
services:
glance:
container_name: glance
image: glanceapp/glance
volumes:
- ./config:/app/config
ports:
- 8080:8080
```
And expects you to have the following directory structure:
```plaintext
glance/
docker-compose.yml
config/
glance.yml
```
## Why this change was necessary
1. Mounting a file rather than a directory is not common practice and leads to some issues, such as creating a directory if the file is not present, which has tripped up multiple people and caused unnecessary confusion
2. v0.7.0 added automatic reloads when the configuration file changes, which based on testing didn't work when mounting a single file
3. v0.7.0 added the ability to include config files, so you'd have to make this change anyways if you wanted to take advantage of that feature
Taking all of these into account, it felt like the right time to implement the change.

27
go.mod
View file

@ -1,19 +1,32 @@
module github.com/glanceapp/glance
go 1.22.0
go 1.24.2
require (
golang.org/x/text v0.14.0
gopkg.in/yaml.v3 v3.0.1
github.com/fsnotify/fsnotify v1.9.0
github.com/mmcdole/gofeed v1.3.0
github.com/shirou/gopsutil/v4 v4.25.3
github.com/tidwall/gjson v1.18.0
golang.org/x/text v0.24.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/PuerkitoBio/goquery v1.8.0 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/PuerkitoBio/goquery v1.10.2 // indirect
github.com/andybalholm/cascadia v1.3.3 // indirect
github.com/ebitengine/purego v0.8.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 // indirect
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
github.com/mmcdole/goxpp v1.1.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.4.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect
github.com/tklauser/numcpus v0.10.0 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/net v0.39.0 // indirect
golang.org/x/sys v0.32.0 // indirect
)

138
go.sum
View file

@ -1,34 +1,148 @@
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8WTFQcU=
github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY=
github.com/PuerkitoBio/goquery v1.10.2 h1:7fh2BdHcG6VFZsK7toXBT/Bh1z5Wmy8Q9MV9HqT2AM8=
github.com/PuerkitoBio/goquery v1.10.2/go.mod h1:0guWGjcLu9AYC7C1GHnpysHy056u9aEkUHwhdnePMCU=
github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM=
github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View file

@ -1,15 +0,0 @@
package assets
import (
"embed"
"io/fs"
)
//go:embed static
var _publicFS embed.FS
//go:embed templates
var _templateFS embed.FS
var PublicFS, _ = fs.Sub(_publicFS, "static")
var TemplateFS, _ = fs.Sub(_templateFS, "templates")

File diff suppressed because it is too large Load diff

View file

@ -1,163 +0,0 @@
function throttledDebounce(callback, maxDebounceTimes, debounceDelay) {
let debounceTimeout;
let timesDebounced = 0;
return function () {
if (timesDebounced == maxDebounceTimes) {
clearTimeout(debounceTimeout);
timesDebounced = 0;
callback();
return;
}
clearTimeout(debounceTimeout);
timesDebounced++;
debounceTimeout = setTimeout(() => {
timesDebounced = 0;
callback();
}, debounceDelay);
};
};
async function fetchPageContents (pageSlug) {
// TODO: handle non 200 status codes/time outs
// TODO: add retries
const response = await fetch(`/api/pages/${pageSlug}/content/`);
const content = await response.text();
return content;
}
function setupCarousels() {
const carouselElements = document.getElementsByClassName("carousel-container");
for (let i = 0; i < carouselElements.length; i++) {
const carousel = carouselElements[i];
const itemsContainer = carousel.getElementsByClassName("carousel-items-container")[0];
const determineSideCutoffs = () => {
if (itemsContainer.scrollLeft != 0) {
carousel.classList.add("show-left-cutoff");
} else {
carousel.classList.remove("show-left-cutoff");
}
if (Math.ceil(itemsContainer.scrollLeft) + itemsContainer.clientWidth < itemsContainer.scrollWidth) {
carousel.classList.add("show-right-cutoff");
} else {
carousel.classList.remove("show-right-cutoff");
}
}
const determineSideCutoffsRateLimited = throttledDebounce(determineSideCutoffs, 20, 100);
itemsContainer.addEventListener("scroll", determineSideCutoffsRateLimited);
document.addEventListener("resize", determineSideCutoffsRateLimited);
determineSideCutoffs();
}
}
const minuteInSeconds = 60;
const hourInSeconds = minuteInSeconds * 60;
const dayInSeconds = hourInSeconds * 24;
const monthInSeconds = dayInSeconds * 30;
const yearInSeconds = monthInSeconds * 12;
function relativeTimeSince(timestamp) {
const delta = Math.round((Date.now() / 1000) - timestamp);
if (delta < minuteInSeconds) {
return "1m";
}
if (delta < hourInSeconds) {
return Math.floor(delta / minuteInSeconds) + "m";
}
if (delta < dayInSeconds) {
return Math.floor(delta / hourInSeconds) + "h";
}
if (delta < monthInSeconds) {
return Math.floor(delta / dayInSeconds) + "d";
}
if (delta < yearInSeconds) {
return Math.floor(delta / monthInSeconds) + "mo";
}
return Math.floor(delta / yearInSeconds) + "y";
}
function updateRelativeTimeForElements(elements)
{
for (let i = 0; i < elements.length; i++)
{
const element = elements[i];
const timestamp = element.dataset.dynamicRelativeTime;
if (timestamp === undefined)
continue
element.innerText = relativeTimeSince(timestamp);
}
}
function setupDynamicRelativeTime() {
const elements = document.querySelectorAll("[data-dynamic-relative-time]");
const updateInterval = 60 * 1000;
let lastUpdateTime = Date.now();
const updateElementsAndTimestamp = () => {
updateRelativeTimeForElements(elements);
lastUpdateTime = Date.now();
};
const scheduleRepeatingUpdate = () => setInterval(updateElementsAndTimestamp, updateInterval);
if (document.hidden === undefined) {
scheduleRepeatingUpdate();
return;
}
let timeout = scheduleRepeatingUpdate();
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
clearTimeout(timeout);
return;
}
const delta = Date.now() - lastUpdateTime;
if (delta >= updateInterval) {
updateElementsAndTimestamp();
timeout = scheduleRepeatingUpdate();
return;
}
timeout = setTimeout(() => {
updateElementsAndTimestamp();
timeout = scheduleRepeatingUpdate();
}, updateInterval - delta);
});
}
async function setupPage() {
const pageElement = document.getElementById("page");
const pageContents = await fetchPageContents(pageData.slug);
pageElement.innerHTML = pageContents;
setTimeout(() => {
document.body.classList.add("animate-element-transition");
}, 150);
setupCarousels();
setupDynamicRelativeTime();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", setupPage);
} else {
setupPage();
}

View file

@ -1,112 +0,0 @@
package assets
import (
"fmt"
"html/template"
"math"
"strconv"
"time"
"golang.org/x/text/language"
"golang.org/x/text/message"
)
var (
PageTemplate = compileTemplate("page.html", "document.html", "page-style-overrides.gotmpl")
PageContentTemplate = compileTemplate("content.html")
CalendarTemplate = compileTemplate("calendar.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")
ForumPostsTemplate = compileTemplate("forum-posts.html", "widget-base.html")
RedditCardsHorizontalTemplate = compileTemplate("reddit-horizontal-cards.html", "widget-base.html")
RedditCardsVerticalTemplate = compileTemplate("reddit-vertical-cards.html", "widget-base.html")
ReleasesTemplate = compileTemplate("releases.html", "widget-base.html")
VideosTemplate = compileTemplate("videos.html", "widget-base.html")
StocksTemplate = compileTemplate("stocks.html", "widget-base.html")
RSSListTemplate = compileTemplate("rss-list.html", "widget-base.html")
RSSCardsTemplate = compileTemplate("rss-cards.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")
)
var globalTemplateFunctions = template.FuncMap{
"relativeTime": relativeTimeSince,
"formatViewerCount": formatViewerCount,
"formatNumber": intl.Sprint,
"absInt": func(i int) int {
return int(math.Abs(float64(i)))
},
"formatPrice": func(price float64) string {
return intl.Sprintf("%.2f", price)
},
"formatTime": func(t time.Time) string {
return t.Format("2006-01-02 15:04:05")
},
"shouldCollapse": func(i int, collapseAfter int) bool {
if collapseAfter < -1 {
return false
}
return i >= collapseAfter
},
"itemAnimationDelay": func(i int, collapseAfter int) string {
return fmt.Sprintf("%dms", (i-collapseAfter)*30)
},
"dynamicRelativeTimeAttrs": func(t time.Time) template.HTMLAttr {
return template.HTMLAttr(fmt.Sprintf(`data-dynamic-relative-time="%d"`, t.Unix()))
},
}
func compileTemplate(primary string, dependencies ...string) *template.Template {
t, err := template.New(primary).
Funcs(globalTemplateFunctions).
ParseFS(TemplateFS, append([]string{primary}, dependencies...)...)
if err != nil {
panic(err)
}
return t
}
var intl = message.NewPrinter(language.English)
func formatViewerCount(count int) string {
if count < 1_000 {
return strconv.Itoa(count)
}
if count < 10_000 {
return fmt.Sprintf("%.1fk", float64(count)/1_000)
}
if count < 1_000_000 {
return fmt.Sprintf("%dk", count/1_000)
}
return fmt.Sprintf("%.1fm", float64(count)/1_000_000)
}
func relativeTimeSince(t time.Time) string {
delta := time.Since(t)
if delta < time.Minute {
return "1m"
}
if delta < time.Hour {
return fmt.Sprintf("%dm", delta/time.Minute)
}
if delta < 24*time.Hour {
return fmt.Sprintf("%dh", delta/time.Hour)
}
if delta < 30*24*time.Hour {
return fmt.Sprintf("%dd", delta/(24*time.Hour))
}
if delta < 12*30*24*time.Hour {
return fmt.Sprintf("%dmo", delta/(30*24*time.Hour))
}
return fmt.Sprintf("%dy", delta/(365*24*time.Hour))
}

View file

@ -1,16 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-24 list-with-separator">
{{ range .Groups }}
<li class="bookmarks-group"{{ if .Color }} style="--bookmarks-group-color: {{ .Color.AsCSSValue }}"{{ end }}>
{{ if ne .Title "" }}<div class="bookmarks-group-title size-h3 margin-bottom-3">{{ .Title }}</div>{{ end }}
<ul class="list list-gap-2">
{{ range .Links }}
<li><a href="{{ .URL }}" class="bookmarks-link color-highlight size-h4" target="_blank" rel="noreferrer">{{ .Title }}</a></li>
{{ end }}
</ul>
</li>
{{ end }}
</ul>
{{ end }}

View file

@ -1,27 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<div class="flex justify-between items-center">
<div class="color-highlight size-h1">{{ .Calendar.CurrentMonthName }}</div>
<ul class="list-horizontal-text color-highlight size-h4">
<li>Week {{ .Calendar.CurrentWeekNumber }}</li>
<li>{{ .Calendar.CurrentYear }}</li>
</ul>
</div>
<div class="flex flex-wrap size-h6 margin-top-10 color-subdue">
<div class="calendar-day">Mo</div>
<div class="calendar-day">Tu</div>
<div class="calendar-day">We</div>
<div class="calendar-day">Th</div>
<div class="calendar-day">Fr</div>
<div class="calendar-day">Sa</div>
<div class="calendar-day">Su</div>
</div>
<div class="flex flex-wrap">
{{ range .Calendar.Days }}
<div class="calendar-day{{ if eq . $.Calendar.CurrentDay }} calendar-day-today{{ end }}">{{ . }}</div>
{{ end }}
</div>
{{ end }}

View file

@ -1,17 +0,0 @@
<!DOCTYPE html>
<html {{ block "document-root-attrs" . }}{{ end }} lang="en" id="top">
<head>
{{ block "document-head-before" . }}{{ end }}
<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>
{{ block "document-head-after" . }}{{ end }}
</head>
<body>
{{ template "document-body" . }}
</body>
</html>

View file

@ -1,22 +0,0 @@
{{ 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 }}>
<a href="{{ $post.DiscussionUrl }}" class="size-h3 color-primary-if-not-visited" target="_blank" rel="noreferrer">{{ .Title }}</a>
<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>
{{ end }}
</ul>
</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 }}

View file

@ -1,39 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-20 list-with-separator">
{{ range .Sites }}
<li class="monitor-site flex items-center gap-15">
{{ if .IconUrl }}
<img class="monitor-site-icon" src="{{ .IconUrl }}" alt="" loading="lazy">
{{ end }}
<div>
<a class="size-h3 color-highlight" href="{{ .Url }}" target="_blank" rel="noreferrer">{{ .Title }}</a>
<ul class="list-horizontal-text">
{{ if not .Status.Error }}
<li>{{ .StatusText }}</li>
<li>{{ .Status.ResponseTime.Milliseconds | formatNumber }}ms</li>
{{ else if .Status.TimedOut }}
<li class="color-negative">Timed Out</li>
{{ else }}
<li class="color-negative" title="{{ .Status.Error }}">ERROR</li>
{{ end }}
</ul>
</div>
{{ if eq .StatusStyle "good" }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-positive)">
<path fill-rule="evenodd" d="M2.25 12c0-5.385 4.365-9.75 9.75-9.75s9.75 4.365 9.75 9.75-4.365 9.75-9.75 9.75S2.25 17.385 2.25 12Zm13.36-1.814a.75.75 0 1 0-1.22-.872l-3.236 4.53L9.53 12.22a.75.75 0 0 0-1.06 1.06l2.25 2.25a.75.75 0 0 0 1.14-.094l3.75-5.25Z" clip-rule="evenodd" />
</svg>
</div>
{{ else }}
<div class="monitor-site-status-icon">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="var(--color-negative)">
<path fill-rule="evenodd" d="M9.401 3.003c1.155-2 4.043-2 5.197 0l7.355 12.748c1.154 2-.29 4.5-2.599 4.5H4.645c-2.309 0-3.752-2.5-2.598-4.5L9.4 3.003ZM12 8.25a.75.75 0 0 1 .75.75v3.75a.75.75 0 0 1-1.5 0V9a.75.75 0 0 1 .75-.75Zm0 8.25a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5Z" clip-rule="evenodd" />
</svg>
</div>
{{ end }}
</li>
{{ end }}
</ul>
{{ end }}

View file

@ -1,14 +0,0 @@
<style>
:root {
{{ if .App.Config.Theme.BackgroundColor }}
--bgh: {{ .App.Config.Theme.BackgroundColor.Hue }};
--bgs: {{ .App.Config.Theme.BackgroundColor.Saturation }}%;
--bgl: {{ .App.Config.Theme.BackgroundColor.Lightness }}%;
{{ end }}
{{ if ne 0.0 .App.Config.Theme.ContrastMultiplier }}--cm: {{ .App.Config.Theme.ContrastMultiplier }};{{ end }}
{{ if ne 0.0 .App.Config.Theme.TextSaturationMultiplier }}--tsm: {{ .App.Config.Theme.TextSaturationMultiplier }};{{ end }}
{{ if .App.Config.Theme.PrimaryColor }}--color-primary: {{ .App.Config.Theme.PrimaryColor.AsCSSValue }};{{ end }}
{{ if .App.Config.Theme.PositiveColor }}--color-positive: {{ .App.Config.Theme.PositiveColor.AsCSSValue }};{{ end }}
{{ if .App.Config.Theme.NegativeColor }}--color-negative: {{ .App.Config.Theme.NegativeColor.AsCSSValue }};{{ end }}
}
</style>

View file

@ -1,64 +0,0 @@
{{ template "document.html" . }}
{{ define "document-title" }}{{ .Page.Title }} - Glance{{ end }}
{{ define "document-head-before" }}
<script>
const pageData = {
slug: "{{ .Page.Slug }}",
};
</script>
{{ end }}
{{ define "document-root-attrs" }}{{ if .App.Config.Theme.Light }}class="light-scheme"{{ end }}{{ end }}
{{ define "document-head-after" }}{{ template "page-style-overrides.gotmpl" . }}{{ end }}
{{ define "navigation-links" }}
{{ range .App.Config.Pages }}
<a href="/{{ .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 flex-grow">
{{ 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>
</div>
</div>
<div class="footer flex items-center flex-column">
<div>
<span class="size-h3">Glance</span> ({{ .App.Version }})
</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>
</div>
{{ end }}

View file

@ -1,21 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-14 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-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>
{{ 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 }}

View file

@ -1,20 +0,0 @@
{{ 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 }}>
<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>
</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 }}

View file

@ -1,23 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<ul class="list list-gap-20 list-with-separator">
{{ range .Stocks }}
<li class="flex items-center gap-15">
<div class="shrink min-width-0">
<div class="color-highlight size-h3 text-truncate">{{ .Symbol }}</div>
<div class="text-truncate">{{ .Name }}</div>
</div>
<svg class="stock-chart shrink-0" viewBox="0 0 100 50">
<polyline fill="none" stroke="var(--color-text-subdue)" stroke-width="1.5px" points="{{ .SvgChartPoints }}" vector-effect="non-scaling-stroke"></polyline>
</svg>
<div class="stock-values shrink-0">
<div class="size-h3 text-right {{ if eq .PercentChange 0.0 }}{{ else if gt .PercentChange 0.0 }}color-positive{{ else }}color-negative{{ end }}">{{ printf "%+.2f" .PercentChange }}%</div>
<div class="text-right">${{ .Price | formatPrice }}</div>
</div>
</li>
{{ end }}
</ul>
{{ end }}

View file

@ -1,40 +0,0 @@
{{ 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">
<div class="twitch-channel-avatar-container">
{{ if $channel.Exists }}
<img class="twitch-channel-avatar thumbnail" src="{{ $channel.AvatarUrl }}" alt="" loading="lazy">
{{ 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>
<ul class="list-horizontal-text">
<li title="{{ $channel.LiveSince | formatTime }}" {{ dynamicRelativeTimeAttrs $channel.LiveSince }}>{{ $channel.LiveSince | relativeTime }}</li>
<li>{{ $channel.ViewersCount | formatViewerCount }} viewers</li>
</ul>
{{ else }}
<div>Offline</div>
{{ end }}
{{ else }}
<div class="color-negative">Not found</div>
{{ end }}
</div>
</div>
</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 }}

View file

@ -1,35 +0,0 @@
{{ 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-horizontal-text">
<li>{{ $category.ViewersCount | formatViewerCount }} viewers</li>
{{ if $category.IsNew }}
<li class="color-primary">NEW</li>
{{ end }}
</ul>
<ul class="list-horizontal-text flex-nowrap">
{{ range $i, $tag := $category.Tags }}
{{ if eq $i 0 }}
<li class="shrink-0">{{ $tag.Name }}</li>
{{ else }}
<li class="text-truncate shrink min-width-0">{{ $tag.Name }}</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</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 }}

View file

@ -1,24 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content-classes" }}widget-content-frameless{{ end }}
{{ define "widget-content" }}
<div class="carousel-container">
<div class="videos cards-horizontal carousel-items-container">
{{ range .Videos }}
<div class="card widget-content-frame thumbnail-container">
<img class="video-thumbnail thumbnail" loading="lazy" src="{{ .ThumbnailUrl }}" alt="">
<div class="margin-top-10 margin-bottom-widget flex flex-column flex-grow padding-inline-widget">
<a class="video-title color-primary-if-not-visited" href="{{ .Url }}" target="_blank" rel="noreferrer" title="{{ .Title }}">{{ .Title }}</a>
<ul class="list-horizontal-text flex-nowrap margin-top-7">
<li class="shrink-0" title="{{ .TimePosted | formatTime }}" {{ dynamicRelativeTimeAttrs .TimePosted }}>{{ .TimePosted | relativeTime }}</li>
<li class="shrink min-width-0">
<a class="block text-truncate" href="{{ .AuthorUrl }}" target="_blank" rel="noreferrer">{{ .Author }}</a>
</li>
</ul>
</div>
</div>
{{ end }}
</div>
</div>
{{ end }}

View file

@ -1,29 +0,0 @@
{{ template "widget-base.html" . }}
{{ define "widget-content" }}
<div class="size-h2 color-highlight text-center">{{ .Weather.WeatherCodeAsString }}</div>
<div class="size-h4 text-center">Feels like {{ .Weather.ApparentTemperature }}°C</div>
<div class="weather-columns flex margin-top-15 justify-center">
{{ range $i, $column := .Weather.Columns }}
<div class="weather-column{{ if eq $i $.Weather.CurrentColumn }} weather-column-current{{ end }}">
{{ if $column.HasPrecipitation }}
<div class="weather-column-rain"></div>
{{ end }}
{{ if and (ge $i $.Weather.SunriseColumn) (le $i $.Weather.SunsetColumn ) }}
<div class="weather-column-daylight{{ if eq $i $.Weather.SunriseColumn }} weather-column-daylight-sunrise{{ else if eq $i $.Weather.SunsetColumn }} weather-column-daylight-sunset{{ end }}"></div>
{{ end }}
<div class="weather-column-value{{ if lt $column.Temperature 0 }} weather-column-value-negative{{ end }}">{{ $column.Temperature | absInt }}</div>
<div class="weather-bar" style='--weather-bar-height: {{ printf "%.2f" $column.Scale }}'></div>
<div class="weather-column-time">{{ index $.TimeLabels $i }}</div>
</div>
{{ end }}
</div>
{{ if not .HideLocation }}
<div class="flex items-center justify-center margin-top-15 gap-7 size-h5">
<div class="location-icon"></div>
<div class="text-truncate">{{ .Place.Name }}, {{ .Place.Country }}</div>
</div>
{{ end }}
{{ end }}

View file

@ -1,21 +0,0 @@
<div class="widget widget-type-{{ .GetType }}">
<div class="widget-header">
<div class="uppercase">{{ .Title }}</div>
{{ 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 }}">
{{ if .ContentAvailable }}
{{ block "widget-content" . }}{{ end }}
{{ else }}
<div class="widget-error-header">
<div class="color-negative size-h3">ERROR</div>
<div class="widget-error-icon"></div>
</div>
<p class="break-all">{{ if .Error }}{{ .Error }}{{ else }}No error information provided{{ end }}</p>
{{ end}}
</div>
</div>

View file

@ -1,53 +0,0 @@
package feed
import "time"
// TODO: very inflexible, refactor to allow more customizability
// TODO: allow changing first day of week
// TODO: allow changing between showing the previous and next week and the entire month
func NewCalendar(now time.Time) *Calendar {
year, week := now.ISOWeek()
weekday := now.Weekday()
if weekday == 0 {
weekday = 7
}
currentMonthDays := daysInMonth(now.Month(), year)
var previousMonthDays int
if previousMonthNumber := now.Month() - 1; previousMonthNumber < 1 {
previousMonthDays = daysInMonth(12, year-1)
} else {
previousMonthDays = daysInMonth(previousMonthNumber, year)
}
startDaysFrom := now.Day() - int(weekday+6)
days := make([]int, 21)
for i := 0; i < 21; i++ {
day := startDaysFrom + i
if day < 1 {
day = previousMonthDays + day
} else if day > currentMonthDays {
day = day - currentMonthDays
}
days[i] = day
}
return &Calendar{
CurrentDay: now.Day(),
CurrentWeekNumber: week,
CurrentMonthName: now.Month().String(),
CurrentYear: year,
Days: days,
}
}
func daysInMonth(m time.Month, year int) int {
return time.Date(year, m+1, 0, 0, 0, 0, 0, time.UTC).Day()
}

View file

@ -1,117 +0,0 @@
package feed
import (
"fmt"
"log/slog"
"net/http"
"time"
)
type githubReleaseResponseJson struct {
TagName string `json:"tag_name"`
PublishedAt string `json:"published_at"`
HtmlUrl string `json:"html_url"`
Draft bool `json:"draft"`
PreRelease bool `json:"prerelease"`
Reactions struct {
Downvotes int `json:"-1"`
} `json:"reactions"`
}
func parseGithubTime(t string) time.Time {
parsedTime, err := time.Parse("2006-01-02T15:04:05Z", t)
if err != nil {
return time.Now()
}
return parsedTime
}
func FetchLatestReleasesFromGithub(repositories []string, token string) (AppReleases, error) {
appReleases := make(AppReleases, 0, len(repositories))
if len(repositories) == 0 {
return appReleases, nil
}
requests := make([]*http.Request, len(repositories))
for i, repository := range repositories {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://api.github.com/repos/%s/releases?per_page=10", repository), nil)
if token != "" {
request.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
}
requests[i] = request
}
task := decodeJsonFromRequestTask[[]githubReleaseResponseJson](defaultClient)
job := newJob(task, requests).withWorkers(15)
responses, errs, err := workerPoolDo(job)
if err != nil {
return nil, err
}
var failed int
for i := range responses {
if errs[i] != nil {
failed++
slog.Error("Failed to fetch or parse github release", "error", errs[i], "url", requests[i].URL)
continue
}
releases := responses[i]
if len(releases) < 1 {
failed++
slog.Error("No releases found", "repository", repositories[i], "url", requests[i].URL)
continue
}
var liveRelease *githubReleaseResponseJson
for i := range releases {
release := &releases[i]
if !release.Draft && !release.PreRelease {
liveRelease = release
break
}
}
if liveRelease == nil {
slog.Error("No live release found", "repository", repositories[i], "url", requests[i].URL)
continue
}
version := liveRelease.TagName
if version[0] != 'v' {
version = "v" + version
}
appReleases = append(appReleases, AppRelease{
Name: repositories[i],
Version: version,
NotesUrl: liveRelease.HtmlUrl,
TimeReleased: parseGithubTime(liveRelease.PublishedAt),
Downvotes: liveRelease.Reactions.Downvotes,
})
}
if len(appReleases) == 0 {
return nil, ErrNoContent
}
appReleases.SortByNewest()
if failed > 0 {
return appReleases, fmt.Errorf("%w: could not get %d releases", ErrPartialContent, failed)
}
return appReleases, nil
}

View file

@ -1,89 +0,0 @@
package feed
import (
"fmt"
"log/slog"
"net/http"
"strconv"
"time"
)
type hackerNewsPostResponseJson struct {
Id int `json:"id"`
Score int `json:"score"`
Title string `json:"title"`
TargetUrl string `json:"url,omitempty"`
CommentCount int `json:"descendants"`
TimePosted int64 `json:"time"`
}
func getHackerNewsTopPostIds() ([]int, error) {
request, _ := http.NewRequest("GET", "https://hacker-news.firebaseio.com/v0/topstories.json", nil)
response, err := decodeJsonFromRequest[[]int](defaultClient, request)
if err != nil {
return nil, fmt.Errorf("%w: could not fetch list of post IDs", ErrNoContent)
}
return response, nil
}
func getHackerNewsPostsFromIds(postIds []int) (ForumPosts, error) {
requests := make([]*http.Request, len(postIds))
for i, id := range postIds {
request, _ := http.NewRequest("GET", fmt.Sprintf("https://hacker-news.firebaseio.com/v0/item/%d.json", id), nil)
requests[i] = request
}
task := decodeJsonFromRequestTask[hackerNewsPostResponseJson](defaultClient)
job := newJob(task, requests).withWorkers(30)
results, errs, err := workerPoolDo(job)
if err != nil {
return nil, err
}
posts := make(ForumPosts, 0, len(postIds))
for i := range results {
if errs[i] != nil {
slog.Error("Failed to fetch or parse hacker news post", "error", errs[i], "url", requests[i].URL)
continue
}
posts = append(posts, ForumPost{
Title: results[i].Title,
DiscussionUrl: "https://news.ycombinator.com/item?id=" + strconv.Itoa(results[i].Id),
TargetUrl: results[i].TargetUrl,
TargetUrlDomain: extractDomainFromUrl(results[i].TargetUrl),
CommentCount: results[i].CommentCount,
Score: results[i].Score,
TimePosted: time.Unix(results[i].TimePosted, 0),
})
}
if len(posts) == 0 {
return nil, ErrNoContent
}
if len(posts) != len(postIds) {
return posts, fmt.Errorf("%w could not fetch some hacker news posts", ErrPartialContent)
}
return posts, nil
}
func FetchHackerNewsTopPosts(limit int) (ForumPosts, error) {
postIds, err := getHackerNewsTopPostIds()
if err != nil {
return nil, err
}
if len(postIds) > limit {
postIds = postIds[:limit]
}
return getHackerNewsPostsFromIds(postIds)
}

View file

@ -1,51 +0,0 @@
package feed
import (
"context"
"errors"
"net/http"
"time"
)
type SiteStatus struct {
Code int
TimedOut bool
ResponseTime time.Duration
Error error
}
func getSiteStatusTask(request *http.Request) (SiteStatus, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
defer cancel()
request = request.WithContext(ctx)
start := time.Now()
response, err := http.DefaultClient.Do(request)
took := time.Since(start)
status := SiteStatus{ResponseTime: took}
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
status.TimedOut = true
}
status.Error = err
return status, err
}
defer response.Body.Close()
status.Code = response.StatusCode
return status, nil
}
func FetchStatusesForRequests(requests []*http.Request) ([]SiteStatus, error) {
job := newJob(getSiteStatusTask, requests).withWorkers(20)
results, _, err := workerPoolDo(job)
if err != nil {
return nil, err
}
return results, nil
}

View file

@ -1,151 +0,0 @@
package feed
import (
"fmt"
"math"
"net/http"
"net/url"
"slices"
"time"
_ "time/tzdata"
)
type PlacesResponseJson struct {
Results []PlaceJson
}
type PlaceJson struct {
Name string
Latitude float64
Longitude float64
Timezone string
Country string
location *time.Location
}
type WeatherResponseJson struct {
Daily struct {
Sunrise []int64 `json:"sunrise"`
Sunset []int64 `json:"sunset"`
} `json:"daily"`
Hourly struct {
Temperature []float64 `json:"temperature_2m"`
PrecipitationProbability []int `json:"precipitation_probability"`
} `json:"hourly"`
Current struct {
Temperature float64 `json:"temperature_2m"`
ApparentTemperature float64 `json:"apparent_temperature"`
WeatherCode int `json:"weather_code"`
} `json:"current"`
}
type weatherColumn struct {
Temperature int
Scale float64
HasPrecipitation bool
}
func FetchPlaceFromName(location string) (*PlaceJson, error) {
requestUrl := fmt.Sprintf("https://geocoding-api.open-meteo.com/v1/search?name=%s&count=1&language=en&format=json", url.QueryEscape(location))
request, _ := http.NewRequest("GET", requestUrl, nil)
responseJson, err := decodeJsonFromRequest[PlacesResponseJson](defaultClient, request)
if err != nil {
return nil, fmt.Errorf("could not fetch places data: %v", err)
}
if len(responseJson.Results) == 0 {
return nil, fmt.Errorf("no places found for %s", location)
}
place := &responseJson.Results[0]
loc, err := time.LoadLocation(place.Timezone)
if err != nil {
return nil, fmt.Errorf("could not load location: %v", err)
}
place.location = loc
return place, nil
}
func barIndexFromHour(h int) int {
return h / 2
}
// TODO: bunch of spaget, refactor
// TODO: allow changing between C and F
func FetchWeatherForPlace(place *PlaceJson) (*Weather, error) {
query := url.Values{}
query.Add("latitude", fmt.Sprintf("%f", place.Latitude))
query.Add("longitude", fmt.Sprintf("%f", place.Longitude))
query.Add("timeformat", "unixtime")
query.Add("timezone", place.Timezone)
query.Add("forecast_days", "1")
query.Add("current", "temperature_2m,apparent_temperature,weather_code,wind_speed_10m")
query.Add("hourly", "temperature_2m,precipitation_probability")
query.Add("daily", "sunrise,sunset")
requestUrl := "https://api.open-meteo.com/v1/forecast?" + query.Encode()
request, _ := http.NewRequest("GET", requestUrl, nil)
responseJson, err := decodeJsonFromRequest[WeatherResponseJson](defaultClient, request)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrNoContent, err)
}
now := time.Now().In(place.location)
bars := make([]weatherColumn, 0, 24)
currentBar := barIndexFromHour(now.Hour())
sunriseBar := barIndexFromHour(time.Unix(int64(responseJson.Daily.Sunrise[0]), 0).In(place.location).Hour())
sunsetBar := barIndexFromHour(time.Unix(int64(responseJson.Daily.Sunset[0]), 0).In(place.location).Hour()) - 1
if sunsetBar < 0 {
sunsetBar = 0
}
if len(responseJson.Hourly.Temperature) == 24 {
temperatures := make([]int, 12)
precipitations := make([]bool, 12)
t := responseJson.Hourly.Temperature
p := responseJson.Hourly.PrecipitationProbability
for i := 0; i < 24; i += 2 {
if i/2 == currentBar {
temperatures[i/2] = int(responseJson.Current.Temperature)
} else {
temperatures[i/2] = int(math.Round((t[i] + t[i+1]) / 2))
}
precipitations[i/2] = (p[i]+p[i+1])/2 > 75
}
minT := slices.Min(temperatures)
maxT := slices.Max(temperatures)
for i := 0; i < 12; i++ {
bars = append(bars, weatherColumn{
Temperature: temperatures[i],
Scale: float64(temperatures[i]-minT) / float64(maxT-minT),
HasPrecipitation: precipitations[i],
})
}
}
return &Weather{
Temperature: int(responseJson.Current.Temperature),
ApparentTemperature: int(responseJson.Current.ApparentTemperature),
WeatherCode: responseJson.Current.WeatherCode,
CurrentColumn: currentBar,
SunriseColumn: sunriseBar,
SunsetColumn: sunsetBar,
Columns: bars,
}, nil
}

View file

@ -1,183 +0,0 @@
package feed
import (
"math"
"sort"
"time"
)
type ForumPost struct {
Title string
DiscussionUrl string
TargetUrl string
TargetUrlDomain string
ThumbnailUrl string
CommentCount int
Score int
Engagement float64
TimePosted time.Time
}
type ForumPosts []ForumPost
type Calendar struct {
CurrentDay int
CurrentWeekNumber int
CurrentMonthName string
CurrentYear int
Days []int
}
type Weather struct {
Temperature int
ApparentTemperature int
WeatherCode int
CurrentColumn int
SunriseColumn int
SunsetColumn int
Columns []weatherColumn
}
type AppRelease struct {
Name string
Version string
NotesUrl string
TimeReleased time.Time
Downvotes int
}
type AppReleases []AppRelease
type Video struct {
ThumbnailUrl string
Title string
Url string
Author string
AuthorUrl string
TimePosted time.Time
}
type Videos []Video
type Stock struct {
Name string
Symbol string
Price float64
PercentChange float64
SvgChartPoints string
}
type Stocks []Stock
func (t Stocks) SortByAbsChange() {
sort.Slice(t, func(i, j int) bool {
return math.Abs(t[i].PercentChange) > math.Abs(t[j].PercentChange)
})
}
var weatherCodeTable = map[int]string{
0: "Clear Sky",
1: "Mainly Clear",
2: "Partly Cloudy",
3: "Overcast",
45: "Fog",
48: "Rime Fog",
51: "Drizzle",
53: "Drizzle",
55: "Drizzle",
56: "Drizzle",
57: "Drizzle",
61: "Rain",
63: "Moderate Rain",
65: "Heavy Rain",
66: "Freezing Rain",
67: "Freezing Rain",
71: "Snow",
73: "Moderate Snow",
75: "Heavy Snow",
77: "Snow Grains",
80: "Rain",
81: "Moderate Rain",
82: "Heavy Rain",
85: "Snow",
86: "Snow",
95: "Thunderstorm",
96: "Thunderstorm",
99: "Thunderstorm",
}
func (w *Weather) WeatherCodeAsString() string {
if weatherCode, ok := weatherCodeTable[w.WeatherCode]; ok {
return weatherCode
}
return ""
}
const depreciatePostsOlderThanHours = 7
const maxDepreciation = 0.9
const maxDepreciationAfterHours = 24
func (p ForumPosts) CalculateEngagement() {
var totalComments int
var totalScore int
for i := range p {
totalComments += p[i].CommentCount
totalScore += p[i].Score
}
numberOfPosts := float64(len(p))
averageComments := float64(totalComments) / numberOfPosts
averageScore := float64(totalScore) / numberOfPosts
for i := range p {
p[i].Engagement = (float64(p[i].CommentCount)/averageComments + float64(p[i].Score)/averageScore) / 2
elapsed := time.Since(p[i].TimePosted)
if elapsed < time.Hour*depreciatePostsOlderThanHours {
continue
}
p[i].Engagement *= 1.0 - (math.Max(elapsed.Hours()-depreciatePostsOlderThanHours, maxDepreciationAfterHours)/maxDepreciationAfterHours)*maxDepreciation
}
}
func (p ForumPosts) SortByEngagement() {
sort.Slice(p, func(i, j int) bool {
return p[i].Engagement > p[j].Engagement
})
}
func (s *ForumPost) HasTargetUrl() bool {
return s.TargetUrl != ""
}
func (p ForumPosts) FilterPostedBefore(postedBefore time.Duration) []ForumPost {
recent := make([]ForumPost, 0, len(p))
for i := range p {
if time.Since(p[i].TimePosted) < postedBefore {
recent = append(recent, p[i])
}
}
return recent
}
func (r AppReleases) SortByNewest() AppReleases {
sort.Slice(r, func(i, j int) bool {
return r[i].TimeReleased.After(r[j].TimeReleased)
})
return r
}
func (v Videos) SortByNewest() Videos {
sort.Slice(v, func(i, j int) bool {
return v[i].TimePosted.After(v[j].TimePosted)
})
return v
}

View file

@ -1,83 +0,0 @@
package feed
import (
"fmt"
"html"
"net/http"
"net/url"
"time"
)
type subredditResponseJson struct {
Data struct {
Children []struct {
Data struct {
Title string `json:"title"`
Upvotes int `json:"ups"`
Url string `json:"url"`
Time float64 `json:"created"`
CommentsCount int `json:"num_comments"`
Domain string `json:"domain"`
Permalink string `json:"permalink"`
Stickied bool `json:"stickied"`
Pinned bool `json:"pinned"`
IsSelf bool `json:"is_self"`
Thumbnail string `json:"thumbnail"`
} `json:"data"`
} `json:"children"`
} `json:"data"`
}
func FetchSubredditPosts(subreddit string) (ForumPosts, error) {
requestUrl := fmt.Sprintf("https://www.reddit.com/r/%s/hot.json", url.QueryEscape(subreddit))
request, err := http.NewRequest("GET", requestUrl, nil)
if err != nil {
return nil, err
}
// Required to increase rate limit, otherwise Reddit randomly returns 429 even after just 2 requests
addBrowserUserAgentHeader(request)
responseJson, err := decodeJsonFromRequest[subredditResponseJson](defaultClient, request)
if err != nil {
return nil, err
}
if len(responseJson.Data.Children) == 0 {
return nil, fmt.Errorf("no posts found")
}
posts := make(ForumPosts, 0, len(responseJson.Data.Children))
for i := range responseJson.Data.Children {
post := &responseJson.Data.Children[i].Data
if post.Stickied || post.Pinned {
continue
}
forumPost := ForumPost{
Title: html.UnescapeString(post.Title),
DiscussionUrl: "https://www.reddit.com" + post.Permalink,
TargetUrlDomain: post.Domain,
CommentCount: post.CommentsCount,
Score: post.Upvotes,
TimePosted: time.Unix(int64(post.Time), 0),
}
if post.Thumbnail != "" && post.Thumbnail != "self" && post.Thumbnail != "default" {
forumPost.ThumbnailUrl = post.Thumbnail
}
if !post.IsSelf {
forumPost.TargetUrl = post.Url
}
posts = append(posts, forumPost)
}
posts.CalculateEngagement()
return posts, nil
}

View file

@ -1,117 +0,0 @@
package feed
import (
"context"
"fmt"
"log/slog"
"sort"
"time"
"github.com/mmcdole/gofeed"
)
type RSSFeedItem struct {
ChannelName string
ChannelURL string
Title string
Link string
ImageURL string
PublishedAt time.Time
}
type RSSFeedRequest struct {
Url string `yaml:"url"`
Title string `yaml:"title"`
}
type RSSFeedItems []RSSFeedItem
func (f RSSFeedItems) SortByNewest() RSSFeedItems {
sort.Slice(f, func(i, j int) bool {
return f[i].PublishedAt.After(f[j].PublishedAt)
})
return f
}
var feedParser = gofeed.NewParser()
func getItemsFromRSSFeedTask(request RSSFeedRequest) ([]RSSFeedItem, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
feed, err := feedParser.ParseURLWithContext(request.Url, ctx)
if err != nil {
return nil, err
}
items := make(RSSFeedItems, 0, len(feed.Items))
for i := range feed.Items {
item := feed.Items[i]
rssItem := RSSFeedItem{
ChannelURL: feed.Link,
Title: item.Title,
Link: item.Link,
}
if request.Title != "" {
rssItem.ChannelName = request.Title
} else {
rssItem.ChannelName = feed.Title
}
if item.Image != nil {
rssItem.ImageURL = item.Image.URL
} else if feed.Image != nil {
rssItem.ImageURL = feed.Image.URL
}
if item.PublishedParsed != nil {
rssItem.PublishedAt = *item.PublishedParsed
} else {
rssItem.PublishedAt = time.Now()
}
items = append(items, rssItem)
}
return items, nil
}
func GetItemsFromRSSFeeds(requests []RSSFeedRequest) (RSSFeedItems, error) {
job := newJob(getItemsFromRSSFeedTask, requests).withWorkers(10)
feeds, errs, err := workerPoolDo(job)
if err != nil {
return nil, fmt.Errorf("%w: %v", ErrNoContent, err)
}
failed := 0
entries := make(RSSFeedItems, 0, len(feeds)*10)
for i := range feeds {
if errs[i] != nil {
failed++
slog.Error("failed to get rss feed", "error", errs[i], "url", requests[i].Url)
continue
}
entries = append(entries, feeds[i]...)
}
if len(entries) == 0 {
return nil, ErrNoContent
}
entries.SortByNewest()
if failed > 0 {
return entries, fmt.Errorf("%w: missing %d RSS feeds", ErrPartialContent, failed)
}
return entries, nil
}

View file

@ -1,248 +0,0 @@
package feed
import (
"encoding/json"
"errors"
"fmt"
"log/slog"
"net/http"
"slices"
"sort"
"strings"
"time"
)
type TwitchCategory struct {
Slug string `json:"slug"`
Name string `json:"name"`
AvatarUrl string `json:"avatarURL"`
ViewersCount int `json:"viewersCount"`
Tags []struct {
Name string `json:"tagName"`
} `json:"tags"`
GameReleaseDate string `json:"originalReleaseDate"`
IsNew bool `json:"-"`
}
type TwitchChannel struct {
Login string
Exists bool
Name string
AvatarUrl string
IsLive bool
LiveSince time.Time
Category string
CategorySlug string
ViewersCount int
}
type TwitchChannels []TwitchChannel
func (channels TwitchChannels) SortByViewers() {
sort.Slice(channels, func(i, j int) bool {
return channels[i].ViewersCount > channels[j].ViewersCount
})
}
type twitchOperationResponse struct {
Data json.RawMessage
Extensions struct {
OperationName string `json:"operationName"`
}
}
type twitchChannelShellOperationResponse struct {
UserOrError struct {
Type string `json:"__typename"`
DisplayName string `json:"displayName"`
ProfileImageUrl string `json:"profileImageURL"`
Stream *struct {
ViewersCount int `json:"viewersCount"`
}
} `json:"userOrError"`
}
type twitchStreamMetadataOperationResponse struct {
UserOrNull *struct {
Stream *struct {
StartedAt string `json:"createdAt"`
Game *struct {
Slug string `json:"slug"`
Name string `json:"name"`
} `json:"game"`
} `json:"stream"`
} `json:"user"`
}
type twitchDirectoriesOperationResponse struct {
Data struct {
DirectoriesWithTags struct {
Edges []struct {
Node TwitchCategory `json:"node"`
} `json:"edges"`
} `json:"directoriesWithTags"`
} `json:"data"`
}
const twitchGqlEndpoint = "https://gql.twitch.tv/gql"
const twitchGqlClientId = "kimne78kx3ncx6brgo4mv6wki5h1ko"
const twitchDirectoriesOperationRequestBody = `[{"operationName": "BrowsePage_AllDirectories","variables": {"limit": %d,"options": {"sort": "VIEWER_COUNT","tags": []}},"extensions": {"persistedQuery": {"version": 1,"sha256Hash": "2f67f71ba89f3c0ed26a141ec00da1defecb2303595f5cda4298169549783d9e"}}}]`
func FetchTopGamesFromTwitch(exclude []string, limit int) ([]TwitchCategory, error) {
reader := strings.NewReader(fmt.Sprintf(twitchDirectoriesOperationRequestBody, len(exclude)+limit))
request, _ := http.NewRequest("POST", twitchGqlEndpoint, reader)
request.Header.Add("Client-ID", twitchGqlClientId)
response, err := decodeJsonFromRequest[[]twitchDirectoriesOperationResponse](defaultClient, request)
if err != nil {
return nil, err
}
if len(response) == 0 {
return nil, errors.New("no categories could be retrieved")
}
edges := (response)[0].Data.DirectoriesWithTags.Edges
categories := make([]TwitchCategory, 0, len(edges))
for i := range edges {
if slices.Contains(exclude, edges[i].Node.Slug) {
continue
}
category := &edges[i].Node
category.AvatarUrl = strings.Replace(category.AvatarUrl, "285x380", "144x192", 1)
if len(category.Tags) > 2 {
category.Tags = category.Tags[:2]
}
gameReleasedDate, err := time.Parse("2006-01-02T15:04:05Z", category.GameReleaseDate)
if err == nil {
if time.Since(gameReleasedDate) < 14*24*time.Hour {
category.IsNew = true
}
}
categories = append(categories, *category)
}
if len(categories) > limit {
categories = categories[:limit]
}
return categories, nil
}
const twitchChannelStatusOperationRequestBody = `[{"operationName":"ChannelShell","variables":{"login":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"580ab410bcd0c1ad194224957ae2241e5d252b2c5173d8e0cce9d32d5bb14efe"}}},{"operationName":"StreamMetadata","variables":{"channelLogin":"%s"},"extensions":{"persistedQuery":{"version":1,"sha256Hash":"676ee2f834ede42eb4514cdb432b3134fefc12590080c9a2c9bb44a2a4a63266"}}}]`
// TODO: rework
// The operations for multiple channels can all be sent in a single request
// rather than sending a separate request for each channel. Need to figure out
// what the limit is for max operations per request and batch operations in
// multiple requests if number of channels exceeds allowed limit.
func fetchChannelFromTwitchTask(channel string) (TwitchChannel, error) {
result := TwitchChannel{
Login: strings.ToLower(channel),
}
reader := strings.NewReader(fmt.Sprintf(twitchChannelStatusOperationRequestBody, channel, channel))
request, _ := http.NewRequest("POST", twitchGqlEndpoint, reader)
request.Header.Add("Client-ID", twitchGqlClientId)
response, err := decodeJsonFromRequest[[]twitchOperationResponse](defaultClient, request)
if err != nil {
return result, err
}
if len(response) != 2 {
return result, fmt.Errorf("expected 2 operation responses, got %d", len(response))
}
var channelShell twitchChannelShellOperationResponse
var streamMetadata twitchStreamMetadataOperationResponse
for i := range response {
switch response[i].Extensions.OperationName {
case "ChannelShell":
err = json.Unmarshal(response[i].Data, &channelShell)
if err != nil {
return result, fmt.Errorf("failed to unmarshal channel shell: %w", err)
}
case "StreamMetadata":
err = json.Unmarshal(response[i].Data, &streamMetadata)
if err != nil {
return result, fmt.Errorf("failed to unmarshal stream metadata: %w", err)
}
default:
return result, fmt.Errorf("unknown operation name: %s", response[i].Extensions.OperationName)
}
}
if channelShell.UserOrError.Type != "User" {
result.Name = result.Login
return result, nil
}
result.Exists = true
result.Name = channelShell.UserOrError.DisplayName
result.AvatarUrl = channelShell.UserOrError.ProfileImageUrl
if channelShell.UserOrError.Stream != nil {
result.IsLive = true
result.ViewersCount = channelShell.UserOrError.Stream.ViewersCount
if streamMetadata.UserOrNull != nil && streamMetadata.UserOrNull.Stream != nil && streamMetadata.UserOrNull.Stream.Game != nil {
result.Category = streamMetadata.UserOrNull.Stream.Game.Name
result.CategorySlug = streamMetadata.UserOrNull.Stream.Game.Slug
startedAt, err := time.Parse("2006-01-02T15:04:05Z", streamMetadata.UserOrNull.Stream.StartedAt)
if err == nil {
result.LiveSince = startedAt
} else {
slog.Warn("failed to parse twitch stream started at", "error", err, "started_at", streamMetadata.UserOrNull.Stream.StartedAt)
}
}
}
return result, nil
}
func FetchChannelsFromTwitch(channelLogins []string) (TwitchChannels, error) {
result := make(TwitchChannels, 0, len(channelLogins))
job := newJob(fetchChannelFromTwitchTask, channelLogins).withWorkers(10)
channels, errs, err := workerPoolDo(job)
if err != nil {
return result, err
}
var failed int
for i := range channels {
if errs[i] != nil {
failed++
slog.Warn("failed to fetch twitch channel", "channel", channelLogins[i], "error", errs[i])
continue
}
result = append(result, channels[i])
}
if failed == len(channelLogins) {
return result, ErrNoContent
}
if failed > 0 {
return result, fmt.Errorf("%w: failed to fetch %d channels", ErrPartialContent, failed)
}
return result, nil
}

View file

@ -1,79 +0,0 @@
package feed
import (
"errors"
"fmt"
"net/url"
"slices"
"strings"
)
var (
ErrNoContent = errors.New("failed to retrieve any content")
ErrPartialContent = errors.New("failed to retrieve some of the content")
)
func percentChange(current, previous float64) float64 {
return (current/previous - 1) * 100
}
func extractDomainFromUrl(u string) string {
if u == "" {
return ""
}
parsed, err := url.Parse(u)
if err != nil {
return ""
}
return strings.TrimPrefix(parsed.Host, "www.")
}
func SvgPolylineCoordsFromYValues(width float64, height float64, values []float64) string {
if len(values) < 2 {
return ""
}
verticalPadding := height * 0.02
height -= verticalPadding * 2
coordinates := make([]string, len(values))
distanceBetweenPoints := width / float64(len(values)-1)
min := slices.Min(values)
max := slices.Max(values)
for i := range values {
coordinates[i] = fmt.Sprintf(
"%.2f,%.2f",
float64(i)*distanceBetweenPoints,
((max-values[i])/(max-min))*height+verticalPadding,
)
}
return strings.Join(coordinates, " ")
}
func maybeCopySliceWithoutZeroValues[T int | float64](values []T) []T {
if len(values) == 0 {
return values
}
for i := range values {
if values[i] != 0 {
continue
}
c := make([]T, 0, len(values)-1)
for i := range values {
if values[i] != 0 {
c = append(c, values[i])
}
}
return c
}
return values
}

Some files were not shown because too many files have changed in this diff Show more