Added tagging, such refactor

This commit is contained in:
Dale Davies
2022-03-15 21:38:39 +00:00
parent a8386d1955
commit 6386d0a6c7
41 changed files with 1041 additions and 309 deletions

View File

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

View File

@@ -30,6 +30,7 @@ RUN apk add --no-cache \
curl \
nginx \
php8 \
php8-curl \
php8-dom \
php8-fileinfo \
php8-fpm \

View File

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

View File

@@ -1,2 +1,2 @@
[Date]
date.timezone="UTC"
date.timezone="UTC"
expose_php = Off

24
jumpapp/api/icon.php Normal file
View 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;

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

View File

@@ -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;
}

View 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

View 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

View File

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

View 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

View 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

View 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
*/

View File

@@ -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);
}
}

View File

@@ -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',

View File

@@ -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);
}
}

View File

@@ -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) + '&deg;' + (this.metrictemp ? 'C' : 'F'),
temp: Math.ceil(data.main.temp) + '&deg;' + (JUMP.metrictemp ? 'C' : 'F'),
description: data.weather[0].main,
iconclass: 'wi-owm-' + daynightvariant + '-' + data.weather[0].id,
timezoneshift: data.timezone
};
});
})
return promise;
}
}

View File

@@ -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);}';

View File

@@ -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();

View File

@@ -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();

View File

@@ -0,0 +1,5 @@
<?php
namespace Jump\Exceptions;
class TagNotFoundException extends \Exception {}

View File

@@ -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();
}
}

View 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);
}
}

View 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);
}
}

View 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'))
]);
});
}
}

View 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'))
]);
});
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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
View File

@@ -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",

View File

@@ -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,
];

View File

@@ -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();

View File

@@ -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",

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

View File

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

View File

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

View File

@@ -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
View File

@@ -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",

View File

@@ -9,5 +9,8 @@
"devDependencies": {
"webpack": "^5.68.0",
"webpack-cli": "^4.9.2"
},
"dependencies": {
"eventemitter3": "^4.0.7"
}
}