Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
947a42ddd4 | ||
|
|
8e4f1380ce | ||
|
|
2351d909ca | ||
|
|
7a0a4b9780 | ||
|
|
e108feb74b | ||
|
|
7f36f30dad | ||
|
|
8ba6dcb704 | ||
|
|
4b8a34a2ae | ||
|
|
2e198a19b4 | ||
|
|
6c5b7a3f4c | ||
|
|
36d5ae023f | ||
|
|
478c08f6a7 | ||
|
|
cae90d16ba | ||
|
|
fbc07bd142 | ||
|
|
4a4d3e1755 | ||
|
|
f243a4938f | ||
|
|
9416de1497 | ||
|
|
283a5fcfd0 | ||
|
|
c88fd526e5 | ||
|
|
f0541ea5c8 | ||
|
|
8f986f1403 | ||
|
|
de9a192ba4 | ||
|
|
88d8fa56fb | ||
|
|
9044e640bc | ||
|
|
808f3c1436 | ||
|
|
d103c81df1 | ||
|
|
429f0be675 | ||
|
|
5a093f42b0 | ||
|
|
ded435df0e | ||
|
|
e52374fa24 | ||
|
|
b94647efc9 | ||
|
|
d512770c10 | ||
|
|
ea52318be6 | ||
|
|
9e5023522b | ||
|
|
4d0fdd9b11 | ||
|
|
78725c8591 | ||
|
|
f7adaad1c5 | ||
|
|
ab093cb232 | ||
|
|
2aaff02db8 | ||
|
|
f5bfd9d4d1 | ||
|
|
9e3639eb54 | ||
|
|
b4094b28bd | ||
|
|
b294839b79 | ||
|
|
14a21de37f | ||
|
|
d9239acbce | ||
|
|
a2247c0b6c | ||
|
|
c1aaec5ffc | ||
|
|
bcef9fbd61 | ||
|
|
32db59fda2 | ||
|
|
92bc68b61a | ||
|
|
a6382b2e1d | ||
|
|
571cdaf618 | ||
|
|
aa3b2c3b1b | ||
|
|
9bbf73db97 | ||
|
|
dc950e7ec2 | ||
|
|
91ca57e242 | ||
|
|
95541ef5e1 | ||
|
|
1ace129a58 | ||
|
|
af04605d7d | ||
|
|
0ec9cf4aaa | ||
|
|
a5b0664b9c | ||
|
|
c67eb4d2c0 | ||
|
|
e99ee4f774 | ||
|
|
74e6c5c960 | ||
|
|
f801da88a7 | ||
|
|
66dcd1fa34 | ||
|
|
4f9e48cc17 | ||
|
|
95702a2e53 | ||
|
|
9de6843a2e | ||
|
|
bd5cacd14a | ||
|
|
96007e98bc | ||
|
|
ae409768ac | ||
|
|
baaf306ebf | ||
|
|
7bbf103e01 | ||
|
|
49c07f397e | ||
|
|
dd91a506fa | ||
|
|
4ed8bef562 | ||
|
|
8db6544400 | ||
|
|
ca9e4d273b | ||
|
|
9324da6a2f | ||
|
|
ce3a7ee29c | ||
|
|
c9e6b774f3 | ||
|
|
9ffb2d9939 | ||
|
|
c5d4cf8f68 | ||
|
|
6b7d68d960 | ||
|
|
0cb8a810e6 | ||
|
|
40ae263248 | ||
|
|
bf97829814 | ||
|
|
b075607bac | ||
|
|
56bdd2c9a3 | ||
|
|
0f6e51ee5c | ||
|
|
1c5701fde5 | ||
|
|
6c1c909c9d | ||
|
|
26ab9c7b05 | ||
|
|
176f14df70 | ||
|
|
2d1e317c1f | ||
|
|
fcdea66a0f | ||
|
|
0e91958de5 | ||
|
|
6624b635df | ||
|
|
0612dc6f89 | ||
|
|
0769f33653 | ||
|
|
c2286f9a22 | ||
|
|
a2d8410fec | ||
|
|
a68805b55d | ||
|
|
8ca6879a1c | ||
|
|
1661f14adb | ||
|
|
7d08eb312f | ||
|
|
47340ed82a | ||
|
|
129441713b | ||
|
|
a3e9510c1d | ||
|
|
328ba9a239 | ||
|
|
3a3ff080ac | ||
|
|
e2d3e1c04f | ||
|
|
b3658bea09 | ||
|
|
1ad71f6974 | ||
|
|
9dde306c0c | ||
|
|
d7a17aab01 | ||
|
|
65adf9b9c3 | ||
|
|
aceb832645 | ||
|
|
ec2f549295 | ||
|
|
6e06c9c489 | ||
|
|
1862858c07 | ||
|
|
306c28f203 | ||
|
|
bba2c5b20c | ||
|
|
18436e91e0 | ||
|
|
77fb199cb3 | ||
|
|
272bf1d2ad | ||
|
|
ceebf34874 | ||
|
|
efd39e1f80 | ||
|
|
93c72f561b | ||
|
|
689e8f216c | ||
|
|
4e4c3cfe64 | ||
|
|
e2112e0d83 | ||
|
|
44fca91089 | ||
|
|
4ae40cbd1e | ||
|
|
ea04aec0b4 | ||
|
|
b8e2717faf | ||
|
|
333d40ed4f | ||
|
|
cb3555f5ed | ||
|
|
7a8f70db02 | ||
|
|
1cf4f520f8 | ||
|
|
e11ce5d3d0 | ||
|
|
502950709c | ||
|
|
2c3e14d84c | ||
|
|
8e71afe158 | ||
|
|
44ea98fc41 | ||
|
|
5214fb9145 | ||
|
|
41e74ba9b5 | ||
|
|
03b616622e | ||
|
|
50e1da01fe | ||
|
|
0daa620b11 | ||
|
|
f8f50b26d8 | ||
|
|
a3bc133bcb | ||
|
|
50dae22ff4 | ||
|
|
5a91154eab | ||
|
|
d5b89d512a | ||
|
|
c4e4c62072 | ||
|
|
33b54ded4f | ||
|
|
0d2f5a818b | ||
|
|
8f217e6f01 | ||
|
|
835d7767cb | ||
|
|
8892a39160 | ||
|
|
db97871945 | ||
|
|
90254b92d2 | ||
|
|
eeda2104a6 | ||
|
|
97f43f88eb | ||
|
|
793da48f5c | ||
|
|
3177af9524 | ||
|
|
f99d22738c | ||
|
|
ea7124db52 | ||
|
|
b1247e5de6 | ||
|
|
5ab962634e | ||
|
|
d99944dcff | ||
|
|
84f7286460 | ||
|
|
920bcb06dc | ||
|
|
3679cc21ab | ||
|
|
07e5626469 | ||
|
|
6bb6f87800 | ||
|
|
cb6b52ff4c | ||
|
|
03c377dff3 | ||
|
|
d9c934d3eb | ||
|
|
0bc31b0f31 | ||
|
|
5a38c8dd2f | ||
|
|
0e3f172032 | ||
|
|
fd704419d9 | ||
|
|
32ac72f592 | ||
|
|
15f20ffaeb | ||
|
|
9c77b87435 | ||
|
|
d4d9e94f44 | ||
|
|
47cef2abf0 | ||
|
|
0a5cdf380f | ||
|
|
7f0e9b3289 | ||
|
|
9da7967158 | ||
|
|
964744a9ae | ||
|
|
779304d035 | ||
|
|
f15d7445bd | ||
|
|
26d68ba3fc | ||
|
|
9effff25df | ||
|
|
958805a1fd | ||
|
|
dd74c173a5 | ||
|
|
bd020c93f5 | ||
|
|
75555f0426 | ||
|
|
4e6b14a467 | ||
|
|
233de7fc37 | ||
|
|
c6e0230e5d | ||
|
|
ca2732668e | ||
|
|
456b76ac96 | ||
|
|
a2c58bea82 | ||
|
|
7276b3d5ef | ||
|
|
1ef8315462 | ||
|
|
2f88ac419b | ||
|
|
cee7c85780 | ||
|
|
b2c5205035 | ||
|
|
2be59c3eb6 | ||
|
|
176fc896ae | ||
|
|
a943555d86 | ||
|
|
58bbfbbc42 | ||
|
|
026b644630 | ||
|
|
1483b355af | ||
|
|
6a5bb635bb | ||
|
|
18241136e3 | ||
|
|
6fb6467b07 | ||
|
|
83f0d16904 | ||
|
|
0e0aca3844 | ||
|
|
97b52792ef | ||
|
|
0680c5ff9c | ||
|
|
f1f0158238 | ||
|
|
bca3617116 | ||
|
|
55ae674e0b | ||
|
|
46eb610d26 | ||
|
|
bfc2e9c7fb | ||
|
|
ffedd9eaf9 | ||
|
|
1512718d41 | ||
|
|
e566dcd0d9 | ||
|
|
43b8f8f31b | ||
|
|
768f700719 | ||
|
|
ab1faedadf | ||
|
|
fa325bd519 | ||
|
|
568876fc4f | ||
|
|
d506604918 | ||
|
|
7c9f79f243 | ||
|
|
a77d0ce5c0 | ||
|
|
55685e6854 | ||
|
|
4731b90899 | ||
|
|
f36527995e | ||
|
|
fd5cf98072 | ||
|
|
844e32a11e | ||
|
|
82cb0143f2 | ||
|
|
51e70347e4 | ||
|
|
075bdfdc23 | ||
|
|
e84edb3e30 | ||
|
|
2bde4656ed | ||
|
|
0b2cfc45c1 | ||
|
|
c8be41ce6c | ||
|
|
ec2f710d06 | ||
|
|
71112173b9 | ||
|
|
1615c20e66 | ||
|
|
fbcea12786 | ||
|
|
047d13afd1 | ||
|
|
4c1165533c | ||
|
|
14bdcf71fe | ||
|
|
1b3f022b2d | ||
|
|
b6d93a7f09 | ||
|
|
4b2438c298 | ||
|
|
3b79c8e09f | ||
|
|
d22ac6a7a4 | ||
|
|
2f50f5ef34 | ||
|
|
d7bbf2b8e2 | ||
|
|
b8df34309f | ||
|
|
278fa3c397 | ||
|
|
6f48ee98e5 | ||
|
|
14db59318c | ||
|
|
f9b3deaff2 | ||
|
|
c265e42220 | ||
|
|
c0bdf1551d | ||
|
|
e373eeeed3 | ||
|
|
774b0c104b | ||
|
|
fbd4d9a74e | ||
|
|
6c8859863a | ||
|
|
31ecd91f7c | ||
|
|
474255c985 | ||
|
|
652f9ceb5c | ||
|
|
49668d4ba9 | ||
|
|
acddaf07db | ||
|
|
8da26ab409 | ||
|
|
3f2fefe4f7 | ||
|
|
fb8513bc9f | ||
|
|
e4ec958edb | ||
|
|
948289a038 | ||
|
|
78ad88a245 | ||
|
|
99660aeee8 | ||
|
|
ce293ed891 | ||
|
|
2738613344 | ||
|
|
5d12d934b8 | ||
|
|
19a89645a1 | ||
|
|
65a412eb59 | ||
|
|
9df9673e84 | ||
|
|
2002ed1c9c | ||
|
|
9c98c6d0c4 | ||
|
|
488a1f6070 | ||
|
|
4d6600b0a3 | ||
|
|
dac0d15e78 | ||
|
|
1e12d937aa | ||
|
|
e643a44b59 | ||
|
|
7319870289 | ||
|
|
72c1ebf66d | ||
|
|
008c4a3ab7 | ||
|
|
38d3d11571 | ||
|
|
5b45751c67 | ||
|
|
c00d937f4c | ||
|
|
d4565acfe7 | ||
|
|
16129c53bd | ||
|
|
cbf1961510 | ||
|
|
c76a4d4be7 | ||
|
|
c33fe45d4c | ||
|
|
e355c643f4 | ||
|
|
fcccb7eb38 | ||
|
|
f9209406fb | ||
|
|
facbf6f529 | ||
|
|
27af0400c0 | ||
|
|
76a80ff034 | ||
|
|
f7f333ad52 | ||
|
|
0ce45e32aa | ||
|
|
abeb11c8a6 | ||
|
|
e01af4adec | ||
|
|
3043a0bd15 | ||
|
|
94806ed45d | ||
|
|
baee94ed1d | ||
|
|
0c8358beaa | ||
|
|
232cab01f8 | ||
|
|
b301953249 | ||
|
|
bb9cb03c8a | ||
|
|
d8a4d39849 | ||
|
|
0d6966726e | ||
|
|
bb26725c67 | ||
|
|
103fe5718b | ||
|
|
a0d1bf1788 | ||
|
|
37f35281b4 | ||
|
|
306fb3cb33 | ||
|
|
24a6107171 | ||
|
|
e495fbfd99 | ||
|
|
da6fbc4995 | ||
|
|
8d673e1229 | ||
|
|
d8c7d02f5e | ||
|
|
8331c4b305 | ||
|
|
5a06f4788b | ||
|
|
827171e206 | ||
|
|
c3f3fa9876 | ||
|
|
825ef84da5 | ||
|
|
4445d46f61 | ||
|
|
c583057d45 | ||
|
|
982f3ee2a1 | ||
|
|
cd1e4e9f91 | ||
|
|
33f8e6d144 | ||
|
|
af70cc83ba | ||
|
|
8b731fd9e5 | ||
|
|
a0c697b982 | ||
|
|
21a6fe407b | ||
|
|
1c1a7b3e27 | ||
|
|
61fd5481cc | ||
|
|
d400b27545 | ||
|
|
ec9d455d18 | ||
|
|
8a75f303bb | ||
|
|
0ce706e02e | ||
|
|
3076593021 | ||
|
|
a83389bb7d | ||
|
|
ee1cde8a04 | ||
|
|
62e9c32082 | ||
|
|
a9efe88461 | ||
|
|
c8200f2972 | ||
|
|
80b43d3e0c | ||
|
|
a7e235441b | ||
|
|
bb80637e72 | ||
|
|
8fb5de4e87 | ||
|
|
1c03f0a07a | ||
|
|
b4ac96ccaf | ||
|
|
5723fbdea5 | ||
|
|
260bc15577 | ||
|
|
59bfe3e835 | ||
|
|
086fac4120 | ||
|
|
46bc36ad4a | ||
|
|
ed477e8cc6 | ||
|
|
8b878149d4 | ||
|
|
db512419ea | ||
|
|
491bc65a38 | ||
|
|
94ec286de6 | ||
|
|
108c83588c | ||
|
|
abbb4950a5 | ||
|
|
0fe0b34898 | ||
|
|
8ad7d4be0a | ||
|
|
ac7f3805d4 | ||
|
|
e524dd111e | ||
|
|
4a14709f13 | ||
|
|
85b8ee493e | ||
|
|
e546488aeb | ||
|
|
0702e0abd3 | ||
|
|
29318005b2 | ||
|
|
b4b61c94d7 | ||
|
|
f18620d890 | ||
|
|
eb86c3fb79 | ||
|
|
f89fc2ee1d | ||
|
|
495eaa0a37 | ||
|
|
b44b7b80ff | ||
|
|
c8ff5362a3 | ||
|
|
d575a0a29f | ||
|
|
2e1134fdfb | ||
|
|
ab6ae15836 | ||
|
|
a4840c7365 | ||
|
|
dbcc13a5cf | ||
|
|
8d2639b349 | ||
|
|
889b034a79 | ||
|
|
e49145023f | ||
|
|
ae1fb05607 | ||
|
|
80a957bc5b | ||
|
|
24f93d8258 | ||
|
|
01016e08d2 | ||
|
|
949fde1517 | ||
|
|
804cf9916b | ||
|
|
ef24680baf | ||
|
|
d4b1d240b9 | ||
|
|
448c795982 | ||
|
|
7f667e2d1c | ||
|
|
a1afdb0c63 | ||
|
|
ee1d7599d5 | ||
|
|
8adad18198 | ||
|
|
49a0758aae | ||
|
|
e3bdc73013 | ||
|
|
1922e1e895 | ||
|
|
7ee8e7c8d2 | ||
|
|
e5106c0704 | ||
|
|
80331aa217 | ||
|
|
84537e6884 | ||
|
|
f7062115bc | ||
|
|
24f9f20eb7 | ||
|
|
217c6f2ed0 | ||
|
|
82490ef5dd | ||
|
|
fcda017c39 | ||
|
|
0c36925783 | ||
|
|
fcb67e62c5 | ||
|
|
0651a2886e | ||
|
|
d876f4cb39 | ||
|
|
b5259d1a98 | ||
|
|
ffe053ffc5 | ||
|
|
1785af4749 | ||
|
|
98b4b7330e | ||
|
|
f68e5ae9ef | ||
|
|
b3e73ce86a | ||
|
|
02cbb5f812 | ||
|
|
1d9ae72c81 | ||
|
|
d6470ae814 | ||
|
|
a816d1a913 | ||
|
|
2c03316f86 | ||
|
|
d19214a390 | ||
|
|
03035d1a2d | ||
|
|
6886716e67 | ||
|
|
6165308c23 | ||
|
|
77a9469ff8 | ||
|
|
2dce9b4c48 | ||
|
|
74e05763f7 | ||
|
|
a4185fde07 | ||
|
|
ebb519e6d8 | ||
|
|
90fbba600f | ||
|
|
4bd4921131 | ||
|
|
44ee813c6f | ||
|
|
3e467c5021 | ||
|
|
2b0dd3ab99 | ||
|
|
2618346a32 | ||
|
|
eacbb14279 | ||
|
|
c8570d07ef | ||
|
|
7e345dd1f9 | ||
|
|
afe6ad6bfc | ||
|
|
af4c1e8514 | ||
|
|
d4fecd1dd8 | ||
|
|
79779eb721 | ||
|
|
6af666b58c | ||
|
|
42872136a0 | ||
|
|
aca7b2ac5f | ||
|
|
a646f19b3f | ||
|
|
9c5298aebf | ||
|
|
c6b07852fe | ||
|
|
40d3053df9 | ||
|
|
5e576a58e9 | ||
|
|
4d63b5dda1 | ||
|
|
c1efd3f68b | ||
|
|
2841cc67e8 | ||
|
|
becf34b0d9 | ||
|
|
1bba915aef | ||
|
|
f7239137d6 | ||
|
|
d656986413 | ||
|
|
d0c4e9d846 | ||
|
|
bacb607d90 | ||
|
|
3914e24b3d | ||
|
|
4582be1da5 | ||
|
|
c3c7f8b14f | ||
|
|
863d4f117b | ||
|
|
35038ed56b | ||
|
|
c4149f5d94 | ||
|
|
de2db71e54 | ||
|
|
2e3ed896cc | ||
|
|
f6f352e9e0 | ||
|
|
33dede8abc | ||
|
|
1e2e66ecf7 | ||
|
|
6e5140d859 | ||
|
|
84a7f90129 | ||
|
|
aff02c2def | ||
|
|
2dd5b29303 | ||
|
|
bc8f17393a | ||
|
|
72f78f1c8d | ||
|
|
201b4ca1e8 | ||
|
|
04811a0658 | ||
|
|
ee94d6aa89 | ||
|
|
3382cd6208 | ||
|
|
56f9ec1d10 | ||
|
|
1b42312d02 | ||
|
|
8a41a9f506 | ||
|
|
18bb2d7501 | ||
|
|
6f8e576c9b | ||
|
|
e5bb102ab1 | ||
|
|
13700fe2b2 | ||
|
|
3cfbe65855 | ||
|
|
ab0b11cc92 | ||
|
|
17c8071de9 | ||
|
|
1fe7f61ec8 | ||
|
|
ea3b8124fc | ||
|
|
c41cfadb19 | ||
|
|
e434fe0847 | ||
|
|
8a8aaa752e | ||
|
|
4420d1df2c | ||
|
|
d90d39933a | ||
|
|
7e38ab624a | ||
|
|
9a36187333 | ||
|
|
b0c4d70628 | ||
|
|
d5fa6424c1 | ||
|
|
07fe5b3cb1 | ||
|
|
672547cd07 | ||
|
|
28167403a4 | ||
|
|
eaf08d42dd | ||
|
|
bae95a5e07 | ||
|
|
db1ed9e257 | ||
|
|
adef35049f | ||
|
|
bd9abf2e52 | ||
|
|
aa74c00fd4 | ||
|
|
a68a907fa7 | ||
|
|
1a05d6d03a | ||
|
|
60f4183057 | ||
|
|
04c3afc850 | ||
|
|
066e512844 | ||
|
|
d60457afaf | ||
|
|
f5a788220e | ||
|
|
2fd5edc6e4 | ||
|
|
26716d0b89 | ||
|
|
3e0f57d0c7 | ||
|
|
5347717f3d | ||
|
|
6b0a569998 | ||
|
|
4476391287 | ||
|
|
16a9d8c244 | ||
|
|
bbda9a0ee8 | ||
|
|
776fdcc6ce | ||
|
|
f76e06ec57 | ||
|
|
3a9cff697b | ||
|
|
e6979c77e3 | ||
|
|
99866507f5 | ||
|
|
cf0dd07c21 | ||
|
|
b9bf8c6c96 | ||
|
|
725d0da15d | ||
|
|
37164070d2 | ||
|
|
d7bd34531f | ||
|
|
71ca1753ef | ||
|
|
c6bfbf4ac1 | ||
|
|
a27fde72ee | ||
|
|
785f6a36bf | ||
|
|
1a4c12d851 | ||
|
|
b484e32b08 | ||
|
|
303438834b | ||
|
|
70b7b7615a | ||
|
|
5c42e60faf | ||
|
|
48ef60e0eb | ||
|
|
01af97ddab | ||
|
|
038794fa1c | ||
|
|
ffb1bccb10 | ||
|
|
371eb3bee6 | ||
|
|
466d935d02 | ||
|
|
7ffca9c3f1 | ||
|
|
a5981b3106 | ||
|
|
1df080983a | ||
|
|
822b72eee4 | ||
|
|
014abdcc00 | ||
|
|
b35cc437d3 | ||
|
|
7ce87c7168 | ||
|
|
2a3f8eedf6 | ||
|
|
895c3f2f97 | ||
|
|
8860f4591b | ||
|
|
984f26807e | ||
|
|
88c58e6108 | ||
|
|
83c7c4a14a | ||
|
|
b4a4df480e | ||
|
|
9843b4d218 | ||
|
|
c1251af597 | ||
|
|
7d09b8bf1b | ||
|
|
f57bdeec12 | ||
|
|
9899f6b761 | ||
|
|
328e10b89f | ||
|
|
b37f8a8375 | ||
|
|
139937f887 | ||
|
|
b8b90451b6 | ||
|
|
c9a2aff6a6 | ||
|
|
b25b117717 | ||
|
|
e174fd731c | ||
|
|
34e6d463be | ||
|
|
808169f43c | ||
|
|
82b6531a07 | ||
|
|
917e2cabfc | ||
|
|
a981025679 | ||
|
|
f9ea2d73e1 | ||
|
|
a5728e9407 | ||
|
|
404a2acc8f | ||
|
|
7bf61ee8ed | ||
|
|
bf1cc47525 | ||
|
|
74ad27d5fb | ||
|
|
60b73aff24 | ||
|
|
4de465e544 | ||
|
|
e1161b9227 | ||
|
|
c2cdd0fa08 | ||
|
|
b365a03e71 | ||
|
|
2e629efc7f | ||
|
|
fcaa2bef6e | ||
|
|
3b2a9fedef | ||
|
|
64256f2638 | ||
|
|
5c8122d93b | ||
|
|
6ffb95779d | ||
|
|
af975d0e7f | ||
|
|
34b2b88cf3 | ||
|
|
3fea166274 | ||
|
|
233b905492 | ||
|
|
7afad765cb | ||
|
|
39663cb594 | ||
|
|
457e0539fa | ||
|
|
871ba619a3 | ||
|
|
f5427310e8 | ||
|
|
e4435252f4 | ||
|
|
a66aa74c7f | ||
|
|
3949e15f77 | ||
|
|
acaf50bf8a | ||
|
|
09eedc08c1 | ||
|
|
738bcf8bcb | ||
|
|
795caa5d9d | ||
|
|
c041197f3f | ||
|
|
0730789cb3 | ||
|
|
1eb0fbd2c9 | ||
|
|
0484122b7f | ||
|
|
7bc9c704d1 | ||
|
|
c9efdc2c16 | ||
|
|
e0dee998d4 | ||
|
|
e09ea60616 | ||
|
|
7cab07fa01 | ||
|
|
5a62a8afe9 | ||
|
|
23d6d41148 | ||
|
|
a07fbfd7b7 | ||
|
|
1411268e35 | ||
|
|
9bda88edbd | ||
|
|
16da25898f | ||
|
|
88ae2b8e83 | ||
|
|
7d1ede8c91 | ||
|
|
3805f8b156 | ||
|
|
39f40670d0 | ||
|
|
514cf2b81c | ||
|
|
4dbb5975c0 | ||
|
|
b327e59ab1 | ||
|
|
917d51e54b | ||
|
|
122d05346b | ||
|
|
645802981e | ||
|
|
0348a2475c | ||
|
|
a2b7f59dc9 | ||
|
|
c23dc93c7a | ||
|
|
d3e3667405 | ||
|
|
d4ed5afe93 | ||
|
|
790ecef1c1 | ||
|
|
edb59ffdc6 | ||
|
|
e137038586 | ||
|
|
761eb343f9 | ||
|
|
6c15c907bf | ||
|
|
06b21e3d27 | ||
|
|
263d2e6f30 | ||
|
|
c97fec66f4 | ||
|
|
d5cf881d7a | ||
|
|
050d3e6cd2 | ||
|
|
757f68c872 | ||
|
|
b83bf9bc09 | ||
|
|
8d96e883d6 | ||
|
|
75079a981b | ||
|
|
e8043be107 | ||
|
|
c276f66017 | ||
|
|
db7df33b27 | ||
|
|
1b8e355944 | ||
|
|
342ef90cbe | ||
|
|
b3be60acf6 | ||
|
|
b58d3e2849 | ||
|
|
4206fb943d | ||
|
|
ee58ff40f1 | ||
|
|
21fd842bde | ||
|
|
b63b5eb262 | ||
|
|
9c69509a19 | ||
|
|
511147fb90 | ||
|
|
9ce0030482 | ||
|
|
4164263639 | ||
|
|
1bebb88d0e | ||
|
|
d2f5dbbc26 | ||
|
|
2af6500028 | ||
|
|
a7cf8be8c4 | ||
|
|
69b3e544bd | ||
|
|
0c63c61db5 | ||
|
|
8dc34ddfa7 | ||
|
|
8efda0047c | ||
|
|
e3c17e0347 | ||
|
|
038c43bc09 | ||
|
|
720549ec7e | ||
|
|
3aa1f273b6 | ||
|
|
5105ebb870 | ||
|
|
0a55c12f4f | ||
|
|
67c20c3a5f | ||
|
|
f4f7892123 | ||
|
|
00a93e466d | ||
|
|
d27993d2c9 | ||
|
|
bfc65d51e3 | ||
|
|
3c29d38599 | ||
|
|
f8816ff227 | ||
|
|
a5be460c2e | ||
|
|
e1e7853c34 | ||
|
|
3f862f67ab | ||
|
|
4bb5b2641b | ||
|
|
f044fdf5dc | ||
|
|
d1be4637ad | ||
|
|
d18f645c18 | ||
|
|
6663360caa | ||
|
|
c2d24ee4d9 | ||
|
|
37471fcdc1 | ||
|
|
0f42567d98 | ||
|
|
8148f09b9c | ||
|
|
c270afb5e5 | ||
|
|
6de0f73ec1 | ||
|
|
a80a34afb1 | ||
|
|
584f813fc4 | ||
|
|
a44056dc43 | ||
|
|
c941c78ebf | ||
|
|
9e9f001fdf | ||
|
|
9060887694 | ||
|
|
6d9ea27e9f | ||
|
|
4f4973157c | ||
|
|
859a6d2f27 | ||
|
|
e3ad646bae | ||
|
|
53a661861a | ||
|
|
4a27ec3271 | ||
|
|
926de71274 | ||
|
|
b0e57bae9f | ||
|
|
0bbb4573b2 | ||
|
|
eeda28a287 | ||
|
|
59c55b96be | ||
|
|
060609e2cb | ||
|
|
a1c48bf012 | ||
|
|
a67fb66d2e | ||
|
|
3001f4845a | ||
|
|
192c1ad669 | ||
|
|
89ae8a3607 | ||
|
|
21909e0bda | ||
|
|
5e9635a76f | ||
|
|
90d718c186 | ||
|
|
6627a635d5 | ||
|
|
20740f33af | ||
|
|
0681a04607 | ||
|
|
3cfbe0c89b | ||
|
|
21d491843f | ||
|
|
566ac99a53 | ||
|
|
fca504901f | ||
|
|
e03231cbd7 | ||
|
|
693051f4e9 | ||
|
|
06e891f9a9 | ||
|
|
a2bb1b88ea | ||
|
|
9602269f27 | ||
|
|
27b9af6067 | ||
|
|
d43c0749c1 | ||
|
|
f4fca7600a | ||
|
|
6134f1c812 | ||
|
|
8392b7a606 | ||
|
|
94cf82c3d9 | ||
|
|
b0018c3f06 | ||
|
|
8977114806 | ||
|
|
36f8eac3e4 | ||
|
|
0f01a8eda5 | ||
|
|
de965dace8 | ||
|
|
f0ca2edb25 | ||
|
|
514d5f92d1 | ||
|
|
7e05f8a5eb | ||
|
|
48d4d4e966 | ||
|
|
6a7b50b35f | ||
|
|
6cad5a8efb | ||
|
|
7743664527 | ||
|
|
714e1b6d29 | ||
|
|
a0585e097f | ||
|
|
6363b76d33 | ||
|
|
503562f2e5 | ||
|
|
01a7331354 | ||
|
|
c07a1ab636 | ||
|
|
eb9f92f04a | ||
|
|
c05607934b | ||
|
|
f9ca93e0b0 | ||
|
|
09b7d71103 | ||
|
|
bc06578e53 | ||
|
|
344f518991 | ||
|
|
6bb44bef20 | ||
|
|
aef0c84286 | ||
|
|
0fbc8dcb41 | ||
|
|
818138340c | ||
|
|
4beab6bcec | ||
|
|
b9b7ae1cac | ||
|
|
91c75518da | ||
|
|
82f51293df | ||
|
|
840ba0f240 | ||
|
|
c5e3eed64b | ||
|
|
7dd1cca65d | ||
|
|
57414bdb67 | ||
|
|
646be63895 | ||
|
|
3c1fb97c12 | ||
|
|
e78619f5e1 | ||
|
|
aa04904a72 | ||
|
|
54102ae35e | ||
|
|
7c4c1798b6 | ||
|
|
f30006093d | ||
|
|
c8ef47064a | ||
|
|
461c984317 | ||
|
|
ecbad7ce6c | ||
|
|
7adf624e95 | ||
|
|
56cb97706b | ||
|
|
dd5bec799f | ||
|
|
2389415071 | ||
|
|
976fb02bd3 | ||
|
|
007fe18d27 | ||
|
|
11f2999b52 | ||
|
|
e204029d3c | ||
|
|
d3eaae2fb0 | ||
|
|
251c22a14f | ||
|
|
7d0a7186e8 | ||
|
|
7b444b88e3 | ||
|
|
f317b1b189 | ||
|
|
3523562c3a | ||
|
|
5fdc3e3f10 | ||
|
|
24ef8f6e2a | ||
|
|
d8d6625478 | ||
|
|
ad06146784 | ||
|
|
c97171883c | ||
|
|
9c5b6045af | ||
|
|
eb85a7656f | ||
|
|
25d45b309d | ||
|
|
7d0b4535bb | ||
|
|
f8af2025e8 | ||
|
|
948c117f5b | ||
|
|
58967ab758 | ||
|
|
44a153d30a | ||
|
|
d5d6103327 |
@@ -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
|
||||
@@ -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.
|
||||
@@ -0,0 +1,2 @@
|
||||
github: [glanceapp]
|
||||
patreon: glanceapp
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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 -->
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -1,4 +1,7 @@
|
||||
/assets
|
||||
/build
|
||||
/playground
|
||||
glance.yml
|
||||
/.idea
|
||||
/glance*.yml
|
||||
*.yml.bak
|
||||
*.yml.bak.*
|
||||
|
||||
@@ -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
|
||||
@@ -1,10 +1,15 @@
|
||||
FROM alpine:3.19
|
||||
|
||||
ARG TARGETOS
|
||||
ARG TARGETARCH
|
||||
FROM golang:1.24.3-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
|
||||
|
||||
LABEL org.opencontainers.image.source=https://github.com/uhlwoogi/modern-glance
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/glance .
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
ENTRYPOINT ["/app/glance"]
|
||||
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
FROM alpine:3.21
|
||||
|
||||
WORKDIR /app
|
||||
COPY glance .
|
||||
|
||||
EXPOSE 8080/tcp
|
||||
ENTRYPOINT ["/app/glance", "--config", "/app/config/glance.yml"]
|
||||
@@ -1,90 +1,123 @@
|
||||
<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"><img src="docs/logo.png"></p>
|
||||
<h1 align="center">modern-glance</h1>
|
||||
<p align="center">
|
||||
<a href="#installation">Install</a> •
|
||||
<a href="#web-based-editor">Editor</a> •
|
||||
<a href="docs/configuration.md#configuring-glance">Configuration</a> •
|
||||
<a href="docs/themes.md">Themes</a>
|
||||
</p>
|
||||
|
||||

