mirror of
https://github.com/daledavies/jump.git
synced 2026-01-10 09:22:06 +01:00
Add search functionality
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
font-weight: 400;
|
||||
text-transform: capitalize;
|
||||
text-shadow: 1px 1px 2px #000000a0;
|
||||
margin-top: -50px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
.tagname {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
right: 0;
|
||||
left: 0;
|
||||
padding: 15px 15px 0 15px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
z-index: 100;
|
||||
|
||||
|
||||
179
jumpapp/assets/css/src/_search.scss
Normal file
179
jumpapp/assets/css/src/_search.scss
Normal file
@@ -0,0 +1,179 @@
|
||||
.search {
|
||||
position:absolute;
|
||||
top:15px;
|
||||
height: 55px;
|
||||
width: 55px;
|
||||
display: block;
|
||||
background-position: top 13px left 13px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../images/search.svg);
|
||||
background-size: 24px;
|
||||
background-color: #ffffff15;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
border: 2px solid #ffffff20;
|
||||
overflow: hidden;
|
||||
padding:0;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
background-image: url(../images/search-dark.svg);
|
||||
border: 2px solid #cecece;
|
||||
transition: background-color, background-image .1s;
|
||||
}
|
||||
|
||||
&.open {
|
||||
@extend :hover;
|
||||
width: calc(100% - 30px);
|
||||
max-width: 450px;
|
||||
cursor: auto;
|
||||
border-radius: 5px;
|
||||
border: .2em solid #cecece;
|
||||
box-shadow: 0 1px 5px rgba(0,0,0,.3);
|
||||
|
||||
.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%;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: #f3f3f3;
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
&.suggestions {
|
||||
height: auto;
|
||||
|
||||
.suggestion-list {
|
||||
display: block;
|
||||
padding-top: 20px;
|
||||
margin-top: 25px;
|
||||
padding-bottom:10px;
|
||||
border-top: 1px solid #ddd;
|
||||
text-align: left;
|
||||
color: #202124;
|
||||
font-size: 15px;
|
||||
|
||||
.selected {
|
||||
border: 1px solid red;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding: 10px 0 0 0;
|
||||
margin: 0;
|
||||
list-style-type: none;
|
||||
|
||||
li a {
|
||||
display: block;
|
||||
padding: 5px 0 5px 15px;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
|
||||
&:focus,
|
||||
&:hover {
|
||||
outline: none;
|
||||
background-color: #eee;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.searchproviders {
|
||||
margin-bottom: 20px;
|
||||
|
||||
li a {
|
||||
display: block;
|
||||
background-position: top 50% left 15px;
|
||||
background-repeat: no-repeat;
|
||||
background-image: url(../images/search-dark.svg);
|
||||
background-size: 17px;
|
||||
padding-left: 50px;
|
||||
|
||||
span {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.suggestiontitle {
|
||||
margin: 0 0 0 15px;
|
||||
display: block;
|
||||
color: #888;
|
||||
font-size: 15px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
ul.suggestions {
|
||||
padding-bottom: 15px;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 20px;
|
||||
vertical-align: middle;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.name {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: none;
|
||||
position: relative;
|
||||
top: 14px;
|
||||
overflow: hidden;
|
||||
width: calc(100% - 35px);
|
||||
text-align: left;
|
||||
color: #202124;
|
||||
padding-right: 48px;
|
||||
|
||||
input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: #202124;
|
||||
font-size: 17px;
|
||||
font-family: 'Quicksand', sans-serif;
|
||||
padding: 0 0 0 15px;
|
||||
margin: 0;
|
||||
outline: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
// Remove the 'x' icon from Internet Explorer
|
||||
input[type=search]::-ms-clear,
|
||||
input[type=search]::-ms-reveal {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
// Remove the 'x' icon from chrome
|
||||
input[type="search"]::-webkit-search-decoration,
|
||||
input[type="search"]::-webkit-search-cancel-button,
|
||||
input[type="search"]::-webkit-search-results-button,
|
||||
input[type="search"]::-webkit-search-results-decoration {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
@use 'footer-bar';
|
||||
@use 'greeting';
|
||||
@use 'header-bar';
|
||||
@use 'search';
|
||||
@use 'sites';
|
||||
@use 'tags';
|
||||
|
||||
@@ -71,5 +72,5 @@ body {
|
||||
justify-content:center;
|
||||
width: 100%;
|
||||
max-width: 650px;
|
||||
margin:0 auto;
|
||||
margin:-50px auto 0 auto;
|
||||
}
|
||||
|
||||
1
jumpapp/assets/css/styles.2596a987cbdacf25a231.min.css
vendored
Normal file
1
jumpapp/assets/css/styles.2596a987cbdacf25a231.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
5
jumpapp/assets/images/search-dark.svg
Normal file
5
jumpapp/assets/images/search-dark.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#222222" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="10" cy="10" r="7" />
|
||||
<line x1="21" y1="21" x2="15" y2="15" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
5
jumpapp/assets/images/search.svg
Normal file
5
jumpapp/assets/images/search.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-search" width="44" height="44" viewBox="0 0 24 24" stroke-width="2" stroke="#ffffff" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
|
||||
<circle cx="10" cy="10" r="7" />
|
||||
<line x1="21" y1="21" x2="15" y2="15" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
1
jumpapp/assets/js/index.1fd54d01d088096b9a90.min.js
vendored
Normal file
1
jumpapp/assets/js/index.1fd54d01d088096b9a90.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,6 +1,8 @@
|
||||
import Clock from './Clock';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import Fuse from 'fuse.js';
|
||||
import Greeting from './Greeting';
|
||||
import SearchSuggestions from './SearchSuggestions';
|
||||
import Weather from './Weather';
|
||||
|
||||
export default class Main {
|
||||
@@ -23,7 +25,8 @@ export default class Main {
|
||||
this.clientlocationelm = document.querySelector('.useclientlocation');
|
||||
this.showtagsbuttonelm = document.querySelector('.show-tags');
|
||||
this.tagselectorelm = document.querySelector('.tags');
|
||||
this.tagsselectorclosebuttonelm = document.querySelector('.tags .close')
|
||||
this.tagsselectorclosebuttonelm = document.querySelector('.tags .close');
|
||||
this.showsearchbuttonelm = document.querySelector('.search');
|
||||
// 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);
|
||||
@@ -32,6 +35,14 @@ export default class Main {
|
||||
this.eventemitter = new EventEmitter();
|
||||
this.clock = new Clock(this.eventemitter, !!JUMP.ampmclock, !JUMP.owmapikey);
|
||||
this.weather = new Weather(this.eventemitter);
|
||||
|
||||
if (this.showsearchbuttonelm) {
|
||||
this.searchclosebuttonelm = this.showsearchbuttonelm.querySelector('.close');
|
||||
this.fuse = new Fuse(JSON.parse(JUMP.search), {
|
||||
threshold: 0.3,
|
||||
keys: ['name', 'tags']
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,6 +126,76 @@ export default class Main {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.showsearchbuttonelm) {
|
||||
const searchinput = document.querySelector('.search-form input');
|
||||
this.searchsuggestions = new SearchSuggestions(JSON.parse(JUMP.searchengines), searchinput, this.showsearchbuttonelm, this.eventemitter);
|
||||
|
||||
// When the search icon is licked, show the search bar and focus on it.
|
||||
this.showsearchbuttonelm.addEventListener('click', e => {
|
||||
if (!e.target.classList.contains('open')) {
|
||||
this.showsearchbuttonelm.classList.add('open');
|
||||
searchinput.focus();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for CTRL+/ key combo and open search bar.
|
||||
document.addEventListener('keyup', e => {
|
||||
if (e.ctrlKey && (e.code == 'Slash')) {
|
||||
if (!this.showsearchbuttonelm.classList.contains('open')) {
|
||||
this.showsearchbuttonelm.classList.add('open');
|
||||
searchinput.focus();
|
||||
} else {
|
||||
this.search_close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle the close button.
|
||||
this.searchclosebuttonelm.addEventListener('click', e => {
|
||||
e.stopPropagation();
|
||||
this.search_close();
|
||||
});
|
||||
|
||||
// Listen for key events triggered by the searh bar and do stuff.
|
||||
searchinput.addEventListener('keyup', e => {
|
||||
// On arrow down, focus on the first search suggestion.
|
||||
let suggestionslist = document.querySelector('.suggestion-list .searchproviders');
|
||||
if (e.code === 'ArrowDown') {
|
||||
if (suggestionslist && suggestionslist.childNodes.length) {
|
||||
suggestionslist.firstChild.firstChild.focus();
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Perform search and display suggestions on the page.
|
||||
let results = [];
|
||||
let siteresults = this.fuse.search(searchinput.value);
|
||||
if (siteresults.length > 0) {
|
||||
siteresults.forEach((result) => {
|
||||
results.push(result.item);
|
||||
});
|
||||
}
|
||||
this.searchsuggestions.replace(results);
|
||||
});
|
||||
|
||||
// If someone presses enter then open up the first link, this is the default seach engine
|
||||
// purely because it is at the top of the list.
|
||||
document.querySelector('.search-form').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
if (searchinput.value != '') {
|
||||
document.querySelector('.searchproviders li a').click();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
search_close() {
|
||||
let suggestions = this.showsearchbuttonelm.querySelector('.suggestionholder');
|
||||
if (suggestions) {
|
||||
suggestions.remove();
|
||||
}
|
||||
this.showsearchbuttonelm.classList.remove('suggestions');
|
||||
document.querySelector('.search').classList.remove('open');
|
||||
document.querySelector('.search-form input').value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
122
jumpapp/assets/js/src/classes/SearchSuggestions.js
Normal file
122
jumpapp/assets/js/src/classes/SearchSuggestions.js
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Generate search suggestions.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
export default class SearchSuggestions {
|
||||
|
||||
constructor(searchengines, inputelm, containerelm, eventemitter) {
|
||||
this.containerelm = containerelm;
|
||||
this.eventemitter = eventemitter;
|
||||
this.inputelm = inputelm;
|
||||
this.suggestionslistelm = containerelm.querySelector('.suggestion-list');
|
||||
this.searchproviderlist = null;
|
||||
this.searchengines = searchengines;
|
||||
}
|
||||
|
||||
build_searchprovider_list_elm(query) {
|
||||
const searchproviderlist = document.createElement('ul');
|
||||
searchproviderlist.classList.add('searchproviders');
|
||||
searchproviderlist.setAttribute('tabindex', -1);
|
||||
this.searchengines.forEach((provider) => {
|
||||
const searchprovider = document.createElement('li');
|
||||
searchprovider.setAttribute('tabindex', -1);
|
||||
searchprovider.innerHTML = '<a target="_blank" rel="noopener" \
|
||||
href="'+provider.url+encodeURIComponent(query)+'"><span>Search on</span> '+provider.name+'</a>';
|
||||
searchproviderlist.appendChild(searchprovider);
|
||||
});
|
||||
searchproviderlist.addEventListener('keyup', e => {
|
||||
switch (e.code) {
|
||||
case 'ArrowUp':
|
||||
if (document.activeElement == e.target.parentNode.parentNode.firstChild.firstChild) {
|
||||
this.inputelm.focus();
|
||||
break;
|
||||
}
|
||||
document.activeElement.parentNode.previousSibling.firstChild.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (document.activeElement == e.target.parentNode.parentNode.lastChild.firstChild) {
|
||||
const suggestionselm = document.querySelector('.suggestionholder .suggestions');
|
||||
if (suggestionselm) {
|
||||
suggestionselm.firstChild.firstChild.focus();
|
||||
} else {
|
||||
e.target.parentNode.parentNode.firstChild.firstChild.focus();
|
||||
}
|
||||
break;
|
||||
}
|
||||
document.activeElement.parentNode.nextSibling.firstChild.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return searchproviderlist;
|
||||
}
|
||||
|
||||
build_suggestion_list_elm(siteresults) {
|
||||
const suggestionslist = document.createElement('ul');
|
||||
suggestionslist.classList.add('suggestions');
|
||||
suggestionslist.setAttribute('tabindex', -1);
|
||||
siteresults.forEach((result) => {
|
||||
const resultitem = document.createElement('li');
|
||||
resultitem.setAttribute('tabindex', -1);
|
||||
resultitem.innerHTML = '<a target="_blank" rel="noopener" href="'+result.url+'">\
|
||||
<img class="icon" src="'+result.iconurl+'"><span class="name">'+result.name+'</span>';
|
||||
suggestionslist.appendChild(resultitem);
|
||||
});
|
||||
suggestionslist.addEventListener('keyup', e => {
|
||||
switch (e.code) {
|
||||
case 'ArrowUp':
|
||||
if (document.activeElement == e.target.parentNode.parentNode.firstChild.firstChild) {
|
||||
this.searchproviderlist.lastChild.firstChild.focus();
|
||||
break;
|
||||
}
|
||||
document.activeElement.parentNode.previousSibling.firstChild.focus();
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
if (document.activeElement == e.target.parentNode.parentNode.lastChild.firstChild) {
|
||||
this.searchproviderlist.firstChild.firstChild.focus();
|
||||
break;
|
||||
}
|
||||
document.activeElement.parentNode.nextSibling.firstChild.focus();
|
||||
break;
|
||||
}
|
||||
});
|
||||
return suggestionslist;
|
||||
}
|
||||
|
||||
replace(siteresults) {
|
||||
const newsuggestionslist = this.build_suggestion_list_elm(siteresults);
|
||||
|
||||
const suggestionholder = document.createElement('span');
|
||||
suggestionholder.classList.add('suggestionholder');
|
||||
|
||||
if (this.inputelm.value !== '') {
|
||||
const searchtitle = document.createElement('span');
|
||||
searchtitle.classList.add('suggestiontitle');
|
||||
searchtitle.innerHTML = 'Search';
|
||||
suggestionholder.appendChild(searchtitle);
|
||||
this.searchproviderlist = this.build_searchprovider_list_elm(this.inputelm.value);
|
||||
suggestionholder.appendChild(this.searchproviderlist);
|
||||
}
|
||||
|
||||
if (newsuggestionslist.childNodes.length > 0) {
|
||||
const suggestiontitle = document.createElement('span');
|
||||
suggestiontitle.classList.add('suggestiontitle');
|
||||
suggestiontitle.innerHTML = 'Sites';
|
||||
suggestionholder.appendChild(suggestiontitle);
|
||||
suggestionholder.appendChild(newsuggestionslist)
|
||||
}
|
||||
|
||||
if (suggestionholder.childNodes.length > 0) {
|
||||
this.containerelm.classList.add('suggestions');
|
||||
this.suggestionslistelm.replaceChildren(suggestionholder);
|
||||
} else {
|
||||
this.containerelm.classList.remove('suggestions');
|
||||
let suggestions = this.containerelm.querySelector('.suggestionholder');
|
||||
if (suggestions) {
|
||||
suggestions.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,11 @@ class Cache {
|
||||
public function __construct(private Config $config) {
|
||||
// Define the various caches used throughout the app.
|
||||
$this->caches = [
|
||||
'searchengines' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
'expirationparams' => $config->get('searchenginesfile')
|
||||
],
|
||||
'sites' => [
|
||||
'cache' => null,
|
||||
'expirationtype' => Caching\Cache::FILES,
|
||||
|
||||
@@ -24,6 +24,7 @@ class Config {
|
||||
private const BASE_APPLICATION_PATHS = [
|
||||
'backgroundsdir' => '/assets/backgrounds',
|
||||
'defaulticonpath' => '/assets/images/default-icon.png',
|
||||
'searchenginesfile' => '/search/searchengines.json',
|
||||
'sitesdir' => '/sites',
|
||||
'sitesfile' => '/sites/sites.json',
|
||||
'templatedir' => '/templates',
|
||||
|
||||
@@ -11,7 +11,7 @@ class HomePage extends AbstractPage {
|
||||
$greeting = 'home';
|
||||
}
|
||||
$csrfsection = $this->session->getSection('csrf');
|
||||
return $template->render([
|
||||
$templatecontext = [
|
||||
'csrftoken' => $csrfsection->get('token'),
|
||||
'greeting' => $greeting,
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
@@ -19,7 +19,13 @@ class HomePage extends AbstractPage {
|
||||
'owmapikey' => !!$this->config->get('owmapikey', false),
|
||||
'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp')),
|
||||
'ampmclock' => $this->config->parse_bool($this->config->get('ampmclock', false)),
|
||||
]);
|
||||
];
|
||||
if ($this->config->parse_bool($this->config->get('showsearch', false))) {
|
||||
$templatecontext = array_merge($templatecontext,
|
||||
['searchengines' => json_encode((new \Jump\SearchEngines($this->config, $this->cache))->get_search_engines()),
|
||||
'searchjson' => json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_search()),]);
|
||||
}
|
||||
return $template->render($templatecontext);
|
||||
}
|
||||
|
||||
protected function render_content(): string {
|
||||
@@ -42,7 +48,8 @@ class HomePage extends AbstractPage {
|
||||
return $template->render([
|
||||
'hastags' => !empty($tags),
|
||||
'tags' => $tags,
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock')),
|
||||
'showsearch' => $this->config->parse_bool($this->config->get('showsearch', false)),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,14 +11,20 @@ class TagPage extends AbstractPage {
|
||||
$greeting = $this->param;
|
||||
$title = 'Tag: '.$this->param;
|
||||
$csrfsection = $this->session->getSection('csrf');
|
||||
return $template->render([
|
||||
$templatecontext = [
|
||||
'csrftoken' => $csrfsection->get('token'),
|
||||
'greeting' => $greeting,
|
||||
'noindex' => $this->config->parse_bool($this->config->get('noindex')),
|
||||
'title' => $title,
|
||||
'owmapikey' => !!$this->config->get('owmapikey', false),
|
||||
'metrictemp' => $this->config->parse_bool($this->config->get('metrictemp')),
|
||||
]);
|
||||
];
|
||||
if ($this->config->parse_bool($this->config->get('showsearch', false))) {
|
||||
$templatecontext = array_merge($templatecontext,
|
||||
['searchengines' => json_encode((new \Jump\SearchEngines($this->config, $this->cache))->get_search_engines()),
|
||||
'searchjson' => json_encode((new \Jump\Sites($this->config, $this->cache))->get_sites_for_search()),]);
|
||||
}
|
||||
return $template->render($templatecontext);
|
||||
}
|
||||
|
||||
protected function render_content(): string {
|
||||
@@ -48,7 +54,8 @@ class TagPage extends AbstractPage {
|
||||
return $template->render([
|
||||
'hastags' => !empty($tags),
|
||||
'tags' => $tags,
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock'))
|
||||
'showclock' => $this->config->parse_bool($this->config->get('showclock')),
|
||||
'showsearch' => $this->config->parse_bool($this->config->get('showsearch', false)),
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
82
jumpapp/classes/SearchEngines.php
Normal file
82
jumpapp/classes/SearchEngines.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Jump;
|
||||
|
||||
use \Exception;
|
||||
|
||||
/**
|
||||
* Loads and validates the search engines defined in searchengines.json.
|
||||
*
|
||||
* @author Dale Davies <dale@daledavies.co.uk>
|
||||
* @license MIT
|
||||
*/
|
||||
class SearchEngines {
|
||||
private array $default;
|
||||
private string $searchfilelocation;
|
||||
private array $loadedsearchengines;
|
||||
|
||||
/**
|
||||
* Automatically load searchengines.json on instantiation.
|
||||
*/
|
||||
public function __construct(private Config $config, private Cache $cache) {
|
||||
$this->config = $config;
|
||||
$this->loadedsearchengines = [];
|
||||
$this->searchfilelocation = $this->config->get('searchenginesfile');
|
||||
$this->cache = $cache;
|
||||
|
||||
// Retrieve search engines from cache. Load from json file if not cached or
|
||||
// the cache has expired.
|
||||
$this->loadedsearchengines = $this->cache->load(cachename: 'searchengines', callback: function() {
|
||||
return $this->load_search_engines_from_json();
|
||||
});
|
||||
|
||||
}
|
||||
/**
|
||||
* Try to load and validate the list of search engines from searchengines.json.
|
||||
*
|
||||
* Throws an exception if the file cannot be loaded, is empty, or cannot
|
||||
* be decoded to an array.
|
||||
*
|
||||
* @return array AArray of parsed/validated search engine information from searchengines.json
|
||||
* @throws Exception If searchengines.json cannot be found.
|
||||
*/
|
||||
private function load_search_engines_from_json(): array {
|
||||
$searchengines = [];
|
||||
$rawjson = file_get_contents($this->searchfilelocation);
|
||||
if ($rawjson === false) {
|
||||
throw new Exception('There was a problem loading the searchengines.json file');
|
||||
}
|
||||
if ($rawjson === '') {
|
||||
throw new Exception('The searchengines.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)) {
|
||||
throw new Exception('The searchengines.json file is invalid');
|
||||
}
|
||||
|
||||
// Build a new array using the values we need...
|
||||
foreach ($decodedjson as $item) {
|
||||
if (!isset($item->name, $item->url)) {
|
||||
throw new Exception('The searchengines.json does not contain the "name" or "url" properties');
|
||||
}
|
||||
$searchengine = new \stdClass();
|
||||
$searchengine->name = $item->name;
|
||||
$searchengine->url = $item->url;
|
||||
$searchengines[] = $searchengine;
|
||||
}
|
||||
|
||||
return $searchengines;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of loaded search engines.
|
||||
*
|
||||
* @return array Array of parsed/validated search engine information from searchengines.json
|
||||
*/
|
||||
public function get_search_engines() {
|
||||
return $this->loadedsearchengines;
|
||||
}
|
||||
}
|
||||
@@ -156,4 +156,17 @@ class Sites {
|
||||
return $found;
|
||||
}
|
||||
|
||||
public function get_sites_for_search(): array {
|
||||
$searchlist = [];
|
||||
foreach ($this->loadedsites as $loadedsite) {
|
||||
$site = new \stdClass();
|
||||
$site->name = $loadedsite->name;
|
||||
$site->url = $loadedsite->url;
|
||||
$site->tags = $loadedsite->tags;
|
||||
$site->iconurl = '/api/icon.php?siteurl='.urlencode($loadedsite->url);
|
||||
$searchlist[] = $site;
|
||||
}
|
||||
return $searchlist;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ return [
|
||||
// Background brightness percentage.
|
||||
'bgbright' => getenv('BGBRIGHT') ?: '85',
|
||||
// Display alternative layout of sites list.
|
||||
'altlayout' => getenv('ALTLAYOUT') ?: false,
|
||||
'altlayout' => getenv('ALTLAYOUT') ?: false,
|
||||
// Show the search bar, requires /search/searchengines.json etc.
|
||||
'showsearch' => getenv('SHOWSEARCH') ?: true,
|
||||
|
||||
// Open Weather Map API key.
|
||||
'owmapikey' => getenv('OWMAPIKEY') ?: '',
|
||||
|
||||
14
jumpapp/search/searchengines.json
Normal file
14
jumpapp/search/searchengines.json
Normal file
@@ -0,0 +1,14 @@
|
||||
[
|
||||
{
|
||||
"name": "Google",
|
||||
"url": "https://www.google.co.uk/search?q="
|
||||
},
|
||||
{
|
||||
"name": "DuckDuckGo",
|
||||
"url": "https://duckduckgo.com/?q="
|
||||
},
|
||||
{
|
||||
"name": "Bing",
|
||||
"url": "https://www.bing.com/search?q="
|
||||
}
|
||||
]
|
||||
@@ -11,6 +11,15 @@
|
||||
</span>
|
||||
<span class="useclientlocation widget clickable"></span>
|
||||
<div class="header-bar">
|
||||
{{# showsearch}}
|
||||
<span class="search">
|
||||
<span class="close"></span>
|
||||
<form class="search-form">
|
||||
<input type="search">
|
||||
</form>
|
||||
<span class="suggestion-list"></span>
|
||||
</span>
|
||||
{{/ showsearch}}
|
||||
{{# hastags }}<a href="#tags" class="show-tags"></a>{{/ hastags }}
|
||||
</div>
|
||||
{{# hastags}}
|
||||
@@ -23,6 +32,6 @@
|
||||
</div>
|
||||
{{/ hastags}}
|
||||
<div class="background fixed"></div>
|
||||
<script defer src="/assets/js/index.52c1dcb71f05502fb292.min.js"></script>
|
||||
<script defer src="/assets/js/index.1fd54d01d088096b9a90.min.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
{{# noindex}}<meta name="robots" content="noindex">{{/ noindex}}
|
||||
<link rel="stylesheet" href="/assets/css/styles.d5cd9fa42e523236402f.min.css">
|
||||
<link rel="stylesheet" href="/assets/css/styles.2596a987cbdacf25a231.min.css">
|
||||
<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">
|
||||
@@ -14,7 +14,9 @@
|
||||
owmapikey: '{{owmapikey}}',
|
||||
metrictemp: '{{metrictemp}}',
|
||||
ampmclock: '{{ampmclock}}',
|
||||
token: '{{csrftoken}}'
|
||||
token: '{{csrftoken}}',
|
||||
search: '{{{searchjson}}}',
|
||||
searchengines: '{{{searchengines}}}',
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
</span>
|
||||
<span class="useclientlocation widget clickable"></span>
|
||||
<div class="header-bar">
|
||||
{{# showsearch}}
|
||||
<span class="search">
|
||||
<span class="close"></span>
|
||||
<form class="search-form">
|
||||
<input type="search">
|
||||
</form>
|
||||
<span class="suggestion-list"></span>
|
||||
</span>
|
||||
{{/ showsearch}}
|
||||
{{# hastags }}<a href="#tags" class="show-tags"></a>{{/ hastags }}
|
||||
</div>
|
||||
{{# hastags}}
|
||||
@@ -14,7 +14,9 @@
|
||||
owmapikey: '{{owmapikey}}',
|
||||
metrictemp: '{{metrictemp}}',
|
||||
ampmclock: '{{ampmclock}}',
|
||||
token: '{{csrftoken}}'
|
||||
token: '{{csrftoken}}',
|
||||
search: '{{{searchjson}}}',
|
||||
searchengines: '{{{searchengines}}}',
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
3388
package-lock.json
generated
3388
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,7 @@
|
||||
"webpack-remove-empty-scripts": "^0.8.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.7"
|
||||
"eventemitter3": "^4.0.7",
|
||||
"fuse.js": "^6.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,13 +38,13 @@ module.exports = {
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, './jumpapp/templates/header.mustache'),
|
||||
template: path.resolve(__dirname, './jumpapp/templates/src/header.mustache'),
|
||||
template: path.resolve(__dirname, './jumpapp/templates/src/header.src.mustache'),
|
||||
inject: false,
|
||||
minify: false, // Required to prevent addition of closing tags like body and html.
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: path.resolve(__dirname, './jumpapp/templates/footer.mustache'),
|
||||
template: path.resolve(__dirname, './jumpapp/templates/src/footer.mustache'),
|
||||
template: path.resolve(__dirname, './jumpapp/templates/src/footer.src.mustache'),
|
||||
inject: false,
|
||||
minify: false, // Required to prevent addition of closing tags like body and html.
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user