Compare commits
1 commit
main
...
add-search
Author | SHA1 | Date | |
---|---|---|---|
|
0d8b5adf67 |
9 changed files with 798 additions and 2 deletions
2
main.go
2
main.go
|
@ -120,6 +120,8 @@ func main() {
|
|||
renderTemplate(w, "download-linux.html", nil)
|
||||
})
|
||||
|
||||
http.HandleFunc("/suggestions", handleSuggestions)
|
||||
|
||||
// Route for generating the RSS feed for all blogs
|
||||
http.HandleFunc("/rss", func(w http.ResponseWriter, r *http.Request) {
|
||||
siteURL := fmt.Sprintf("http://%s", r.Host) // or you can use a fixed base URL
|
||||
|
|
52
printing.go
Normal file
52
printing.go
Normal file
|
@ -0,0 +1,52 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
var LogLevel = 1
|
||||
|
||||
// printDebug logs debug-level messages when LogLevel is set to 4.
|
||||
func printDebug(format string, args ...interface{}) {
|
||||
if LogLevel >= 4 {
|
||||
logMessage("DEBUG", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// printInfo logs info-level messages when LogLevel is set to 3 or higher.
|
||||
func printInfo(format string, args ...interface{}) {
|
||||
if LogLevel >= 3 {
|
||||
logMessage("INFO", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// printWarn logs warning-level messages when LogLevel is set to 2 or higher.
|
||||
func printWarn(format string, args ...interface{}) {
|
||||
if LogLevel >= 2 {
|
||||
logMessage("WARN", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// printErr logs error-level messages regardless of LogLevel.
|
||||
func printErr(format string, args ...interface{}) {
|
||||
if LogLevel >= 1 {
|
||||
logMessage("ERROR", format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// printMessage logs messages without a specific log level (e.g., general output).
|
||||
func printMessage(format string, args ...interface{}) {
|
||||
logMessage("", format, args...)
|
||||
}
|
||||
|
||||
// logMessage handles the actual logging logic without using the default logger's timestamp.
|
||||
func logMessage(level string, format string, args ...interface{}) {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05")
|
||||
message := fmt.Sprintf(format, args...)
|
||||
if level != "" {
|
||||
fmt.Printf("[%s %s] %s\n", timestamp, level, message)
|
||||
} else {
|
||||
fmt.Printf("[%s] %s\n", timestamp, message)
|
||||
}
|
||||
}
|
2
run.sh
2
run.sh
|
@ -13,4 +13,4 @@ while [[ "$#" -gt 0 ]]; do
|
|||
done
|
||||
|
||||
# Run the Go application with the parsed flags
|
||||
go run discord.go rss.go telegram.go save.go main.go -p=$PORT
|
||||
go run printing.go suggestions.go discord.go rss.go telegram.go save.go main.go -p=$PORT
|
||||
|
|
|
@ -23,4 +23,10 @@
|
|||
width: 48px;
|
||||
height: 48px;
|
||||
overflow: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.reset-styles * {
|
||||
all: unset;
|
||||
display: revert;
|
||||
}
|
||||
|
237
static/css/search.css
Normal file
237
static/css/search.css
Normal file
|
@ -0,0 +1,237 @@
|
|||
/* inter-300 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local(''),
|
||||
url('/static/fonts/inter-v12-latin-300.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/fonts/inter-v12-latin-300.woff') format('woff');
|
||||
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* inter-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('/static/fonts/inter-v12-latin-regular.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/fonts/inter-v12-latin-regular.woff') format('woff');
|
||||
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* inter-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''),
|
||||
url('/static/fonts/inter-v12-latin-700.woff2') format('woff2'),
|
||||
/* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/fonts/inter-v12-latin-700.woff') format('woff');
|
||||
/* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* material-icons-round-regular - latin */
|
||||
@font-face {
|
||||
font-display: swap;
|
||||
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||
font-family: 'Material Icons Round';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/static/webfonts/material-icons-round-v108-latin-regular.woff2') format('woff2');
|
||||
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||
}
|
||||
|
||||
#search-wrapper-ico,
|
||||
#clearSearch {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--fg);
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#search-wrapper-ico:hover,
|
||||
#clearSearch:hover {
|
||||
transition: all .3s ease;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
#sub-search-wrapper-ico {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 17px;
|
||||
/* will be set to 17px if icon pack can be loaded. */
|
||||
padding-right: 0px;
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
text-align: center;
|
||||
margin-top: 10%;
|
||||
}
|
||||
|
||||
.search-container h1 {
|
||||
font-size: 70px;
|
||||
color: var(--font-fg);
|
||||
font-family: 'Inter';
|
||||
}
|
||||
|
||||
.search-container input {
|
||||
width: 90%;
|
||||
color: var(--font-fg);
|
||||
background-color: var(--search-bg-input);
|
||||
font-size: inherit;
|
||||
font-family: sans-serif;
|
||||
border: none;
|
||||
margin-right: 100%;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
.search-container-results-btn {
|
||||
display: flex;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.search-container-results-btn:hover button {
|
||||
transition: all 0.3s ease;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
#search-wrapper-ico,
|
||||
#clearSearch {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--fg);
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-button-wrapper button:hover {
|
||||
border: 1px solid #5f6368;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
.wrapper {
|
||||
margin: 0 auto;
|
||||
background: var(--search-bg-input);
|
||||
border-radius: 22px;
|
||||
position: absolute;
|
||||
width: 520px;
|
||||
overflow: hidden;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 2;
|
||||
border: 1px solid var(--search-bg-input-border);
|
||||
}
|
||||
|
||||
.wrapper input {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.wrapper-results {
|
||||
margin: 0 auto;
|
||||
background: var(--search-bg-input);
|
||||
border-radius: 22px;
|
||||
position: absolute;
|
||||
width: 628px;
|
||||
overflow: hidden;
|
||||
margin-top: 0px;
|
||||
top: 18px;
|
||||
left: 170px;
|
||||
z-index: 2;
|
||||
border: 1px solid var(--search-bg-input-border);
|
||||
}
|
||||
|
||||
.wrapper-results:hover,
|
||||
.wrapper-results:focus-within,
|
||||
.wrapper:hover,
|
||||
.wrapper:focus-within {
|
||||
box-shadow: 0px 6px 12px rgba(0, 0, 0, 0.24);
|
||||
transition: all 0.3s cubic-bezier(.25, .8, .25, 1);
|
||||
}
|
||||
|
||||
|
||||
.autocomplete {
|
||||
padding: 0px;
|
||||
}
|
||||
|
||||
.autocomplete ul {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.autocomplete ul li {
|
||||
list-style: none;
|
||||
opacity: 0;
|
||||
display: none;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.show .autocomplete ul li {
|
||||
opacity: 1;
|
||||
display: block;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.show .autocomplete {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
color: var(--font-fg);
|
||||
}
|
||||
|
||||
.autocomplete ul li:hover {
|
||||
cursor: pointer;
|
||||
background: var(--search-select);
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 750px) {
|
||||
|
||||
.wrapper {
|
||||
width: 86%;
|
||||
position: absolute;
|
||||
float: none;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 0px;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
display: block;
|
||||
margin-top: 0px;
|
||||
top: 110px;
|
||||
left: 4px;
|
||||
}
|
||||
|
||||
.wrapper input {
|
||||
padding: 10px;
|
||||
max-width: 92%;
|
||||
}
|
||||
|
||||
.results-search-container {
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.results-search-container input {
|
||||
width: 84%;
|
||||
margin-right: 100%;
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
#search-wrapper-ico {
|
||||
top: 5px;
|
||||
}
|
||||
|
||||
}
|
315
static/js/autocomplete.js
Normal file
315
static/js/autocomplete.js
Normal file
|
@ -0,0 +1,315 @@
|
|||
/**
|
||||
* @source: ./script.js (originally from araa-search on Github)
|
||||
*
|
||||
* @licstart The following is the entire license notice for the
|
||||
* JavaScript code in this page.
|
||||
*
|
||||
* Copyright (C) 2023 Extravi
|
||||
*
|
||||
* The JavaScript code in this page is free software: you can
|
||||
* redistribute it and/or modify it under the terms of the GNU Affero
|
||||
* General Public License as published by the Free Software Foundation,
|
||||
* either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* The code is distributed WITHOUT ANY WARRANTY; without even the
|
||||
* implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU Affero General Public License for more details.
|
||||
*
|
||||
* As additional permission under GNU Affero General Public License
|
||||
* section 7, you may distribute non-source (e.g., minimized or compacted)
|
||||
* forms of that code without the copy of the GNU Affero General Public
|
||||
* License normally required by section 4, provided you include this
|
||||
* license notice and a URL through which recipients can access the
|
||||
* Corresponding Source.
|
||||
*
|
||||
* @licend The above is the entire license notice
|
||||
* for the JavaScript code in this page.
|
||||
*/
|
||||
|
||||
// Removes the 'Apply Settings' button for Javascript users,
|
||||
// since changing any of the elements causes the settings to apply
|
||||
// automatically.
|
||||
let resultsSave = document.querySelector(".results-save");
|
||||
if (resultsSave != null) {
|
||||
resultsSave.style.display = "none";
|
||||
}
|
||||
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const searchWrapper = document.querySelectorAll('.wrapper, .wrapper-results')[0];
|
||||
const resultsWrapper = document.querySelector('.autocomplete');
|
||||
const clearSearch = document.querySelector("#clearSearch");
|
||||
|
||||
async function getSuggestions(query) {
|
||||
try {
|
||||
const params = new URLSearchParams({ "q": query }).toString();
|
||||
const response = await fetch(`/suggestions?${params}`);
|
||||
const data = await response.json();
|
||||
return data[1]; // Return only the array of suggestion strings
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
let currentIndex = -1; // Keep track of the currently selected suggestion
|
||||
|
||||
let results = [];
|
||||
searchInput.addEventListener('input', async () => {
|
||||
let input = searchInput.value;
|
||||
if (input.length) {
|
||||
results = await getSuggestions(input);
|
||||
}
|
||||
renderResults(results);
|
||||
currentIndex = -1; // Reset index when we return new results
|
||||
});
|
||||
|
||||
searchInput.addEventListener("focus", async () => {
|
||||
let input = searchInput.value;
|
||||
if (results.length === 0 && input.length != 0) {
|
||||
results = await getSuggestions(input);
|
||||
}
|
||||
renderResults(results);
|
||||
})
|
||||
|
||||
clearSearch.style.visibility = "visible"; // Only show the clear search button for JS users.
|
||||
clearSearch.addEventListener("click", () => {
|
||||
searchInput.value = "";
|
||||
searchInput.focus();
|
||||
})
|
||||
|
||||
searchInput.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'ArrowUp' || event.key === 'ArrowDown') {
|
||||
event.preventDefault(); // Prevent the cursor from moving in the search input
|
||||
|
||||
// Find the currently selected suggestion element
|
||||
const selectedSuggestion = resultsWrapper.querySelector('.selected');
|
||||
if (selectedSuggestion) {
|
||||
selectedSuggestion.classList.remove('selected'); // Deselect the currently selected suggestion
|
||||
}
|
||||
|
||||
// Increment or decrement the current index based on the arrow key pressed
|
||||
if (event.key === 'ArrowUp') {
|
||||
currentIndex--;
|
||||
} else {
|
||||
currentIndex++;
|
||||
}
|
||||
|
||||
// Wrap around the index if it goes out of bounds
|
||||
if (currentIndex < 0) {
|
||||
currentIndex = resultsWrapper.querySelectorAll('li').length - 1;
|
||||
} else if (currentIndex >= resultsWrapper.querySelectorAll('li').length) {
|
||||
currentIndex = 0;
|
||||
}
|
||||
|
||||
// Select the new suggestion
|
||||
resultsWrapper.querySelectorAll('li')[currentIndex].classList.add('selected');
|
||||
// Update the value of the search input
|
||||
searchInput.value = resultsWrapper.querySelectorAll('li')[currentIndex].textContent;
|
||||
}
|
||||
});
|
||||
|
||||
function renderResults(results) {
|
||||
if (!results || !results.length || !searchInput.value) {
|
||||
return searchWrapper.classList.remove('show');
|
||||
}
|
||||
|
||||
let content = '';
|
||||
results.forEach((item) => {
|
||||
content += `<li>${item}</li>`;
|
||||
});
|
||||
|
||||
// Only show the autocomplete suggestions if the search input has a non-empty value
|
||||
if (searchInput.value) {
|
||||
searchWrapper.classList.add('show');
|
||||
}
|
||||
resultsWrapper.innerHTML = `<ul>${content}</ul>`;
|
||||
}
|
||||
|
||||
resultsWrapper.addEventListener('click', (event) => {
|
||||
if (event.target.tagName === 'LI') {
|
||||
// Set the value of the search input to the clicked suggestion
|
||||
searchInput.value = event.target.textContent;
|
||||
// Reset the current index
|
||||
currentIndex = -1;
|
||||
// Submit the form
|
||||
searchWrapper.querySelector('input[type="submit"]').click();
|
||||
// Remove the show class from the search wrapper
|
||||
searchWrapper.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
document.addEventListener("keypress", (event) => {
|
||||
if (document.activeElement == searchInput) {
|
||||
// Allow the '/' character to be pressed when searchInput is active
|
||||
} else if (document.querySelector(".calc") != null) {
|
||||
// Do nothing if the calculator is available, so the division keybinding
|
||||
// will still work
|
||||
}
|
||||
else if (event.key == "/") {
|
||||
event.preventDefault();
|
||||
searchInput.focus();
|
||||
searchInput.selectionStart = searchInput.selectionEnd = searchInput.value.length;
|
||||
}
|
||||
})
|
||||
|
||||
// Add event listener to hide autocomplete suggestions when clicking outside of search-input or wrapper
|
||||
document.addEventListener('click', (event) => {
|
||||
// Check if the target of the event is the search-input or any of its ancestors
|
||||
if (!searchInput.contains(event.target) && !searchWrapper.contains(event.target)) {
|
||||
// Remove the show class from the search wrapper
|
||||
searchWrapper.classList.remove('show');
|
||||
}
|
||||
});
|
||||
|
||||
// Load material icons. If the file cannot be loaded,
|
||||
// skip them and put a warning in the console.
|
||||
const font = new FontFace('Material Icons Round', 'url("/fonts/material-icons-round-v108-latin-regular.woff2") format("woff2")');
|
||||
font.load().then(() => {
|
||||
const icons = document.getElementsByClassName('material-icons-round');
|
||||
|
||||
// Display all icons.
|
||||
for (let icon of icons) {
|
||||
icon.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
// Ensure icons for the different types of searches are sized correctly.
|
||||
document.querySelectorAll('#sub-search-wrapper-ico').forEach((el) => {
|
||||
el.style.fontSize = '17px';
|
||||
});
|
||||
}).catch(() => {
|
||||
console.warn('Failed to load Material Icons Round. Hiding any icons using said pack.');
|
||||
});
|
||||
|
||||
// load image after server side processing
|
||||
window.addEventListener('DOMContentLoaded', function () {
|
||||
var knoTitleElement = document.getElementById('kno_title');
|
||||
var kno_title = knoTitleElement.dataset.knoTitle;
|
||||
fetch(kno_title)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
const pageId = Object.keys(data.query.pages)[0];
|
||||
const thumbnailSource = data.query.pages[pageId].thumbnail.source;
|
||||
const url = "/img_proxy?url=" + thumbnailSource;
|
||||
|
||||
// update the img tag with url and add kno_wiki_show
|
||||
var imgElement = document.querySelector('.kno_wiki');
|
||||
imgElement.src = url;
|
||||
imgElement.classList.add('kno_wiki_show');
|
||||
|
||||
console.log(url);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('Error fetching data:', error);
|
||||
});
|
||||
});
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
|
||||
if (document.querySelectorAll(".search-active")[1].getAttribute("value") === "image") {
|
||||
|
||||
// image viewer for image search
|
||||
const closeButton = document.querySelector('.image-close');
|
||||
const imageView = document.querySelector('.image_view');
|
||||
const images = document.querySelector('.images');
|
||||
const viewImageImg = document.querySelector('.view-image-img');
|
||||
const imageSource = document.querySelector('.image-source');
|
||||
const imageFull = document.querySelector(".full-size");
|
||||
const imageProxy = document.querySelector('.proxy-size');
|
||||
const imageViewerLink = document.querySelector('.image-viewer-link');
|
||||
const imageSize = document.querySelector('.image-size');
|
||||
const fullImageSize = document.querySelector(".full-image-size");
|
||||
const imageAlt = document.querySelector('.image-alt');
|
||||
const openImageViewer = document.querySelectorAll('.open-image-viewer');
|
||||
const imageBefore = document.querySelector('.image-before');
|
||||
const imageNext = document.querySelector('.image-next');
|
||||
let currentImageIndex = 0;
|
||||
|
||||
closeButton.addEventListener('click', function () {
|
||||
imageView.classList.remove('image_show');
|
||||
imageView.classList.add('image_hide');
|
||||
for (const image of document.querySelectorAll(".image_selected")) {
|
||||
image.classList = ['image'];
|
||||
}
|
||||
images.classList.add('images_viewer_hidden');
|
||||
});
|
||||
|
||||
openImageViewer.forEach((image, index) => {
|
||||
image.addEventListener('click', function (event) {
|
||||
event.preventDefault();
|
||||
currentImageIndex = index;
|
||||
showImage();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (event) {
|
||||
if (searchInput == document.activeElement)
|
||||
return;
|
||||
if (event.key === 'ArrowLeft') {
|
||||
currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length;
|
||||
showImage();
|
||||
}
|
||||
else if (event.key === 'ArrowRight') {
|
||||
currentImageIndex = (currentImageIndex + 1) % openImageViewer.length;
|
||||
showImage();
|
||||
}
|
||||
});
|
||||
|
||||
imageBefore.addEventListener('click', function () {
|
||||
currentImageIndex = (currentImageIndex - 1 + openImageViewer.length) % openImageViewer.length;
|
||||
showImage();
|
||||
});
|
||||
|
||||
imageNext.addEventListener('click', function () {
|
||||
currentImageIndex = (currentImageIndex + 1) % openImageViewer.length;
|
||||
showImage();
|
||||
});
|
||||
|
||||
function showImage() {
|
||||
for (const image of document.querySelectorAll(".image_selected")) {
|
||||
image.classList = ['image'];
|
||||
}
|
||||
const current_image = document.querySelectorAll(".image")[currentImageIndex];
|
||||
current_image.classList.add("image_selected");
|
||||
var rect = current_image.getBoundingClientRect();
|
||||
if (!(rect.top >= 0 && rect.left >= 0 &&
|
||||
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
||||
rect.right <= (window.innerWidth || document.documentElement.clientWidth))) {
|
||||
current_image.scrollIntoView(false);
|
||||
}
|
||||
|
||||
const src = openImageViewer[currentImageIndex].getAttribute('src');
|
||||
const alt = openImageViewer[currentImageIndex].getAttribute('alt');
|
||||
const data = openImageViewer[currentImageIndex].getAttribute('data');
|
||||
const clickableLink = openImageViewer[currentImageIndex].closest('.clickable');
|
||||
const href = clickableLink.getAttribute('href');
|
||||
viewImageImg.src = src;
|
||||
imageProxy.href = src;
|
||||
imageFull.href = data;
|
||||
imageSource.href = href;
|
||||
imageSource.textContent = href;
|
||||
imageViewerLink.href = href;
|
||||
images.classList.remove('images_viewer_hidden');
|
||||
imageView.classList.remove('image_hide');
|
||||
imageView.classList.add('image_show');
|
||||
imageAlt.textContent = alt;
|
||||
fullImageSize.textContent = document.querySelector(".image_selected .resolution").textContent;
|
||||
|
||||
getImageSize(src).then(size => {
|
||||
imageSize.textContent = size;
|
||||
});
|
||||
}
|
||||
|
||||
function getImageSize(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.onload = function () {
|
||||
const size = `${this.width} x ${this.height}`;
|
||||
resolve(size);
|
||||
};
|
||||
img.onerror = function () {
|
||||
reject('Error loading image');
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
}
|
BIN
static/webfonts/material-icons-round-v108-latin-regular.woff2
Normal file
BIN
static/webfonts/material-icons-round-v108-latin-regular.woff2
Normal file
Binary file not shown.
160
suggestions.go
Normal file
160
suggestions.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func handleSuggestions(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query().Get("q")
|
||||
if query == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `["",[]]`)
|
||||
return
|
||||
}
|
||||
|
||||
// Define the fallback sequence with Google lower in the hierarchy
|
||||
suggestionSources := []func(string) []string{
|
||||
fetchDuckDuckGoSuggestions,
|
||||
fetchEdgeSuggestions,
|
||||
fetchBraveSuggestions,
|
||||
fetchEcosiaSuggestions,
|
||||
fetchQwantSuggestions,
|
||||
fetchStartpageSuggestions,
|
||||
// fetchGoogleSuggestions, // I advise against it, but you can use it if you want to
|
||||
}
|
||||
|
||||
var suggestions []string
|
||||
for _, fetchFunc := range suggestionSources {
|
||||
suggestions = fetchFunc(query)
|
||||
if len(suggestions) > 0 {
|
||||
printDebug("Suggestions found using %T", fetchFunc)
|
||||
break
|
||||
} else {
|
||||
printWarn("%T did not return any suggestions or failed.", fetchFunc)
|
||||
}
|
||||
}
|
||||
|
||||
if len(suggestions) == 0 {
|
||||
printErr("All suggestion services failed. Returning empty response.")
|
||||
}
|
||||
|
||||
// Return the final suggestions as JSON
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `["",%s]`, toJSONStringArray(suggestions))
|
||||
}
|
||||
|
||||
func fetchGoogleSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("http://suggestqueries.google.com/complete/search?client=firefox&q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Google: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchDuckDuckGoSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://duckduckgo.com/ac/?q=%s&type=list", encodedQuery)
|
||||
printDebug("Fetching suggestions from DuckDuckGo: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchEdgeSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://api.bing.com/osjson.aspx?query=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Edge (Bing): %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchBraveSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://search.brave.com/api/suggest?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Brave: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchEcosiaSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://ac.ecosia.org/?q=%s&type=list", encodedQuery)
|
||||
printDebug("Fetching suggestions from Ecosia: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchQwantSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://api.qwant.com/v3/suggest?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Qwant: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchStartpageSuggestions(query string) []string {
|
||||
encodedQuery := url.QueryEscape(query)
|
||||
url := fmt.Sprintf("https://startpage.com/suggestions?q=%s", encodedQuery)
|
||||
printDebug("Fetching suggestions from Startpage: %s", url)
|
||||
return fetchSuggestionsFromURL(url)
|
||||
}
|
||||
|
||||
func fetchSuggestionsFromURL(url string) []string {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
printWarn("Error fetching suggestions from %s: %v", url, err)
|
||||
return []string{}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
printWarn("Error reading response body from %s: %v", url, err)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Log the Content-Type for debugging
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
printDebug("Response Content-Type from %s: %s", url, contentType)
|
||||
|
||||
// Check if the body is non-empty
|
||||
if len(body) == 0 {
|
||||
printWarn("Received empty response body from %s", url)
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Attempt to parse the response as JSON regardless of Content-Type
|
||||
var parsedResponse []interface{}
|
||||
if err := json.Unmarshal(body, &parsedResponse); err != nil {
|
||||
printErr("Error parsing JSON from %s: %v", url, err)
|
||||
printDebug("Response body: %s", string(body))
|
||||
return []string{}
|
||||
}
|
||||
|
||||
// Ensure the response structure is as expected
|
||||
if len(parsedResponse) < 2 {
|
||||
printWarn("Unexpected response format from %v: %v", url, string(body))
|
||||
return []string{}
|
||||
}
|
||||
|
||||
suggestions := []string{}
|
||||
if items, ok := parsedResponse[1].([]interface{}); ok {
|
||||
for _, item := range items {
|
||||
if suggestion, ok := item.(string); ok {
|
||||
suggestions = append(suggestions, suggestion)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
printErr("Unexpected suggestions format in response from: %v", url)
|
||||
}
|
||||
|
||||
return suggestions
|
||||
}
|
||||
|
||||
func toJSONStringArray(strings []string) string {
|
||||
result := ""
|
||||
for i, str := range strings {
|
||||
result += fmt.Sprintf(`"%s"`, str)
|
||||
if i < len(strings)-1 {
|
||||
result += ","
|
||||
}
|
||||
}
|
||||
return "[" + result + "]"
|
||||
}
|
|
@ -21,6 +21,7 @@
|
|||
<link rel="stylesheet" href="/static/css/stars.css" />
|
||||
<link rel="stylesheet" href="/static/css/fancy-gallery.css" />
|
||||
<link rel="stylesheet" href="/static/css/extras.css" />
|
||||
<link rel="stylesheet" href="/static/css/search.css" />
|
||||
<noscript><link rel="stylesheet" href="/static/css/noscript.css" /></noscript>
|
||||
</head>
|
||||
<body class="is-preload">
|
||||
|
@ -164,6 +165,28 @@
|
|||
</footer>
|
||||
</section>
|
||||
|
||||
<!-- Search -->
|
||||
<section id="search" class="main special">
|
||||
<div class="reset-styles">
|
||||
<form action="https://search.spitfirebrowser.com/search" class="search-container" method="post" autocomplete="off">
|
||||
<h1>Ocásek</h1>
|
||||
<div class="wrapper">
|
||||
<input type="text" name="q" autofocus id="search-input" placeholder="Type to search..." />
|
||||
<button id="search-wrapper-ico" class="material-icons-round" name="t" value="text" type="submit">search</button>
|
||||
<div class="autocomplete">
|
||||
<ul>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-button-wrapper">
|
||||
<input type="hidden" name="p" value="1">
|
||||
<!-- <button name="t" value="text" type="submit">Search Text</button>
|
||||
<button name="t" value="image" type="submit">Search Images</button> -->
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Get Started -->
|
||||
<section id="cta" class="main special">
|
||||
<header class="major">
|
||||
|
@ -220,6 +243,7 @@
|
|||
<script src="/static/js/breakpoints.min.js" defer></script>
|
||||
<script src="/static/js/util.js" defer></script>
|
||||
<script src="/static/js/main.js" defer></script>
|
||||
<script defer src="/static/js/autocomplete.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue