mirror of
https://github.com/daledavies/jump.git
synced 2026-08-05 17:11:16 +02:00
Added tagging, such refactor
This commit is contained in:
@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- New weather description and temperature display in bottom right of page.
|
||||
- Option to show/hide clock (SHOWCLOCK).
|
||||
- Option to switch between metric and imperial temperature (METRICTEMP).
|
||||
- Global defaults in dites.json for nofollow and icon.
|
||||
- Global defaults in sites.json for nofollow and icon.
|
||||
- Jump now has a favicon!
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -30,6 +30,7 @@ RUN apk add --no-cache \
|
||||
curl \
|
||||
nginx \
|
||||
php8 \
|
||||
php8-curl \
|
||||
php8-dom \
|
||||
php8-fileinfo \
|
||||
php8-fpm \
|
||||
|
||||
@@ -40,40 +40,40 @@ http {
|
||||
root /var/www/html;
|
||||
index index.php index.html;
|
||||
|
||||
# Hide nginx server tokens and version number
|
||||
server_tokens off;
|
||||
|
||||
location / {
|
||||
# Exclude unused HTTP methods
|
||||
limit_except GET HEAD POST { deny all; }
|
||||
# First attempt to serve request as file, then
|
||||
# as directory, then fall back to index.php
|
||||
try_files $uri $uri/ /index.php?q=$uri&$args;
|
||||
try_files $uri $uri/ index.php$is_args$args;
|
||||
}
|
||||
|
||||
# Redirect server error pages to the static page /50x.html
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /var/lib/nginx/html;
|
||||
}
|
||||
|
||||
# Pass the PHP scripts to PHP-FPM listening on php-fpm.sock
|
||||
location ~ \.php$ {
|
||||
try_files $uri =404;
|
||||
fastcgi_split_path_info ^(.+\.php)(/.+)$;
|
||||
fastcgi_pass unix:/run/php-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_pass unix:/run/php-fpm.sock;
|
||||
fastcgi_index $document_root/index.php;
|
||||
|
||||
fastcgi_split_path_info ^((?U).+\.php)(/?.+)$;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/$fastcgi_script_name;
|
||||
fastcgi_param PATH_TRANSLATED $document_root/$fastcgi_path_info;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml)$ {
|
||||
expires 5d;
|
||||
# Tell browsers to cache static assets
|
||||
location ~* \.(jpg|jpeg|gif|png|css|js|ico|xml|svg)$ {
|
||||
expires 3d;
|
||||
}
|
||||
|
||||
# Deny access to . files, for security
|
||||
# Deny access to dot files
|
||||
location ~ /\. {
|
||||
log_not_found off;
|
||||
deny all;
|
||||
}
|
||||
|
||||
# Allow fpm ping and status from localhost
|
||||
# Allow fpm ping from localhost, useful for docker HEALTHCHECK.
|
||||
location ~ ^/(fpm-ping)$ {
|
||||
access_log off;
|
||||
allow 127.0.0.1;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
[Date]
|
||||
date.timezone="UTC"
|
||||
date.timezone="UTC"
|
||||
expose_php = Off
|
||||
24
jumpapp/api/icon.php
Normal file
24
jumpapp/api/icon.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* Return icon image data for a given site from sites.json
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Provided by composer for psr-4 style autoloading.
|
||||
require __DIR__ .'/../vendor/autoload.php';
|
||||
|
||||
$config = new Jump\Config();
|
||||
$cache = new Jump\Cache($config);
|
||||
$sites = new Jump\Sites($config, $cache);
|
||||
|
||||
$siteurl = isset($_GET['siteurl']) ? urldecode($_GET['siteurl']) : (throw new Exception('siteurl param not provided'));
|
||||
|
||||
$site = $sites->get_site_by_url($siteurl);
|
||||
|
||||
$imagedata = $site->get_favicon_image_data();
|
||||
|
||||
// We made it here so output the API response as json.
|
||||
header('Content-Type: '.$imagedata->mimetype);
|
||||
echo $imagedata->data;
|
||||
63
jumpapp/api/weatherdata.php
Normal file
63
jumpapp/api/weatherdata.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
/**
|
||||
* Proxy requests to OpenWeather API and cache response.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
// Provided by composer for psr-4 style autoloading.
|
||||
require __DIR__ .'/../vendor/autoload.php';
|
||||
|
||||
$config = new Jump\Config();
|
||||
$cache = new Jump\Cache($config);
|
||||
|
||||
$owmapiurlbase = 'https://api.openweathermap.org/data/2.5/weather';
|
||||
$units = $config->parse_bool($config->get('metrictemp')) ? 'metric' : 'imperial';
|
||||
|
||||
// If we have either lat or lon query params then cast them to a float, if not then
|
||||
// set the values to zero.
|
||||
$lat = isset($_GET['lat']) ? (float) $_GET['lat'] : 0;
|
||||
$lon = isset($_GET['lon']) ? (float) $_GET['lon'] : 0;
|
||||
|
||||
// Use the lat and lon values provided unless they are zero, this might mean that
|
||||
// either they werent provided as query params or they couldn't be cast to a float.
|
||||
// If they are zero then use the default latlong from config.
|
||||
$latlong = [$lat, $lon];
|
||||
if ($lat === 0 || $lon === 0) {
|
||||
$latlong = explode(',', $config->get('latlong', false));
|
||||
}
|
||||
|
||||
// This is the API endpoint and params we are using for the query,
|
||||
$url = $owmapiurlbase
|
||||
.'?units=' . $units
|
||||
.'&lat=' . $latlong[0]
|
||||
.'&lon=' . $latlong[1]
|
||||
.'&appid=' . $config->get('owmapikey', false);
|
||||
|
||||
// Use the cache to store/retrieve data, make an md5 hash of latlong so it is not possible
|
||||
// to track location history form the stored cache.
|
||||
$weatherdata = $cache->load(cachename: 'weatherdata', key: md5(json_encode($latlong)), callback: function() use ($url) {
|
||||
// Ask the API for some data.
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
|
||||
curl_setopt($ch, CURLOPT_FAILONERROR, true);
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Just in case something went wrong with the request we'll capture the error.
|
||||
if (curl_errno($ch)) {
|
||||
$curlerror = curl_error($ch);
|
||||
}
|
||||
curl_close($ch);
|
||||
// If we had an error then return the error message and exit, otherwise return the API response.
|
||||
if (isset($curlerror)) {
|
||||
die($curlerror);
|
||||
}
|
||||
return $response;
|
||||
});
|
||||
|
||||
// We made it here so output the API response as json.
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
echo $weatherdata;
|
||||
@@ -30,8 +30,12 @@ body {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.enable {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.background {
|
||||
filter: brightness(0.8) blur(10px);
|
||||
filter: brightness(0.85) blur(10px);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
background-position: center center;
|
||||
@@ -39,8 +43,41 @@ body {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 15px 15px 0 15px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.show-tags {
|
||||
height: 55px;
|
||||
width: 55px;
|
||||
display: inline-block;
|
||||
background-position: top 50% left 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../images/tags.svg);
|
||||
background-size: 35px;
|
||||
background-color: #ffffff15;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid #ffffff20;
|
||||
}
|
||||
|
||||
.show-tags:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
background-image: url(../images/tags-dark.svg);
|
||||
border: 2px solid #cecece;
|
||||
transition: background-color, background-image .1s;
|
||||
}
|
||||
|
||||
.content {
|
||||
z-index: 10;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content:center;
|
||||
@@ -54,48 +91,67 @@ body {
|
||||
font-size: 2.3em;
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
text-shadow: 1px 1px 2px #00000070;
|
||||
text-shadow: 1px 1px 2px #000000a0;
|
||||
margin-top: -50px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.greeting .tagname {
|
||||
text-transform: lowercase;
|
||||
}
|
||||
.greeting .tagname span {
|
||||
opacity: 0.5;
|
||||
margin-right:5px;
|
||||
}
|
||||
|
||||
.widget {
|
||||
display: inline-block;
|
||||
padding:5px 10px;
|
||||
height: 58px;
|
||||
user-select: none;
|
||||
z-index:1000;
|
||||
}
|
||||
|
||||
.widget.clickable {
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.widget.clickable:hover {
|
||||
background-color: #ffffff15;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
|
||||
.time-weather {
|
||||
display: block;
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
bottom: 10px;
|
||||
padding:5px 10px;
|
||||
height: 58px;
|
||||
z-index: 100;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-weight: 400;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
text-shadow: 1px 1px 2px #00000070;
|
||||
user-select: none;
|
||||
border-radius: 6px;
|
||||
text-shadow: 1px 1px 2px #000000a0;
|
||||
}
|
||||
.time-weather:hover {
|
||||
background-color: #ffffff15;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
|
||||
.time {
|
||||
display: inline-block;
|
||||
font-size: 2.4em;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.weather {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.weather-icon {
|
||||
display: inline-block;
|
||||
font-size: 1.9em;
|
||||
vertical-align: middle;
|
||||
height: 48px;
|
||||
line-height: 48px !important;
|
||||
}
|
||||
.weather::before {
|
||||
.weather-icon::before {
|
||||
position: relative;
|
||||
top: 4px;
|
||||
/* top: 4px; */
|
||||
}
|
||||
|
||||
.weather-info {
|
||||
@@ -107,20 +163,16 @@ body {
|
||||
font-weight: 600;
|
||||
line-height: normal;
|
||||
vertical-align: middle;
|
||||
text-shadow: 1px 1px 1px #000000a0;
|
||||
}
|
||||
|
||||
.useclientlocation {
|
||||
font-size: 14px;
|
||||
text-shadow: 1px 1px 2px #00000070;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
text-shadow: 1px 1px 1px #000000a0;
|
||||
display: none;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
left: 15px;
|
||||
z-index: 10;
|
||||
border-radius: 6px;
|
||||
height: 58px;
|
||||
line-height:58px;
|
||||
padding: 0 10px 0 45px;
|
||||
background-size: 37px;
|
||||
@@ -129,20 +181,12 @@ body {
|
||||
background-image: url(../images/map-pin.svg);
|
||||
}
|
||||
|
||||
.useclientlocation:hover {
|
||||
background-color: #ffffff15;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
|
||||
.useclientlocation.enable {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sites, .sites li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
font-size: 14px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.sites li {
|
||||
@@ -172,10 +216,15 @@ body {
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
padding: 15px;
|
||||
margin-bottom: 8px;
|
||||
background-image: url(../images/loading.svg);
|
||||
background-repeat: no-repeat;
|
||||
background-position: 50%;
|
||||
background-size: 20px;
|
||||
}
|
||||
|
||||
.sites .icon img {
|
||||
width:100%;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.sites .name {
|
||||
@@ -184,7 +233,84 @@ body {
|
||||
max-height: 3.3em;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
text-shadow: 1px 1px 2px #00000070;
|
||||
text-shadow: 1px 1px 1px #000000a0;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tags {
|
||||
display:none;
|
||||
color: #202124;
|
||||
position: fixed;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
text-align: left;
|
||||
background-color: #fff;
|
||||
border-radius: 6px;
|
||||
border: .2em solid #cecece;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
padding: 15px 15px 15px 15px;
|
||||
min-width: 250px;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
font-weight: 400;
|
||||
z-index:100;
|
||||
}
|
||||
.tags:target {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tags .header {
|
||||
font-size: 20px;
|
||||
height: 35px;
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
line-height: 18px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.tags .header .close {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 48px;
|
||||
width: 48px;
|
||||
display: inline-block;
|
||||
background-position: top 50% left 50%;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../images/close-dark.svg);
|
||||
background-size: 30px;
|
||||
cursor: pointer;
|
||||
border: 5px solid #fff;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.tags .header .close:hover {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
|
||||
.tags ul {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
list-style-position: inside;
|
||||
}
|
||||
.tags ul li {
|
||||
text-transform: lowercase;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tags ul li::marker {
|
||||
color: #bbb;
|
||||
content: '#';
|
||||
}
|
||||
.tags ul li a {
|
||||
display:inline-block;
|
||||
color: inherit;
|
||||
text-decoration: dotted;
|
||||
padding: 3px 5px;
|
||||
margin-left: 1px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tags ul li a:hover {
|
||||
background-color: #f3f3f3;
|
||||
transition: background-color .1s;
|
||||
}
|
||||
|
||||
5
jumpapp/assets/images/close-dark.svg
Normal file
5
jumpapp/assets/images/close-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-x" width="32" height="32" viewBox="0 0 24 24" stroke-width="1" stroke="#222222" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="18" y1="6" x2="6" y2="18" />
|
||||
<line x1="6" y1="6" x2="18" y2="18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
15
jumpapp/assets/images/loading.svg
Normal file
15
jumpapp/assets/images/loading.svg
Normal file
@@ -0,0 +1,15 @@
|
||||
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
|
||||
<svg width="120" height="30" viewBox="0 0 120 30" xmlns="http://www.w3.org/2000/svg" fill="#888">
|
||||
<circle cx="15" cy="15" r="15">
|
||||
<animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/>
|
||||
<animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="60" cy="15" r="9" fill-opacity="0.3">
|
||||
<animate attributeName="r" from="9" to="9" begin="0s" dur="0.8s" values="9;15;9" calcMode="linear" repeatCount="indefinite"/>
|
||||
<animate attributeName="fill-opacity" from="0.5" to="0.5" begin="0s" dur="0.8s" values=".5;1;.5" calcMode="linear" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
<circle cx="105" cy="15" r="15">
|
||||
<animate attributeName="r" from="15" to="15" begin="0s" dur="0.8s" values="15;9;15" calcMode="linear" repeatCount="indefinite"/>
|
||||
<animate attributeName="fill-opacity" from="1" to="1" begin="0s" dur="0.8s" values="1;.5;1" calcMode="linear" repeatCount="indefinite"/>
|
||||
</circle>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -1,6 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-map-pin-off" width="44" height="44" viewBox="0 0 24 24" stroke-width="1" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<line x1="3" y1="3" x2="21" y2="21" />
|
||||
<path d="M9.44 9.435a3 3 0 0 0 4.126 4.124m1.434 -2.559a3 3 0 0 0 -3 -3" />
|
||||
<path d="M8.048 4.042a8 8 0 0 1 10.912 10.908m-1.8 2.206l-3.745 3.744a2 2 0 0 1 -2.827 0l-4.244 -4.243a8 8 0 0 1 -.48 -10.79" />
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 536 B |
Binary file not shown.
|
Before Width: | Height: | Size: 76 B |
6
jumpapp/assets/images/tags-dark.svg
Normal file
6
jumpapp/assets/images/tags-dark.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tags" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#222222" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z" />
|
||||
<path d="M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116" />
|
||||
<path d="M6 9h-.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
6
jumpapp/assets/images/tags.svg
Normal file
6
jumpapp/assets/images/tags.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-tags" width="44" height="44" viewBox="0 0 24 24" stroke-width="1.5" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<path d="M7.859 6h-2.834a2.025 2.025 0 0 0 -2.025 2.025v2.834c0 .537 .213 1.052 .593 1.432l6.116 6.116a2.025 2.025 0 0 0 2.864 0l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-6.117 -6.116a2.025 2.025 0 0 0 -1.431 -.593z" />
|
||||
<path d="M17.573 18.407l2.834 -2.834a2.025 2.025 0 0 0 0 -2.864l-7.117 -7.116" />
|
||||
<path d="M6 9h-.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 611 B |
File diff suppressed because one or more lines are too long
8
jumpapp/assets/js/index.bundle.js.LICENSE.txt
Normal file
8
jumpapp/assets/js/index.bundle.js.LICENSE.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* Calculate the time, local to the requested location from
|
||||
* the OpenWeather API, by passing in the number of seconds
|
||||
* that location has shifted from UTC based on the timezones.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
@@ -13,7 +13,13 @@ export default class Clock {
|
||||
*
|
||||
* @param number utcshift Number of seconds to shift time from UTC.
|
||||
*/
|
||||
constructor(utcshift = 0) {
|
||||
constructor(eventemitter) {
|
||||
this.set_utc_shift();
|
||||
this.contentintervalid = null;
|
||||
this.eventemitter = eventemitter;
|
||||
}
|
||||
|
||||
set_utc_shift(utcshift = 0) {
|
||||
this.utcshift = utcshift*1000;
|
||||
this.shiftedtimestamp = new Date().getTime()+this.utcshift;
|
||||
this.shifteddate = new Date(this.shiftedtimestamp);
|
||||
@@ -42,4 +48,27 @@ export default class Clock {
|
||||
return this.shifteddate.getUTCHours();
|
||||
}
|
||||
|
||||
update_time() {
|
||||
this.set_utc_shift(this.utcshift);
|
||||
this.eventemitter.emit('clock-updated', {
|
||||
formatted_time: this.get_formatted_time(),
|
||||
hour: this.get_hour(),
|
||||
utcshift: this.utcshift
|
||||
});
|
||||
}
|
||||
|
||||
run(updatefrequency) {
|
||||
// Clear any previously set intervals for updating content.
|
||||
if (this.contentintervalid) {
|
||||
clearInterval(this.contentintervalid);
|
||||
}
|
||||
// Set the clock and greeting text appropriately for the requested location.
|
||||
this.update_time();
|
||||
// Update the content periodically, we don't need to be too frequent as we are
|
||||
// not displaying seconds on the clock.
|
||||
this.contentintervalid = setInterval(() => {
|
||||
this.update_time();
|
||||
}, updatefrequency);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import Clock from "./Clock";
|
||||
|
||||
export default class Greeting {
|
||||
|
||||
constructor(clock) {
|
||||
this.hour = clock.get_hour();
|
||||
constructor(hour) {
|
||||
this.hour = hour;
|
||||
this.greetings = {
|
||||
0 : 'morning',
|
||||
12 : 'afternoon',
|
||||
|
||||
@@ -1,83 +1,116 @@
|
||||
import Clock from './Clock';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import Greeting from './Greeting';
|
||||
import Weather from './Weather';
|
||||
|
||||
export default class Main {
|
||||
|
||||
constructor() {
|
||||
this.owmapikey = null;
|
||||
this.latlong = [];
|
||||
this.storage = window.localStorage;
|
||||
this.updatefrequency = 10000;
|
||||
this.contentintervalid = null;
|
||||
this.timezoneshift = 0;
|
||||
this.metrictemp = JUMP.metrictemp;
|
||||
|
||||
// Cache some DOM elements that we will access frequently.
|
||||
this.greetingelm = document.querySelector('.greeting .chosen');
|
||||
this.holderelm = document.querySelector('.time-weather');
|
||||
this.tempelm = document.querySelector('.weather-info .temp');
|
||||
this.weatherdescelm = document.querySelector('.weather-info .desc');
|
||||
this.tempelm = this.holderelm.querySelector('.weather-info .temp');
|
||||
this.weatherdescelm = this.holderelm.querySelector('.weather-info .desc');
|
||||
this.timeelm = this.holderelm.querySelector('.time');
|
||||
this.weatherelm = this.holderelm.querySelector('.weather');
|
||||
this.weathericonelm = this.holderelm.querySelector('.weather-icon');
|
||||
this.clientlocationelm = document.querySelector('.useclientlocation');
|
||||
|
||||
// See if we were provided a latlong and api key via the apps config.php.
|
||||
if (JUMP.latlong && JUMP.owmapikey) {
|
||||
this.owmapikey = JUMP.owmapikey;
|
||||
this.latlong = JUMP.latlong.split(',');
|
||||
}
|
||||
|
||||
this.showtagsbuttonelm = document.querySelector('.show-tags');
|
||||
this.tagselectorelm = document.querySelector('.tags');
|
||||
this.tagsselectorclosebuttonelm = document.querySelector('.tags .close')
|
||||
// If the user has previously asked for geolocation we will have stored the latlong.
|
||||
if (this.lastrequestedlocation = this.storage.getItem('lastrequestedlocation')){
|
||||
this.latlong = JSON.parse(this.lastrequestedlocation);
|
||||
}
|
||||
// Finally create instances of the classes we'll be using.
|
||||
this.eventemitter = new EventEmitter();
|
||||
this.clock = new Clock(this.eventemitter);
|
||||
this.weather = new Weather(this.eventemitter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get data from OWM and do stuff with it.
|
||||
*/
|
||||
init() {
|
||||
// Start listening for events so we can do stuff when needed.
|
||||
this.add_event_listeners();
|
||||
// If there is no OWM API key provided then just update the greeting
|
||||
// and clock, otherwise we can go get the weather data and set everything
|
||||
// up properly.
|
||||
if (!this.owmapikey) {
|
||||
this.refresh_basic_content();
|
||||
this.show_content();
|
||||
if (!JUMP.owmapikey) {
|
||||
this.eventemitter.emit('show-content');
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve weather and timezone data from Open Weather Map API.
|
||||
new Weather(this.owmapikey, this.latlong, this.metrictemp).fetch_owm_data().then(owmdata => {
|
||||
this.weather.fetch_owm_data(this.latlong);
|
||||
}
|
||||
|
||||
/**
|
||||
* Umm... adds event listeners
|
||||
*/
|
||||
add_event_listeners() {
|
||||
this.eventemitter.on('weather-loaded', owmdata => {
|
||||
// Update the timezone shift from UTC to whatever it should be for the
|
||||
// requested location, then tell the greeting and clock to update.
|
||||
this.timezoneshift = owmdata.timezoneshift;
|
||||
this.refresh_basic_content();
|
||||
|
||||
// Display the weather icon, link to the requested location in OWM
|
||||
// and update location name element.
|
||||
this.holderelm.href = 'https://openweathermap.org/city/' + owmdata.locationcode;
|
||||
this.weatherelm.classList.add(owmdata.iconclass);
|
||||
this.weatherelm.href = 'https://openweathermap.org/city/' + owmdata.locationcode;
|
||||
this.weathericonelm.classList.add(owmdata.iconclass);
|
||||
this.clientlocationelm.innerHTML = owmdata.locationname;
|
||||
this.tempelm.innerHTML = owmdata.temp;
|
||||
this.weatherdescelm.innerHTML = owmdata.description;
|
||||
|
||||
// Should someone click on the location button then request their location
|
||||
// from the client and store it, then re run init() to update the page.
|
||||
this.clientlocationelm.addEventListener('click', e => {
|
||||
navigator.geolocation.getCurrentPosition(position => {
|
||||
this.latlong = [position.coords.latitude, position.coords.longitude];
|
||||
this.storage.setItem('lastrequestedlocation', JSON.stringify(this.latlong));
|
||||
this.init();
|
||||
}, null, {enableHighAccuracy: true});
|
||||
}, {once: true});
|
||||
this.clientlocationelm.classList.add('enable');
|
||||
this.eventemitter.emit('show-content');
|
||||
});
|
||||
|
||||
// Finally we can make everything visible.
|
||||
this.eventemitter.on('clock-updated', clockdata => {
|
||||
if (this.timeelm != null) {
|
||||
this.timeelm.innerHTML = clockdata.formatted_time;
|
||||
}
|
||||
if (this.greetingelm != null) {
|
||||
let greeting = new Greeting(clockdata.hour);
|
||||
this.greetingelm.innerHTML = greeting.get_greeting();
|
||||
}
|
||||
});
|
||||
|
||||
this.eventemitter.on('show-content', () => {
|
||||
this.set_clock();
|
||||
this.show_content();
|
||||
});
|
||||
|
||||
// Should someone click on the location button then request their location
|
||||
// from the client and store it, then refetch weather data to update the page.
|
||||
this.clientlocationelm.addEventListener('click', e => {
|
||||
navigator.geolocation.getCurrentPosition(position => {
|
||||
this.latlong = [position.coords.latitude, position.coords.longitude];
|
||||
this.storage.setItem('lastrequestedlocation', JSON.stringify(this.latlong));
|
||||
this.weather.fetch_owm_data(this.latlong);
|
||||
},
|
||||
error => {
|
||||
console.error(error.message);
|
||||
},
|
||||
{enableHighAccuracy: true});
|
||||
});
|
||||
|
||||
if (this.showtagsbuttonelm) {
|
||||
this.showtagsbuttonelm.addEventListener('click', e => {
|
||||
this.tagselectorelm.classList.add('enable');
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
if (this.tagsselectorclosebuttonelm) {
|
||||
this.tagsselectorclosebuttonelm.addEventListener('click', e => {
|
||||
this.tagselectorelm.classList.remove('enable');
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,37 +124,9 @@ export default class Main {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the correct time for the requested location and display it,
|
||||
* along with an appropriate greeting.
|
||||
*/
|
||||
update_basic_content() {
|
||||
let clock = new Clock(this.timezoneshift);
|
||||
let greeting = new Greeting(clock);
|
||||
if (this.timeelm != null) {
|
||||
this.timeelm.innerHTML = clock.get_formatted_time();
|
||||
}
|
||||
this.greetingelm.innerHTML = greeting.get_greeting();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the greeting message and clock initially, then continue to update
|
||||
* them at the frequency set in this.updatefrequency.
|
||||
*/
|
||||
refresh_basic_content() {
|
||||
// Clear any previously set intervals for updating content.
|
||||
if (this.contentintervalid) {
|
||||
clearInterval(this.contentintervalid);
|
||||
}
|
||||
|
||||
// Set the clock and greeting text appropriately for the requested location.
|
||||
this.update_basic_content();
|
||||
|
||||
// Update the content periodically, we don't need to be too frequent as we are
|
||||
// not displaying seconds on the clock.
|
||||
this.contentintervalid = setInterval(() => {
|
||||
this.update_basic_content();
|
||||
}, this.updatefrequency);
|
||||
set_clock() {
|
||||
this.clock.set_utc_shift(this.timezoneshift);
|
||||
this.clock.run(this.updatefrequency);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,34 +1,27 @@
|
||||
export default class Weather {
|
||||
|
||||
/**
|
||||
* Reposible for retrieveing weather data from OWM and doing
|
||||
* Responsible for retrieveing weather data from OWM and doing
|
||||
* stuff with it.
|
||||
*
|
||||
* @param {string} owmapikey OWM API key.
|
||||
* @param {string} latlong Comma separated string representing a lattitude and longitude.
|
||||
* @param {boolean} metrictemp Are temperature units in metric or imperial.
|
||||
*/
|
||||
constructor(owmapikey, latlong, metrictemp) {
|
||||
this.owmapiurlbase = 'https://api.openweathermap.org/data/2.5/weather';
|
||||
this.owmapikey = owmapikey;
|
||||
this.latlong = latlong;
|
||||
this.metrictemp = metrictemp
|
||||
constructor(eventemitter) {
|
||||
this.eventemitter = eventemitter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Make an async request to the OWM API, parse and return the response.
|
||||
*
|
||||
* @returns {Promise} Containing parsed OWM data.
|
||||
* Make an async request to the weather API, parse and return the response.
|
||||
*/
|
||||
async fetch_owm_data() {
|
||||
const url = this.owmapiurlbase
|
||||
+'?units=' + (this.metrictemp ? 'metric' : 'imperial')
|
||||
+'&lat=' + this.latlong[0]
|
||||
+'&lon=' + this.latlong[1]
|
||||
+'&appid=' + this.owmapikey;
|
||||
|
||||
// Get some data from the open weather map api...
|
||||
const promise = await fetch(url)
|
||||
fetch_owm_data(latlong) {
|
||||
// If we are provided with a latlong then the user must have cliecked on the location
|
||||
// button at some point, so let's use this in the api url...
|
||||
let apiurl = '/api/weatherdata.php';
|
||||
if (latlong.length) {
|
||||
apiurl += ('?lat=' + latlong[0] + '&lon=' + latlong[1]);
|
||||
}
|
||||
// Get some data from the weather api...
|
||||
fetch(apiurl)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.cod === 401) {
|
||||
@@ -39,16 +32,15 @@ export default class Weather {
|
||||
if (data.dt > data.sys.sunrise && data.dt < data.sys.sunset) {
|
||||
daynightvariant = 'day'
|
||||
}
|
||||
return {
|
||||
this.eventemitter.emit('weather-loaded', {
|
||||
locationcode: data.id,
|
||||
locationname: data.name,
|
||||
temp: Math.ceil(data.main.temp) + '°' + (this.metrictemp ? 'C' : 'F'),
|
||||
temp: Math.ceil(data.main.temp) + '°' + (JUMP.metrictemp ? 'C' : 'F'),
|
||||
description: data.weather[0].main,
|
||||
iconclass: 'wi-owm-' + daynightvariant + '-' + data.weather[0].id,
|
||||
timezoneshift: data.timezone
|
||||
};
|
||||
});
|
||||
})
|
||||
return promise;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,7 +10,10 @@
|
||||
require __DIR__ .'/vendor/autoload.php';
|
||||
|
||||
$config = new Jump\Config();
|
||||
$backgroundfile = (new Jump\Background($config))->get_random_background_file();
|
||||
$backgroundimgfile = (new Jump\Background($config))->get_random_background_file();
|
||||
|
||||
$blur = floor((int)$config->get('bgblur', false) / 100 * 15);
|
||||
$brightness = (int)$config->get('bgbright', false) ? (int)$config->get('bgbright', false) / 100 : 1;
|
||||
|
||||
header('Content-Type: text/css');
|
||||
echo '.background {background-image: url("'.$backgroundfile.'");}';
|
||||
echo '.background {background-image: url("'.$backgroundimgfile.'");filter: brightness('.$brightness.') blur('.$blur.'px);}';
|
||||
@@ -14,8 +14,7 @@ class Background {
|
||||
private string $backgroundsdirectory;
|
||||
private array $backgroundfiles;
|
||||
|
||||
public function __construct(Config $config) {
|
||||
$this->config = $config;
|
||||
public function __construct(private Config $config) {
|
||||
$this->backgroundsdirectory = $config->get('backgroundsdir');
|
||||
$this->webaccessibledir = str_replace($config->get('wwwroot'), '', $config->get('backgroundsdir'));
|
||||
$this->enumerate_files();
|
||||
|
||||
@@ -21,14 +21,12 @@ class Cache {
|
||||
* @var array Multidimensional array
|
||||
*/
|
||||
private array $caches;
|
||||
private Config $config;
|
||||
|
||||
/**
|
||||
* Creates file storage for cache and initialises cache objects for each
|
||||
* name/type specified in $caches definition.
|
||||
*/
|
||||
public function __construct(Config $config) {
|
||||
$this->config = $config;
|
||||
public function __construct(private Config $config) {
|
||||
// Define the various caches used throughout the app.
|
||||
$this->caches = [
|
||||
'sites' => [
|
||||
@@ -36,6 +34,11 @@ class Cache {
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
'expirationparams' => $config->get('sitesfile')
|
||||
],
|
||||
'tags' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
'expirationparams' => $config->get('sitesfile')
|
||||
],
|
||||
'templates/sites' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
@@ -44,25 +47,34 @@ class Cache {
|
||||
$config->get('sitesfile'),
|
||||
$config->get('templatedir').'/sites.mustache'
|
||||
]
|
||||
]
|
||||
],
|
||||
'templates/errorpage' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
'expirationparams' => [
|
||||
$config->get('templatedir').'/errorpage.mustache'
|
||||
]
|
||||
],
|
||||
'weatherdata' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::EXPIRE,
|
||||
'expirationparams' => '5 minutes'
|
||||
],
|
||||
];
|
||||
// Inititalise file storage for cache using cachedir path from config.
|
||||
$this->storage = new Caching\Storages\FileStorage($this->config->get('cachedir').'/application');
|
||||
// Initialise a cache object for each cache name/type specified in caches array.
|
||||
array_walk($this->caches, function(&$cachedef, $cachename) {
|
||||
$cachedef['cache'] = new Caching\Cache($this->storage, $cachename);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the specified item from the cache or generate it, mostly a wrapper
|
||||
* around Nette\Caching\Cache::load().
|
||||
*
|
||||
* @param string $cachename The name of a cache type, must match a key in $caches definition.
|
||||
* @param string $cachename The name of a cache, must match a key in $caches definition.
|
||||
* @param string $key A key used to represent an object within a cache,
|
||||
* @param callable $callback The code from which the result should be stored in cache.
|
||||
* @return mixed The result of callback function retreieved from cache.
|
||||
*/
|
||||
public function load(string $cachename, callable $callback): mixed {
|
||||
public function load(string $cachename, ?string $key = 'default', callable $callback): mixed {
|
||||
// If cachebypass has been set in config.php then just execute the callback.
|
||||
if ($this->config->parse_bool($this->config->get('cachebypass'))) {
|
||||
return $callback();
|
||||
@@ -71,9 +83,13 @@ class Cache {
|
||||
if (!array_key_exists($cachename, $this->caches)) {
|
||||
throw new \Exception('Cache name not found ('.$cachename.')');
|
||||
}
|
||||
// If a cache key has not been used then intialise a cache object for it.
|
||||
if (!isset($this->caches[$cachename]['cache']) || !array_key_exists($key, $this->caches[$cachename]['cache'])) {
|
||||
$this->caches[$cachename]['cache'][$key] = new Caching\Cache($this->storage, $cachename.'/'.$key);
|
||||
}
|
||||
// Retrieve the initialised cache object from $caches, defines the caches expiry
|
||||
// and executes the callback.
|
||||
return $this->caches[$cachename]['cache']->load($cachename,
|
||||
return $this->caches[$cachename]['cache'][$key]->load($cachename.'/'.$key,
|
||||
function (&$dependencies) use ($callback, $cachename) {
|
||||
$dependencies[$this->caches[$cachename]['expirationtype']] = $this->caches[$cachename]['expirationparams'];
|
||||
return $callback();
|
||||
|
||||
5
jumpapp/classes/Exceptions/TagNotFoundException.php
Normal file
5
jumpapp/classes/Exceptions/TagNotFoundException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php
|
||||
|
||||
namespace Jump\Exceptions;
|
||||
|
||||
class TagNotFoundException extends \Exception {}
|
||||
@@ -1,61 +1,45 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* TO DO:
|
||||
* - use CSRF token in weatherdata and icon api
|
||||
*
|
||||
*/
|
||||
|
||||
namespace Jump;
|
||||
|
||||
use Nette\Routing\RouteList;
|
||||
|
||||
class Main {
|
||||
|
||||
private Cache $cache;
|
||||
private \Mustache_Engine $mustache;
|
||||
private array $outputarray;
|
||||
private Sites $sites;
|
||||
private Config $config;
|
||||
|
||||
public function __construct() {
|
||||
$this->config = new Config();
|
||||
$this->mustache = new \Mustache_Engine([
|
||||
'loader' => new \Mustache_Loader_FilesystemLoader($this->config->get('templatedir'))
|
||||
]);
|
||||
$this->cache = new Cache($this->config);
|
||||
$this->sites = new Sites($this->config, $this->cache);
|
||||
$this->router = new RouteList;
|
||||
|
||||
// Set up the routes that Jump expects.
|
||||
$this->router->addRoute('/tag/<param>', [
|
||||
'class' => 'Jump\Pages\TagPage'
|
||||
]);
|
||||
}
|
||||
|
||||
private function render_header(): string {
|
||||
$template = $this->mustache->loadTemplate('header');
|
||||
return $template->render([
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
'sitename' => $this->config->get('sitename'),
|
||||
'latlong' => $this->config->get('latlong', false),
|
||||
'owmapikey' => $this->config->get('owmapikey', false),
|
||||
'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp')),
|
||||
]);
|
||||
}
|
||||
function init() {
|
||||
// Try to match the correct route based on the HTTP request.
|
||||
$matchedroute = $this->router->match(
|
||||
(new \Nette\Http\RequestFactory)->fromGlobals()
|
||||
);
|
||||
|
||||
private function render_sites(): string {
|
||||
return $this->cache->load('templates/sites', function() {
|
||||
$template = $this->mustache->loadTemplate('sites');
|
||||
return $template->render([
|
||||
'hassites' => !empty($this->sites->get_sites()),
|
||||
'sites' => $this->sites->get_sites()
|
||||
]);
|
||||
});
|
||||
}
|
||||
// If we do not have a matched route then just serve up the home page.
|
||||
$pageclass = $matchedroute['class'] ?? 'Jump\Pages\HomePage';
|
||||
$param = $matchedroute['param'] ?? null;
|
||||
|
||||
private function render_footer(): string {
|
||||
$template = $this->mustache->loadTemplate('footer');
|
||||
return $template->render([
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
]);
|
||||
}
|
||||
|
||||
public function build_index_page(): void {
|
||||
$this->outputarray = [
|
||||
$this->render_header(),
|
||||
$this->render_sites(),
|
||||
$this->render_footer(),
|
||||
];
|
||||
}
|
||||
|
||||
public function get_output(): string {
|
||||
return implode('', $this->outputarray);
|
||||
// Instantiate the correct class to build the requested page, get the
|
||||
// content and return it.
|
||||
$page = new $pageclass($this->config, $this->cache, $param ?? null);
|
||||
return $page->get_output();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
56
jumpapp/classes/Pages/AbstractPage.php
Normal file
56
jumpapp/classes/Pages/AbstractPage.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace Jump\Pages;
|
||||
|
||||
abstract class AbstractPage {
|
||||
|
||||
protected \Mustache_Engine $mustache;
|
||||
private array $outputarray;
|
||||
|
||||
/**
|
||||
* Construct an instance of a page.
|
||||
*
|
||||
* @param \Jump\Config $config
|
||||
* @param \Jump\Cache $cache
|
||||
* @param string|null $generic param, passed from router.
|
||||
*/
|
||||
public function __construct(protected \Jump\Config $config, protected \Jump\Cache $cache, protected ?string $param = null) {
|
||||
$this->hastags = false;
|
||||
$this->mustache = new \Mustache_Engine([
|
||||
'loader' => new \Mustache_Loader_FilesystemLoader($this->config->get('templatedir'))
|
||||
]);
|
||||
}
|
||||
|
||||
abstract protected function render_content(): string;
|
||||
|
||||
protected function render_header(): string {
|
||||
$template = $this->mustache->loadTemplate('header');
|
||||
return $template->render([
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
'title' => $this->config->get('sitename'),
|
||||
'owmapikey' => !!$this->config->get('owmapikey', false),
|
||||
'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp'))
|
||||
]);
|
||||
}
|
||||
|
||||
protected function render_footer(): string {
|
||||
$template = $this->mustache->loadTemplate('footer');
|
||||
return $template->render([
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
]);
|
||||
}
|
||||
|
||||
protected function render_page(): void {
|
||||
$this->outputarray = [
|
||||
$this->render_header(),
|
||||
$this->render_content(),
|
||||
$this->render_footer(),
|
||||
];
|
||||
}
|
||||
|
||||
public function get_output(): string {
|
||||
$this->render_page();
|
||||
return implode('', $this->outputarray);
|
||||
}
|
||||
|
||||
}
|
||||
26
jumpapp/classes/Pages/ErrorPage.php
Normal file
26
jumpapp/classes/Pages/ErrorPage.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace Jump\Pages;
|
||||
|
||||
class ErrorPage {
|
||||
|
||||
private string $content;
|
||||
|
||||
public function __construct(private \Jump\Cache $cache, private \Jump\Config $config, private int $httpcode, public string $message) {
|
||||
$this->mustache = new \Mustache_Engine([
|
||||
'loader' => new \Mustache_Loader_FilesystemLoader($this->config->get('templatedir'))
|
||||
]);
|
||||
$this->content = $cache->load(cachename: 'templates/errorpage', key: $httpcode.md5($message), callback: function() use ($httpcode, $message) {
|
||||
$template = $this->mustache->loadTemplate('errorpage');
|
||||
return $template->render([
|
||||
'code' => $httpcode,
|
||||
'message' => $message
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
public function init() {
|
||||
http_response_code($this->httpcode);
|
||||
die($this->content);
|
||||
}
|
||||
}
|
||||
45
jumpapp/classes/Pages/HomePage.php
Normal file
45
jumpapp/classes/Pages/HomePage.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Jump\Pages;
|
||||
|
||||
class HomePage extends AbstractPage {
|
||||
|
||||
protected function render_header(): string {
|
||||
$template = $this->mustache->loadTemplate('header');
|
||||
$greeting = null;
|
||||
if (!$this->config->parse_bool($this->config->get('showgreeting'))) {
|
||||
$greeting = 'home';
|
||||
}
|
||||
return $template->render([
|
||||
'greeting' => $greeting,
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
'title' => $this->config->get('sitename'),
|
||||
'owmapikey' => !!$this->config->get('owmapikey', false),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function render_content(): string {
|
||||
return $this->cache->load(cachename: 'templates/sites', callback: function() {
|
||||
$sites = new \Jump\Sites($this->config, $this->cache);
|
||||
$template = $this->mustache->loadTemplate('sites');
|
||||
return $template->render([
|
||||
'hassites' => !empty($sites->get_sites()),
|
||||
'sites' => $sites->get_sites_by_tag('home'),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
protected function render_footer(): string {
|
||||
return $this->cache->load(cachename: 'templates/sites', key: 'footer', callback: function() {
|
||||
$sites = new \Jump\Sites(config: $this->config, cache: $this->cache);
|
||||
$tags = $sites->get_tags_for_template();
|
||||
$template = $this->mustache->loadTemplate('footer');
|
||||
return $template->render([
|
||||
'hastags' => !empty($tags),
|
||||
'tags' => $tags,
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
52
jumpapp/classes/Pages/TagPage.php
Normal file
52
jumpapp/classes/Pages/TagPage.php
Normal file
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Jump\Pages;
|
||||
|
||||
use \Jump\Exceptions\TagNotFoundException;
|
||||
|
||||
class TagPage extends AbstractPage {
|
||||
|
||||
protected function render_header(): string {
|
||||
$template = $this->mustache->loadTemplate('header');
|
||||
$greeting = $this->param;
|
||||
$title = 'Tag: '.$this->param;
|
||||
return $template->render([
|
||||
'greeting' => $greeting,
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
'title' => $title,
|
||||
'owmapikey' => !!$this->config->get('owmapikey', false),
|
||||
]);
|
||||
}
|
||||
|
||||
protected function render_content(): string {
|
||||
$cachekey = isset($this->param) ? 'tag:'.$this->param : null;
|
||||
return $this->cache->load(cachename: 'templates/sites', key: $cachekey, callback: function() {
|
||||
$sites = new \Jump\Sites(config: $this->config, cache: $this->cache);
|
||||
try {
|
||||
$taggedsites = $sites->get_sites_by_tag($this->param);
|
||||
}
|
||||
catch (TagNotFoundException) {
|
||||
(new ErrorPage($this->cache, $this->config, 404, 'There are no sites with this tag.'))->init();
|
||||
}
|
||||
$template = $this->mustache->loadTemplate('sites');
|
||||
return $template->render([
|
||||
'hassites' => !empty($taggedsites),
|
||||
'sites' => $taggedsites,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
protected function render_footer(): string {
|
||||
return $this->cache->load(cachename: 'templates/sites', key: 'footer', callback: function() {
|
||||
$sites = new \Jump\Sites(config: $this->config, cache: $this->cache);
|
||||
$tags = $sites->get_tags_for_template();
|
||||
$template = $this->mustache->loadTemplate('footer');
|
||||
return $template->render([
|
||||
'hastags' => !empty($tags),
|
||||
'tags' => $tags,
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Jump;
|
||||
|
||||
use stdClass;
|
||||
|
||||
/**
|
||||
* Parse the data required to represent a site and provide method for generating
|
||||
* and/or retrieving the site's icon.
|
||||
@@ -11,43 +13,49 @@ namespace Jump;
|
||||
*/
|
||||
class Site {
|
||||
|
||||
private Config $config;
|
||||
public string $name;
|
||||
public bool $nofollow;
|
||||
public string $icon;
|
||||
public ?string $iconname;
|
||||
public string $url;
|
||||
public array $tags = ['home'];
|
||||
|
||||
public function __construct(Config $config, array $sitearray, array $default) {
|
||||
$this->config = $config;
|
||||
$this->defaults = $default;
|
||||
/**
|
||||
* Parse the data required to represent a site and provide method for generating
|
||||
* and/or retrieving the site's icon.
|
||||
*
|
||||
* @param Config $config A Jump Config() object.
|
||||
* @param array $sitearray Array of options for this site from sites.json.
|
||||
* @param array $defaults Array of default values for this site to use, defined in sites.json.
|
||||
*/
|
||||
public function __construct(private Config $config, array $sitearray, private array $defaults) {
|
||||
if (!isset($sitearray['name'], $sitearray['url'])) {
|
||||
throw new \Exception('The array passed to Site() must contain the keys "name" and "url"!');
|
||||
}
|
||||
$this->name = $sitearray['name'];
|
||||
$this->url = $sitearray['url'];
|
||||
$this->nofollow = isset($sitearray['nofollow']) ? $sitearray['nofollow'] : (isset($this->defaults['nofollow']) ? $this->defaults['nofollow'] : false);
|
||||
$this->icon = isset($sitearray['icon']) ? $this->get_favicon_datauri($sitearray['icon']) : $this->get_favicon_datauri();
|
||||
$this->iconname = $sitearray['icon'] ?? null;
|
||||
$this->tags = $sitearray['tags'] ?? $this->tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a data uri for a given icon, or a site's favicon if an icon
|
||||
* is not provided.
|
||||
* Return an object containing mimetype and raw image data, or a site's
|
||||
* favicon if an icon is not provided in sites.json.
|
||||
*
|
||||
* @param string|null $icon File name of a given icon to retrieve.
|
||||
* @return string Base 64 encoded datauri for the icon image.
|
||||
* @return object Containing mimetype and raw image data.
|
||||
*/
|
||||
public function get_favicon_datauri(?string $icon = null): string {
|
||||
public function get_favicon_image_data(): object {
|
||||
// Use the applications own default icon unless one is supplied via the sites.json file.
|
||||
$defaulticon = $this->config->get('defaulticonpath');
|
||||
if (isset($this->defaults['icon'])) {
|
||||
$defaulticon = $this->config->get('sitesdir').'/icons/'.$this->defaults['icon'];
|
||||
}
|
||||
// Did we have a supplied icon or are we going to try retrieving the favicon?
|
||||
if ($icon === null) {
|
||||
if ($this->iconname === null) {
|
||||
// Go get the favicon, if there isnt one then use the default icon.
|
||||
$favicon = new \Favicon\Favicon();
|
||||
$favicon->cache([
|
||||
'dir' => $this->config->get('cachedir').'/icons/',
|
||||
'dir' => $this->config->get('cachedir').'/icons',
|
||||
'timeout' => 86400
|
||||
]);
|
||||
$rawimage = $favicon->get($this->url, \Favicon\FaviconDLType::RAW_IMAGE);
|
||||
@@ -55,10 +63,22 @@ class Site {
|
||||
$rawimage = file_get_contents($defaulticon);
|
||||
}
|
||||
} else {
|
||||
$rawimage = file_get_contents($this->config->get('sitesdir').'/icons/'.$icon);
|
||||
$rawimage = file_get_contents($this->config->get('sitesdir').'/icons/'.$this->iconname);
|
||||
}
|
||||
$mimetype = (new \finfo(FILEINFO_MIME_TYPE))->buffer($rawimage);
|
||||
return 'data:'.$mimetype.';base64,'.base64_encode($rawimage);
|
||||
$imagedata = new stdClass();
|
||||
$imagedata->mimetype = (new \finfo(FILEINFO_MIME_TYPE))->buffer($rawimage);
|
||||
$imagedata->data = $rawimage;
|
||||
return $imagedata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a data uri or a site's favicon if an icon is not provided.
|
||||
*
|
||||
* @return string Base 64 encoded datauri for the icon image.
|
||||
*/
|
||||
public function get_favicon_datauri(): string {
|
||||
$imagedata = $this->get_favicon_image_data();
|
||||
return 'data:'.$imagedata->mimetype.';base64,'.base64_encode($imagedata->data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@
|
||||
|
||||
namespace Jump;
|
||||
|
||||
use Exception;
|
||||
use \Exception;
|
||||
use \Jump\Exceptions\TagNotFoundException;
|
||||
|
||||
/**
|
||||
* Loads, validates and caches the site data defined in sites.json
|
||||
* into an array of Site objects.
|
||||
*
|
||||
* TO DO: Implement search() method.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
class Sites {
|
||||
|
||||
private Cache $cache;
|
||||
private Config $config;
|
||||
private array $default;
|
||||
private string $sitesfilelocation;
|
||||
private array $loadedsites;
|
||||
@@ -22,7 +23,7 @@ class Sites {
|
||||
/**
|
||||
* Automatically load sites.json on instantiation.
|
||||
*/
|
||||
public function __construct(Config $config, Cache $cache) {
|
||||
public function __construct(private Config $config, private Cache $cache) {
|
||||
$this->config = $config;
|
||||
$this->loadedsites = [];
|
||||
$this->sitesfilelocation = $this->config->get('sitesfile');
|
||||
@@ -31,69 +32,126 @@ class Sites {
|
||||
'icon' => null,
|
||||
'nofollow' => false
|
||||
];
|
||||
$this->load_sites_from_json();
|
||||
}
|
||||
$this->tags = [];
|
||||
|
||||
/**
|
||||
* Try to load the list of sites from site.json.
|
||||
*
|
||||
* Throws an exception if the file cannot be loaded, is empty, or cannot
|
||||
* be decoded to an array,
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception if sites.json cannot be found
|
||||
*/
|
||||
private function load_sites_from_json(): void {
|
||||
$this->loadedsites = $this->cache->load('sites', function() {
|
||||
$sites = [];
|
||||
$rawjson = file_get_contents($this->sitesfilelocation);
|
||||
if ($rawjson === false) {
|
||||
throw new Exception('There was a problem loading the sites.json file');
|
||||
// Retrieve sites from cache. Load all sites from json file if not cached or
|
||||
// the cache has expired.
|
||||
$this->loadedsites = $this->cache->load(cachename: 'sites', callback: function() {
|
||||
return $this->load_sites_from_json();
|
||||
});
|
||||
|
||||
// Enumerate a list of unique tags from loaded sites. Again will retrieve from
|
||||
// cache if available.
|
||||
$this->tags = $this->cache->load(cachename: 'tags', callback: function() {
|
||||
$uniquetags = [];
|
||||
foreach (array_column($this->get_sites(), 'tags') as $tags) {
|
||||
foreach ($tags as $tag) {
|
||||
$uniquetags[] = $tag;
|
||||
}
|
||||
}
|
||||
if ($rawjson === '') {
|
||||
throw new Exception('The sites.json file is empty');
|
||||
}
|
||||
// Do some checks to see if the JSON decodes into something
|
||||
// like what we expect to see...
|
||||
$decodedjson = json_decode($rawjson);
|
||||
if (is_array($decodedjson)) {
|
||||
$sites = $decodedjson;
|
||||
}
|
||||
if (isset($decodedjson->sites) && is_array($decodedjson->sites)) {
|
||||
$sites = $decodedjson->sites;
|
||||
$this->default = (array) $decodedjson->default;
|
||||
}
|
||||
// Walk over the sites array and instantiate an actual Site() object
|
||||
// for each element.
|
||||
array_walk($sites, function(&$item, $key, $default) {
|
||||
$item = new Site($this->config, (array) $item, $default);
|
||||
}, $this->default);
|
||||
// Return the array of Site() objects, note we are in a callback
|
||||
// so the return is not from the outer function.
|
||||
return $sites;
|
||||
return array_values(array_unique($uniquetags));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the loaded sites.
|
||||
* Try to load the list of sites from sites.json.
|
||||
*
|
||||
* @return array of sites loaded from sites.json
|
||||
* Throws an exception if the file cannot be loaded, is empty, or cannot
|
||||
* be decoded to an array,
|
||||
*
|
||||
* @return array Array of Site objects sites loaded from sites.json
|
||||
* @throws Exception If sites.json cannot be found.
|
||||
*/
|
||||
private function load_sites_from_json(): array {
|
||||
$allsites = [];
|
||||
$rawjson = file_get_contents($this->sitesfilelocation);
|
||||
if ($rawjson === false) {
|
||||
throw new Exception('There was a problem loading the sites.json file');
|
||||
}
|
||||
if ($rawjson === '') {
|
||||
throw new Exception('The sites.json file is empty');
|
||||
}
|
||||
// Do some checks to see if the JSON decodes into something
|
||||
// like what we expect to see...
|
||||
$decodedjson = json_decode($rawjson);
|
||||
// First we'll assume maybe the old format for sites.json.
|
||||
if (is_array($decodedjson)) {
|
||||
$allsites = $decodedjson;
|
||||
}
|
||||
// Now check for the newer format.
|
||||
if (isset($decodedjson->sites) && is_array($decodedjson->sites)) {
|
||||
$allsites = $decodedjson->sites;
|
||||
$this->default = (array) $decodedjson->default;
|
||||
}
|
||||
|
||||
// Instantiate an actual Site() object for each element.
|
||||
foreach ($allsites as $key => $item) {
|
||||
$allsites[$key] = new Site($this->config, (array) $item, $this->default);
|
||||
}
|
||||
|
||||
// Return the array of Site() objects, note we are in a callback
|
||||
// so the return is not from the outer function.
|
||||
return $allsites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of all loaded Site objects.
|
||||
*
|
||||
* @return array Array of all loaded Site objects.
|
||||
*/
|
||||
public function get_sites(): array {
|
||||
return $this->loadedsites;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of tags sorted alphabetically, minus the home tag.
|
||||
*
|
||||
* @return array Array of tag names.
|
||||
*/
|
||||
public function get_tags_for_template(): array {
|
||||
$template_tags = [];
|
||||
foreach ($this->tags as $tag) {
|
||||
if ($tag === 'home') {
|
||||
continue;
|
||||
}
|
||||
$template_tags[] = $tag;
|
||||
}
|
||||
sort($template_tags);
|
||||
return $template_tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a URL, does that site exist in our list of sites?
|
||||
*
|
||||
* @param string $url The URL to search for.
|
||||
* @return Site
|
||||
* @return Site A matching Site object if found.
|
||||
* @throws Exception If a site with given URL does not exist.
|
||||
*/
|
||||
public function get_site_by_url(string $url): Site {
|
||||
$found = array_search($url, array_column($this->get_sites(), 'url'));
|
||||
if (!$found) {
|
||||
if ($found === false) {
|
||||
throw new Exception('The site could not be found ('.$url.')');
|
||||
}
|
||||
return $this->loadedsites[$found];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of Site objects with a given tag.
|
||||
*
|
||||
* @param string $tagname The tag to look look up sites.
|
||||
* @return array Array of Site objects with the given tag.
|
||||
* @throws Exception If there are no sites tagged with $tagname.
|
||||
*/
|
||||
public function get_sites_by_tag(string $tagname): array {
|
||||
if (!in_array($tagname, $this->tags)) {
|
||||
throw new TagNotFoundException('No sites have been tagged with "'.$tagname.'"');
|
||||
}
|
||||
$found = [];
|
||||
foreach ($this->get_sites() as $site) {
|
||||
if (in_array($tagname, $site->tags)) {
|
||||
$found[] = $site;
|
||||
}
|
||||
}
|
||||
return $found;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
"mustache/mustache": "~2.5",
|
||||
"arthurhoaro/favicon": "~1.0",
|
||||
"nette/caching": "^3.1",
|
||||
"nette/routing": "^3.0.2",
|
||||
"phlak/config": "^7.0"
|
||||
}
|
||||
}
|
||||
|
||||
134
jumpapp/composer.lock
generated
134
jumpapp/composer.lock
generated
@@ -4,7 +4,7 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "972ae27bc31bfd2e428f6cba56a0dc37",
|
||||
"content-hash": "097843a2f00f12e9786893c07a3ae8e3",
|
||||
"packages": [
|
||||
{
|
||||
"name": "arthurhoaro/favicon",
|
||||
@@ -235,6 +235,138 @@
|
||||
],
|
||||
"time": "2021-12-12T17:43:24+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nette/http",
|
||||
"version": "v3.1.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/http.git",
|
||||
"reference": "8146c2f2a262691a7139f9c56007961dcc5c1f42"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/http/zipball/8146c2f2a262691a7139f9c56007961dcc5c1f42",
|
||||
"reference": "8146c2f2a262691a7139f9c56007961dcc5c1f42",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nette/utils": "^3.1",
|
||||
"php": ">=7.2 <8.2"
|
||||
},
|
||||
"conflict": {
|
||||
"nette/di": "<3.0.3",
|
||||
"nette/schema": "<1.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/di": "^3.0",
|
||||
"nette/security": "^3.0",
|
||||
"nette/tester": "^2.0",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"tracy/tracy": "^2.4"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-fileinfo": "to detect type of uploaded files"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.1-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause",
|
||||
"GPL-2.0-only",
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"description": "🌐 Nette Http: abstraction for HTTP request, response and session. Provides careful data sanitization and utility for URL and cookies manipulation.",
|
||||
"homepage": "https://nette.org",
|
||||
"keywords": [
|
||||
"cookies",
|
||||
"http",
|
||||
"nette",
|
||||
"proxy",
|
||||
"request",
|
||||
"response",
|
||||
"security",
|
||||
"session",
|
||||
"url"
|
||||
],
|
||||
"time": "2021-11-29T18:56:42+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nette/routing",
|
||||
"version": "v3.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/routing.git",
|
||||
"reference": "5532e7e3612e13def357f089c1a5c25793a16843"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/routing/zipball/5532e7e3612e13def357f089c1a5c25793a16843",
|
||||
"reference": "5532e7e3612e13def357f089c1a5c25793a16843",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"nette/http": "^3.0",
|
||||
"nette/utils": "^3.0",
|
||||
"php": ">=7.1"
|
||||
},
|
||||
"require-dev": {
|
||||
"nette/tester": "^2.0",
|
||||
"phpstan/phpstan": "^0.12",
|
||||
"tracy/tracy": "^2.3"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.0-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"BSD-3-Clause",
|
||||
"GPL-2.0-only",
|
||||
"GPL-3.0-only"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "David Grudl",
|
||||
"homepage": "https://davidgrudl.com"
|
||||
},
|
||||
{
|
||||
"name": "Nette Community",
|
||||
"homepage": "https://nette.org/contributors"
|
||||
}
|
||||
],
|
||||
"description": "Nette Routing: two-ways URL conversion",
|
||||
"homepage": "https://nette.org",
|
||||
"keywords": [
|
||||
"nette"
|
||||
],
|
||||
"time": "2021-02-06T04:08:30+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nette/utils",
|
||||
"version": "v3.2.7",
|
||||
|
||||
@@ -8,21 +8,30 @@
|
||||
|
||||
return [
|
||||
// The site name is displayed in the browser tab.
|
||||
'sitename' => getenv('SITENAME') ?: 'Jump',
|
||||
// Should the clock be displayed?
|
||||
'showclock' => getenv('SHOWCLOCK') ?: true,
|
||||
// Temperature unit: True = metric / False = imperial.
|
||||
'metrictemp' => getenv('METRICTEMP') ?: true,
|
||||
'sitename' => getenv('SITENAME') ?: 'Jump',
|
||||
// Where on the this code is located.
|
||||
'wwwroot' => getenv('WWWROOT') ?: '/var/www/html',
|
||||
'wwwroot' => getenv('WWWROOT') ?: '/var/www/html',
|
||||
|
||||
// Stop retrieving items from the cache, useful for testing.
|
||||
'cachebypass' => getenv('CACHEBYPASS') ?: false,
|
||||
'cachebypass' => getenv('CACHEBYPASS') ?: false,
|
||||
// Where is the cache storage directory, should not be public.
|
||||
'cachedir' => getenv('CACHEDIR') ?: '/var/www/cache',
|
||||
'cachedir' => getenv('CACHEDIR') ?: '/var/www/cache',
|
||||
|
||||
// Include the robots noindex meta tag in site header.
|
||||
'noindex' => getenv('NOINDEX') ?: true,
|
||||
// Coordinates for weather location. E.g. 51.509865,-0.118092
|
||||
'latlong' => getenv('LATLONG') ?: '',
|
||||
'noindex' => getenv('NOINDEX') ?: true,
|
||||
// Should the clock be displayed?
|
||||
'showclock' => getenv('SHOWCLOCK') ?: true,
|
||||
// Show a friendly greeting message rather than "#home".
|
||||
'showgreeting' => getenv('SHOWGREETING') ?: true,
|
||||
// Background blur percentage.
|
||||
'bgblur' => getenv('BGBLUR') ?: '70',
|
||||
// Background brightness percentage.
|
||||
'bgbright' => getenv('BGBRIGHT') ?: '85',
|
||||
|
||||
// Open Weather Map API key.
|
||||
'owmapikey' => getenv('OWMAPIKEY') ?: '',
|
||||
'owmapikey' => getenv('OWMAPIKEY') ?: '',
|
||||
// Coordinates for weather location. E.g. 51.509865,-0.118092
|
||||
'latlong' => getenv('LATLONG') ?: '',
|
||||
// Temperature unit: True = metric / False = imperial.
|
||||
'metrictemp' => getenv('METRICTEMP') ?: true,
|
||||
];
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* Initialise the application and generate index page content.
|
||||
* Initialise the application, generate and output page content.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
@@ -9,7 +9,5 @@
|
||||
// Provided by composer for psr-4 style autoloading.
|
||||
require __DIR__ .'/vendor/autoload.php';
|
||||
|
||||
// Initialise the application, then render and output its index page.
|
||||
$jumpapp = new Jump\Main();
|
||||
$jumpapp->build_index_page();
|
||||
echo $jumpapp->get_output();
|
||||
echo $jumpapp->init();
|
||||
|
||||
@@ -4,25 +4,37 @@
|
||||
"icon": "my-default-icon.png"
|
||||
},
|
||||
"sites": [
|
||||
{
|
||||
"name": "Github",
|
||||
"url" : "https://github.com/daledavies/jump"
|
||||
},
|
||||
{
|
||||
"name": "Docker Hub",
|
||||
"url" : "https://hub.docker.com/r/daledavies/jump"
|
||||
},
|
||||
{
|
||||
"name": "Bitwarden",
|
||||
"url" : "https://bitwarden.example.com",
|
||||
"icon": "bitwarden.png"
|
||||
"icon": "bitwarden.png",
|
||||
"tags": ["stuff"]
|
||||
},
|
||||
{
|
||||
"name": "Gitea",
|
||||
"url" : "https://git.example.com",
|
||||
"icon": "gitea.png"
|
||||
"icon": "gitea.png",
|
||||
"tags": ["stuff"]
|
||||
},
|
||||
{
|
||||
"name": "Nextcloud",
|
||||
"url" : "https://cloud.example.com",
|
||||
"icon": "nextcloud.png"
|
||||
"icon": "nextcloud.png",
|
||||
"tags": ["home", "stuff", "things"]
|
||||
},
|
||||
{
|
||||
"name": "Paperless",
|
||||
"url" : "https://paperless.example.com",
|
||||
"icon": "paperless.jpg"
|
||||
"icon": "paperless.jpg",
|
||||
"tags": ["things", "home"]
|
||||
},
|
||||
{
|
||||
"name": "Google",
|
||||
|
||||
18
jumpapp/templates/errorpage.mustache
Normal file
18
jumpapp/templates/errorpage.mustache
Normal file
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
<link rel="stylesheet" href="/background-css.php">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<title>{{message}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="content fixed">
|
||||
<div class="greeting">{{code}}</div>
|
||||
{{message}}
|
||||
</div>
|
||||
<div class="background fixed"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,14 +1,28 @@
|
||||
</div>
|
||||
<a href="https://openweathermap.org/" class="time-weather hidden">
|
||||
{{# showclock}}<span class="time"></span>{{/ showclock}}
|
||||
<span class="weather-info">
|
||||
<span class="desc"></span>
|
||||
<span class="temp"></span>
|
||||
</span>
|
||||
<i class="weather wi"></i>
|
||||
</a>
|
||||
<span class="useclientlocation"></span>
|
||||
<span class="time-weather hidden">
|
||||
<a class="weather widget clickable" href="https://openweathermap.org/">
|
||||
<span class="weather-info">
|
||||
<span class="desc"></span>
|
||||
<span class="temp"></span>
|
||||
</span>
|
||||
<i class="weather-icon wi"></i>
|
||||
</a>
|
||||
{{# showclock}}<span class="time widget"></span>{{/ showclock}}
|
||||
</span>
|
||||
<span class="useclientlocation widget clickable"></span>
|
||||
<div class="header-bar">
|
||||
{{# hastags }}<a href="#tags" class="show-tags"></a>{{/ hastags }}
|
||||
</div>
|
||||
{{# hastags}}
|
||||
<div id="tags" class="tags">
|
||||
<span class="header">Tags<span class="close"></span></span>
|
||||
<ul>
|
||||
<li><a href="/">home</a></li>
|
||||
{{# tags}}<li><a href="/tag/{{.}}/">{{.}}</a></li>{{/ tags}}
|
||||
</ul>
|
||||
</div>
|
||||
{{/ hastags}}
|
||||
<div class="background fixed"></div>
|
||||
<script src="/assets/js/index.bundle.js"></script>
|
||||
<script defer src="/assets/js/index.bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -8,13 +8,12 @@
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Quicksand:wght@400&display=swap" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/assets/css/styles.css">
|
||||
<link rel="stylesheet" href="background-css.php">
|
||||
<link rel="stylesheet" href="/background-css.php">
|
||||
<link rel="stylesheet" href="/assets/css/weather-icons.min.css">
|
||||
<link rel="icon" type="image/png" href="/favicon.png">
|
||||
<title>{{sitename}}</title>
|
||||
<title>{{title}}</title>
|
||||
<script>
|
||||
const JUMP = {
|
||||
latlong: '{{latlong}}',
|
||||
owmapikey: '{{owmapikey}}',
|
||||
metrictemp: '{{metrictemp}}'
|
||||
};
|
||||
@@ -22,4 +21,7 @@
|
||||
</head>
|
||||
<body>
|
||||
<div class="content fixed hidden">
|
||||
<div class="greeting">Good <span class="chosen"></span></div>
|
||||
<div class="greeting">
|
||||
{{# greeting}}<span class="tagname"><span>#</span>{{greeting}}</span>{{/ greeting}}
|
||||
{{^ greeting}}Good <span class="chosen"></span>{{/ greeting}}
|
||||
</div>
|
||||
@@ -3,7 +3,7 @@
|
||||
{{# sites}}
|
||||
<li><a {{# nofollow}}rel="nofollow"{{/ nofollow}} title="{{name}}" href="{{url}}">
|
||||
<span class="icon">
|
||||
<img src="{{icon}}">
|
||||
<img src="/api/icon.php?siteurl={{url}}">
|
||||
</span>
|
||||
<span class="name">{{name}}</span>
|
||||
</a></li>
|
||||
|
||||
7
package-lock.json
generated
7
package-lock.json
generated
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jump",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@@ -389,6 +389,11 @@
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"dev": true
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="
|
||||
},
|
||||
"events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
|
||||
@@ -9,5 +9,8 @@
|
||||
"devDependencies": {
|
||||
"webpack": "^5.68.0",
|
||||
"webpack-cli": "^4.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.7"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user