|
||||
<p align="center">A fork of <a href="https://github.com/glanceapp/glance">glanceapp/glance</a> that adds a web-based dashboard editor so you can manage your config without touching YAML.</p>
|
||||
|
||||
### 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
|
||||
> **Upstream credit:** All core functionality — widgets, theming, layout engine, hot-reload — is the work of the [Glance](https://github.com/glanceapp/glance) project and its contributors. This fork adds an editing UI on top and nothing else. If you find Glance useful, consider [sponsoring the upstream project](https://github.com/sponsors/glanceapp).
|
||||
|
||||
#### Themeable
|
||||

|
||||
> **⚠️ Vibe coded:** The editing UI in this fork was built entirely with AI assistance and has not been rigorously tested. It may have bugs, edge cases, or security issues — particularly around config file handling. Use it on a trusted network, keep backups, and don't be surprised if something breaks. PRs welcome.
|
||||
|
||||
#### Optimized for mobile devices
|
||||

|
||||
---
|
||||
|
||||
#### 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)
|
||||
## What's added
|
||||
|
||||
### 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.
|
||||
### In-place edit mode
|
||||
A pencil button in the dashboard header toggles edit mode. While active you can:
|
||||
- **Drag and drop** widgets between columns and reorder them
|
||||
- **Add widgets** via a form dialog (for most widget types) or a YAML editor fallback
|
||||
- **Edit widgets** inline with per-field forms — includes live lookup for weather locations, market symbols, and RSS feed validation
|
||||
- **Delete widgets**
|
||||
- **Add, delete, resize, and reorder columns**
|
||||
- **Add and manage head widgets** (the row above the columns)
|
||||
|
||||
### Installation
|
||||
> [!CAUTION]
|
||||
>
|
||||
> The project is under active development, expect things to break every once in a while.
|
||||
### Page management (`/edit`)
|
||||
- Add, delete, and reorder pages
|
||||
- Per-page settings: name, slug, width, navigation options, mobile header
|
||||
|
||||
#### 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:
|
||||
### Site settings (`/edit/site-settings`)
|
||||
- App name, logo, favicon, footer — all editable without touching YAML
|
||||
|
||||
```
|
||||
/opt/glance/glance --config /etc/glance.yml
|
||||
```
|
||||
### Theme settings (`/edit/theme-settings`)
|
||||
- Color pickers for background, primary, positive, and negative colors (hex input, stored as HSL)
|
||||
- Contrast and text saturation multipliers
|
||||
- Light mode toggle, hide-picker toggle, custom CSS file path
|
||||
- **Preset management**: add, edit, and delete named theme presets with live preview swatches
|
||||
- **Built-in theme catalog**: 14 themes from the upstream docs (Dracula, Catppuccin variants, Gruvbox, and more) importable with one click
|
||||
|
||||
#### Docker
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> Make sure you have a valid `glance.yml` file before running the container.
|
||||
### Multi-step undo
|
||||
`/edit` keeps a 10-step numbered backup chain (`glance.yml.bak.1` … `.bak.10`). Each save rotates the chain; any backup can be restored individually.
|
||||
|
||||
```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
|
||||
```
|
||||
### Auth gate
|
||||
`/edit` returns 403 unless `auth.users` is configured **or** `admin.allow-without-auth: true` is set. See `docs/glance.yml` for examples of both.
|
||||
|
||||
Or if you prefer docker compose:
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
### Docker (recommended)
|
||||
|
||||
```yaml
|
||||
# docker-compose.yml
|
||||
services:
|
||||
glance:
|
||||
image: glanceapp/glance
|
||||
volumes:
|
||||
- ./glance.yml:/app/glance.yml
|
||||
- /etc/timezone:/etc/timezone:ro
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
image: ghcr.io/uhlwoogi/modern-glance:latest
|
||||
ports:
|
||||
- 8080:8080
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# Config dir — glance.yml lives here and is written by /edit
|
||||
- ./config:/app/config
|
||||
# Optional: custom CSS / images (set server.assets-path: /app/assets in glance.yml)
|
||||
- ./assets:/app/assets
|
||||
restart: unless-stopped
|
||||
# environment:
|
||||
# MY_SECRET_TOKEN: abc123 # reference as ${env:MY_SECRET_TOKEN} in glance.yml
|
||||
```
|
||||
|
||||
### Building from source
|
||||
1. Create the config directory and grab the starter config:
|
||||
```bash
|
||||
mkdir -p config assets
|
||||
curl -o config/glance.yml https://raw.githubusercontent.com/uhlwoogi/modern-glance/main/docs/glance.yml
|
||||
```
|
||||
|
||||
Requirements: [Go](https://go.dev/dl/) >= v1.22
|
||||
2. Start:
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
To build:
|
||||
3. Open `http://localhost:8080` — click the **pencil icon** in the header to start editing.
|
||||
|
||||
```
|
||||
go build .
|
||||
> The starter `glance.yml` has `admin.allow-without-auth: true` so `/edit` is open by default. Switch to the auth block in the file before exposing to a network you don't fully trust.
|
||||
|
||||
### Updating
|
||||
|
||||
```bash
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
To run:
|
||||
---
|
||||
|
||||
```
|
||||
## Original Glance features
|
||||
|
||||
Everything from upstream is intact:
|
||||
|
||||
- **Widgets**: RSS, Reddit, Hacker News, weather, YouTube, Twitch, markets, Docker containers, server stats, bookmarks, calendar, monitors, and [many more](docs/configuration.md#configuring-glance)
|
||||
- **Fast and lightweight**: low memory, minimal JS, single ~20 MB binary, pages load in ~1s
|
||||
- **Highly customizable**: multiple layouts, pages, themes, custom CSS
|
||||
- **Mobile optimized**
|
||||
- **Themeable**: tweak a few numbers or pick from the [theme gallery](docs/themes.md)
|
||||
|
||||
Full configuration reference: [docs/configuration.md](docs/configuration.md#configuring-glance)
|
||||
|
||||
---
|
||||
|
||||
## Building from source
|
||||
|
||||
```bash
|
||||
# Run locally
|
||||
go run .
|
||||
|
||||
# Build binary
|
||||
go build -o glance .
|
||||
|
||||
# Build and push Docker image
|
||||
docker build -t ghcr.io/uhlwoogi/modern-glance:latest .
|
||||
docker push ghcr.io/uhlwoogi/modern-glance:latest
|
||||
```
|
||||
|
||||
Requires Go ≥ 1.23.
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
services:
|
||||
glance:
|
||||
image: ghcr.io/uhlwoogi/modern-glance:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# Config directory: glance.yml lives here and is written by the /edit UI.
|
||||
# Copy docs/glance.yml → ./config/glance.yml to get started.
|
||||
- ./config:/app/config
|
||||
# Assets directory: custom CSS, images, icons referenced by server.assets-path.
|
||||
# Set `server: { assets-path: /app/assets }` in glance.yml to enable.
|
||||
- ./assets:/app/assets
|
||||
restart: unless-stopped
|
||||
# Uncomment to inject secrets as environment variables.
|
||||
# Reference them in glance.yml as ${env:MY_SECRET_TOKEN}.
|
||||
# environment:
|
||||
# MY_SECRET_TOKEN: abc123
|
||||
@@ -0,0 +1,475 @@
|
||||
[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`, `div` and `mod`.
|
||||
|
||||
<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>
|
||||
```
|
||||
|
||||
<hr>
|
||||
|
||||
In some instances, you may need to make two consecutive API calls, where you use the result of the first call in the second call. To achieve this, you can make additional HTTP requests from within the template itself using the following syntax:
|
||||
|
||||
```yaml
|
||||
- type: custom-api
|
||||
url: https://api.example.com/get-id-of-something
|
||||
template: |
|
||||
{{ $theID := .JSON.String "id" }}
|
||||
|
||||
{{
|
||||
$something := newRequest (concat "https://api.example.com/something/" $theID)
|
||||
| withParameter "key" "value"
|
||||
| withHeader "Authorization" "Bearer token"
|
||||
| getResponse
|
||||
}}
|
||||
|
||||
{{ $something.JSON.String "title" }}
|
||||
```
|
||||
|
||||
Here, `$theID` gets retrieved from the result of the first API call and used in the second API call. The `newRequest` function creates a new request, and the `getResponse` function executes it. You can also use `withParameter` and `withHeader` to optionally add parameters and headers to the request.
|
||||
|
||||
If you need to make a request to a URL that requires dynamic parameters, you can omit the `url` property in the YAML and run the request entirely from within the template itself:
|
||||
|
||||
```yaml
|
||||
- type: custom-api
|
||||
title: Events from the last 24h
|
||||
template: |
|
||||
{{
|
||||
$events := newRequest "https://api.example.com/events"
|
||||
| withParameter "after" (offsetNow "-24h" | formatTime "rfc3339")
|
||||
| getResponse
|
||||
}}
|
||||
|
||||
{{ if eq $events.Response.StatusCode 200 }}
|
||||
{{ range $events.JSON.Array "events" }}
|
||||
<div>{{ .String "title" }}</div>
|
||||
<div {{ .String "date" | parseTime "rfc3339" | toRelativeTime }}></div>
|
||||
{{ end }}
|
||||
{{ else }}
|
||||
<p>Failed to fetch data: {{ $events.Response.Status }}</p>
|
||||
{{ end }}
|
||||
```
|
||||
|
||||
*Note that you need to manually check for the correct status code.*
|
||||
|
||||
## 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 functions are available on the `Options` object:
|
||||
|
||||
- `StringOr(key string, default string) string`: Returns the value of the key as a string, or the default value if the key does not exist.
|
||||
- `IntOr(key string, default int) int`: Returns the value of the key as an integer, or the default value if the key does not exist.
|
||||
- `FloatOr(key string, default float) float`: Returns the value of the key as a float, or the default value if the key does not exist.
|
||||
- `BoolOr(key string, default bool) bool`: Returns the value of the key as a boolean, or the default value if the key does not exist.
|
||||
- `JSON(key string) JSON`: Returns the value of the key as a stringified `JSON` object, or throws an error if the key does not exist.
|
||||
|
||||
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".
|
||||
- `formatTime(layout string, s string) time.Time`: Formats a `time.Time` into a string. The layout uses the same format as `parseTime`.
|
||||
- `parseLocalTime(layout string, s string) time.Time`: Same as the above, except in the absence of a timezone, it will use the local timezone instead of UTC.
|
||||
- `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.
|
||||
- `mod(a, b int) int`: Remainder after dividing a by b (a % b).
|
||||
- `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.
|
||||
- `percentChange(current float, previous float) float`: Calculates the percentage change between two numbers.
|
||||
- `startOfDay(t time.Time) time.Time`: Returns the start of the day for a given time.
|
||||
- `endOfDay(t time.Time) time.Time`: Returns the end of the day for a given time.
|
||||
|
||||
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.
|
||||
- `le(a, b any) bool`: Compares two values for less than or equal to.
|
||||
- `gt(a, b any) bool`: Compares two values for greater than.
|
||||
- `ge(a, b any) bool`: Compares two values for greater than or equal to.
|
||||
- `and(args ...bool) bool`: Returns true if **all** arguments are true; accepts two or more boolean values.
|
||||
- `or(args ...bool) bool`: Returns true if **any** argument is true; accepts two or more boolean values.
|
||||
- `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.
|
||||
@@ -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:
|
||||
|
||||

|
||||
|
||||
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:
|
||||
|
||||

|
||||
|
||||
**Class names or features may change, once again, you are responsible for maintaining your own extensions.**
|
||||
@@ -0,0 +1,79 @@
|
||||
# --- Edit access ---
|
||||
# /edit is locked down by default. Pick ONE of the two blocks below.
|
||||
#
|
||||
# Option A (recommended): require login.
|
||||
# Generate values with:
|
||||
# go run . secret:make
|
||||
# go run . password:hash <your-password>
|
||||
#
|
||||
# auth:
|
||||
# secret-key: <paste base64 secret here>
|
||||
# users:
|
||||
# yourname:
|
||||
# password-hash: <paste bcrypt hash here>
|
||||
#
|
||||
# Option B: open /edit without authentication. ONLY on a trusted network —
|
||||
# anyone who can reach this server can rewrite your config.
|
||||
# (The config key is still named `admin` for backward compatibility.)
|
||||
#
|
||||
admin:
|
||||
allow-without-auth: true
|
||||
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: group
|
||||
widgets:
|
||||
- type: hacker-news
|
||||
- size: full
|
||||
widgets:
|
||||
- type: rss
|
||||
limit: 10
|
||||
collapse-after: 3
|
||||
cache: 12h
|
||||
feeds:
|
||||
- url: https://www.techmeme.com/feed.xml
|
||||
title: TechMeme
|
||||
- size: small
|
||||
widgets:
|
||||
- type: markets
|
||||
markets:
|
||||
- symbol: AMZN
|
||||
name: Amazon.com, Inc.
|
||||
- type: weather
|
||||
location: Houston, US
|
||||
units: imperial
|
||||
hour-format: 12h
|
||||
# Optionally hide the location from being displayed in the widget
|
||||
# hide-location: true
|
||||
|
||||
hide-location: true
|
||||
show-area-name: false
|
||||
- type: group
|
||||
widgets:
|
||||
- type: reddit
|
||||
subreddit: technology
|
||||
show-thumbnails: true
|
||||
- type: reddit
|
||||
subreddit: selfhosted
|
||||
show-thumbnails: true
|
||||
|
||||
# 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
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 9.7 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 133 KiB |
|
After Width: | Height: | Size: 87 KiB |
|
After Width: | Height: | Size: 946 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 59 KiB |
|
After Width: | Height: | Size: 39 KiB |
|
After Width: | Height: | Size: 637 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 38 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 261 KiB After Width: | Height: | Size: 310 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 343 KiB After Width: | Height: | Size: 362 KiB |
|
Before Width: | Height: | Size: 370 KiB After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 42 KiB |
|
After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 792 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 229 KiB |
|
After Width: | Height: | Size: 531 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 181 KiB |
|
After Width: | Height: | Size: 325 KiB |
|
After Width: | Height: | Size: 339 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 365 KiB |
|
After Width: | Height: | Size: 366 KiB |
|
After Width: | Height: | Size: 368 KiB |
|
Before Width: | Height: | Size: 341 KiB After Width: | Height: | Size: 341 KiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 549 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 367 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 330 KiB |
|
After Width: | Height: | Size: 77 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
@@ -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
|
||||
|
||||

|
||||
|
||||
<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
|
||||
|
||||

|
||||
|
||||
<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
|
||||
|
||||

|
||||
|
||||
<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>
|
||||
@@ -11,8 +11,30 @@ theme:
|
||||
contrast-multiplier: 1.1
|
||||
```
|
||||
|
||||
### Catppuccin
|
||||

|
||||
### Catppuccin Frappe
|
||||

|
||||
```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
|
||||

|
||||
```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
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 240 21 15
|
||||
@@ -31,6 +53,26 @@ theme:
|
||||
primary-color: 97 13 80
|
||||
```
|
||||
|
||||
### Gruvbox Dark
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 0 0 16
|
||||
primary-color: 43 59 81
|
||||
positive-color: 61 66 44
|
||||
negative-color: 6 96 59
|
||||
```
|
||||
|
||||
### Kanagawa Dark
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 240 13 14
|
||||
primary-color: 51 33 68
|
||||
negative-color: 358 100 68
|
||||
contrast-multiplier: 1.2
|
||||
```
|
||||
|
||||
### Tucan
|
||||

|
||||
```yaml
|
||||
@@ -40,8 +82,53 @@ theme:
|
||||
negative-color: 209 88 54
|
||||
```
|
||||
|
||||
### Dracula
|
||||

|
||||
```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
|
||||
```
|
||||
|
||||
### Shades of Purple
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 243 33 25
|
||||
contrast-multiplier: 1.2
|
||||
primary-color: 50 100 49
|
||||
positive-color: 98 82 71
|
||||
negative-color: 12 77 52
|
||||
```
|
||||
|
||||
### Neon Pink
|
||||

|
||||
```yaml
|
||||
theme:
|
||||
background-color: 240 27 11
|
||||
contrast-multiplier: 1.5
|
||||
primary-color: 321 100 71
|
||||
positive-color: 165 78 51
|
||||
negative-color: 360 100 71
|
||||
```
|
||||
|
||||
## Light
|
||||
|
||||
### Catppuccin Latte
|
||||

|
||||
```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
|
||||

|
||||
```yaml
|
||||
|
||||
@@ -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.
|
||||
@@ -1,19 +1,33 @@
|
||||
module github.com/glanceapp/glance
|
||||
|
||||
go 1.22.0
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/mmcdole/gofeed v1.3.0
|
||||
golang.org/x/text v0.14.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.4
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/text v0.25.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.9.1 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.10.3 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.3 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // 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.24.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.40.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI=
|
||||
github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/PuerkitoBio/goquery v1.10.3 h1:pFYcNSqHxBD06Fpj/KsbStFRsgRATgnf3LeXiUkhzPo=
|
||||
github.com/PuerkitoBio/goquery v1.10.3/go.mod h1:tMUX0zDMHXYlAQk6p35XxQMqMweEKB7iK7iLNd4RH4Y=
|
||||
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.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
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-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 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
|
||||
@@ -19,47 +31,101 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
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.4 h1:cdtFO363VEOOFrUCjZRh4XVJkb548lyF0q0uTeMqYPw=
|
||||
github.com/shirou/gopsutil/v4 v4.25.4/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=
|
||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
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.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
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/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
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.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
|
||||
golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
|
||||
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-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.7.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.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.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/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.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
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.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
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=
|
||||
|
||||
@@ -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")
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -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();
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -1,9 +0,0 @@
|
||||
<div class="page-columns">
|
||||
{{ range .Page.Columns }}
|
||||
<div class="page-column page-column-{{ .Size }}">
|
||||
{{ range .Widgets }}
|
||||
{{ .Render }}
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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>
|
||||
@@ -1,69 +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" . }}
|
||||
{{ if ne "" .App.Config.Theme.CustomCSSFile }}
|
||||
<link rel="stylesheet" href="{{ .App.Config.Theme.CustomCSSFile }}?v={{ .App.Config.Server.StartedAt.Unix }}">
|
||||
{{ end }}
|
||||
{{ 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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}°{{ if eq .Units "metric" }}C{{ else }}F{{ end }}</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 }}
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
}
|
||||