diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dc1715a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "monthly" diff --git a/Cargo.lock b/Cargo.lock index 4e1814f..e644185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -865,9 +865,9 @@ dependencies = [ [[package]] name = "error-stack" -version = "0.5.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe413319145d1063f080f27556fd30b1d70b01e2ba10c2a6e40d4be982ffc5d1" +checksum = "27a72baa257b5e0e2de241967bc5ee8f855d6072351042688621081d66b2a76b" dependencies = [ "anyhow", "rustc_version 0.4.0", @@ -1188,16 +1188,16 @@ dependencies = [ [[package]] name = "html5ever" -version = "0.27.0" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" dependencies = [ "log", "mac", - "markup5ever 0.12.1", + "markup5ever 0.11.0", "proc-macro2 1.0.86", "quote 1.0.36", - "syn 2.0.74", + "syn 1.0.109", ] [[package]] @@ -1598,13 +1598,13 @@ dependencies = [ [[package]] name = "markup5ever" -version = "0.12.1" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ "log", - "phf 0.11.2", - "phf_codegen 0.11.2", + "phf 0.10.1", + "phf_codegen 0.10.0", "string_cache 0.8.7", "string_cache_codegen 0.5.2", "tendril", @@ -1992,16 +1992,6 @@ dependencies = [ "phf_shared 0.10.0", ] -[[package]] -name = "phf_codegen" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a" -dependencies = [ - "phf_generator 0.11.2", - "phf_shared 0.11.2", -] - [[package]] name = "phf_generator" version = "0.7.24" @@ -2677,14 +2667,14 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "scraper" -version = "0.20.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b90460b31bfe1fc07be8262e42c665ad97118d4585869de9345a84d501a9eaf0" +checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" dependencies = [ "ahash", "cssparser", "ego-tree", - "html5ever 0.27.0", + "html5ever 0.26.0", "once_cell", "selectors", "tendril", @@ -2788,9 +2778,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.124" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "66ad62847a56b3dba58cc891acd13884b9c61138d330c0d7b6181713d4fce38d" dependencies = [ "itoa 1.0.11", "memchr", diff --git a/Cargo.toml b/Cargo.toml index 57881ae..27ddf91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,11 +20,11 @@ tokio = { version = "1.32.0", features = [ "io-util", ], default-features = false } serde = { version = "1.0.196", default-features = false, features = ["derive"] } -serde_json = { version = "1.0.125", default-features = false } +serde_json = { version = "1.0.116", default-features = false } maud = { version = "0.26.0", default-features = false, features = [ "actix-web", ] } -scraper = { version = "0.20.0", default-features = false } +scraper = { version = "0.18.1", default-features = false } actix-web = { version = "4.4.0", features = [ "cookies", "macros", @@ -35,7 +35,7 @@ actix-cors = { version = "0.7.0", default-features = false } fake-useragent = { version = "0.1.3", default-features = false } env_logger = { version = "0.11.1", default-features = false } log = { version = "0.4.21", default-features = false } -error-stack = { version = "0.5.0", default-features = false, features = [ +error-stack = { version = "0.4.0", default-features = false, features = [ "std", ] } async-trait = { version = "0.1.80", default-features = false } diff --git a/public/static/cookies.js b/public/static/cookies.js new file mode 100644 index 0000000..3525c5b --- /dev/null +++ b/public/static/cookies.js @@ -0,0 +1,94 @@ +/** + * This functions gets the saved cookies if it is present on the user's machine If it + * is available then it is parsed and converted to an object which is then used to + * retrieve the preferences that the user had selected previously and is then loaded + * and used for displaying the user provided settings by setting them as the selected + * options in the settings page. + * + * @function + * @param {string} cookie - It takes the client settings cookie as a string. + * @returns {void} + */ +function setClientSettingsOnPage(cookie) { + let cookie_value = cookie + .split(';') + .map((item) => item.split('=')) + .reduce((acc, [_, v]) => (acc = JSON.parse(v)) && acc, {}) + + // Loop through all select tags and add their values to the cookie dictionary + document.querySelectorAll('select').forEach((select_tag) => { + switch (select_tag.name) { + case 'themes': + select_tag.value = cookie_value['theme'] + break + case 'colorschemes': + select_tag.value = cookie_value['colorscheme'] + break + case 'animations': + select_tag.value = cookie_value['animation'] + break + case 'safe_search_levels': + select_tag.value = cookie_value['safe_search_level'] + break + } + }) + let engines = document.querySelectorAll('.engine') + let engines_cookie = cookie_value['engines'] + + if (engines_cookie.length === engines.length) { + document.querySelector('.select_all').checked = true + engines.forEach((engine_checkbox) => { + engine_checkbox.checked = true + }) + } else { + engines.forEach((engines_checkbox) => { + engines_checkbox.checked = false + }) + engines_cookie.forEach((engine_name) => { + engines.forEach((engine_checkbox) => { + if ( + engine_checkbox.parentNode.parentNode.innerText.trim() === + engine_name.trim() + ) { + engine_checkbox.checked = true + } + }) + }) + } +} + +/** + * This function is executed when any page on the website finishes loading and + * this function retrieves the cookies if it is present on the user's machine. + * If it is available then the saved cookies is display in the cookies tab + * otherwise an appropriate message is displayed if it is not available. + * + * @function + * @listens DOMContentLoaded + * @returns {void} + */ +document.addEventListener( + 'DOMContentLoaded', + () => { + try { + // Decode the cookie value + let cookie = decodeURIComponent(document.cookie) + // Set the value of the input field to the decoded cookie value if it is not empty + // Otherwise, display a message indicating that no cookies have been saved on the user's system + if (cookie.length) { + document.querySelector('.cookies input').value = cookie + // This function displays the user provided settings on the settings page. + setClientSettingsOnPage(cookie) + } else { + document.querySelector('.cookies input').value = + 'No cookies have been saved on your system' + } + } catch (error) { + // If there is an error decoding the cookie, log the error to the console + // and display an error message in the input field + console.error('Error decoding cookie:', error) + document.querySelector('.cookies input').value = 'Error decoding cookie' + } + }, + false, +) diff --git a/public/static/error_box.js b/public/static/error_box.js new file mode 100644 index 0000000..1e2e874 --- /dev/null +++ b/public/static/error_box.js @@ -0,0 +1,7 @@ +/** + * This function provides the ability for the button to toggle the dropdown error-box + * in the search page. + */ +function toggleErrorBox() { + document.querySelector('.dropdown_error_box').classList.toggle('show') +} diff --git a/public/static/index.js b/public/static/index.js new file mode 100644 index 0000000..515065a --- /dev/null +++ b/public/static/index.js @@ -0,0 +1,34 @@ +/** + * Selects the input element for the search box + * @type {HTMLInputElement} + */ +const searchBox = document.querySelector('input') + +/** + * Redirects the user to the search results page with the query parameter + */ +function searchWeb() { + const query = searchBox.value.trim() + try { + let safeSearchLevel = document.querySelector('.search_options select').value + if (query) { + window.location.href = `search?q=${encodeURIComponent( + query, + )}&safesearch=${encodeURIComponent(safeSearchLevel)}` + } + } catch (error) { + if (query) { + window.location.href = `search?q=${encodeURIComponent(query)}` + } + } +} + +/** + * Listens for the 'Enter' key press event on the search box and calls the searchWeb function + * @param {KeyboardEvent} e - The keyboard event object + */ +searchBox.addEventListener('keyup', (e) => { + if (e.key === 'Enter') { + searchWeb() + } +}) diff --git a/public/static/pagination.js b/public/static/pagination.js new file mode 100644 index 0000000..bdbfb39 --- /dev/null +++ b/public/static/pagination.js @@ -0,0 +1,39 @@ +/** + * Navigates to the next page by incrementing the current page number in the URL query string. + * @returns {void} + */ +function navigate_forward() { + let url = new URL(window.location); + let searchParams = url.searchParams; + + let q = searchParams.get('q'); + let page = parseInt(searchParams.get('page')); + + if (isNaN(page)) { + page = 1; + } else { + page++; + } + + window.location.href = `${url.origin}${url.pathname}?q=${encodeURIComponent(q)}&page=${page}`; +} + +/** + * Navigates to the previous page by decrementing the current page number in the URL query string. + * @returns {void} + */ +function navigate_backward() { + let url = new URL(window.location); + let searchParams = url.searchParams; + + let q = searchParams.get('q'); + let page = parseInt(searchParams.get('page')); + + if (isNaN(page)) { + page = 0; + } else if (page > 0) { + page--; + } + + window.location.href = `${url.origin}${url.pathname}?q=${encodeURIComponent(q)}&page=${page}`; +} diff --git a/public/static/search_area_options.js b/public/static/search_area_options.js new file mode 100644 index 0000000..10e0390 --- /dev/null +++ b/public/static/search_area_options.js @@ -0,0 +1,18 @@ +document.addEventListener( + 'DOMContentLoaded', + () => { + let url = new URL(window.location) + let searchParams = url.searchParams + + let safeSearchLevel = searchParams.get('safesearch') + + if ( + safeSearchLevel >= 0 && + safeSearchLevel <= 2 && + safeSearchLevel !== null + ) { + document.querySelector('.search_options select').value = safeSearchLevel + } + }, + false, +) diff --git a/public/static/settings.js b/public/static/settings.js new file mode 100644 index 0000000..b8d8f88 --- /dev/null +++ b/public/static/settings.js @@ -0,0 +1,155 @@ +/** + * This function handles the toggling of selections of all upstream search engines + * options in the settings page under the tab engines. + */ +function toggleAllSelection() { + document + .querySelectorAll('.engine') + .forEach( + (engine_checkbox) => + (engine_checkbox.checked = + document.querySelector('.select_all').checked), + ) +} + +/** + * This function adds the functionality to sidebar buttons to only show settings + * related to that tab. + * @param {HTMLElement} current_tab - The current tab that was clicked. + */ +function setActiveTab(current_tab) { + // Remove the active class from all tabs and buttons + document + .querySelectorAll('.tab') + .forEach((tab) => tab.classList.remove('active')) + document + .querySelectorAll('.btn') + .forEach((tab) => tab.classList.remove('active')) + + // Add the active class to the current tab and its corresponding settings + current_tab.classList.add('active') + document + .querySelector(`.${current_tab.innerText.toLowerCase().replace(' ', '_')}`) + .classList.add('active') +} + +/** + * This function adds the functionality to save all the user selected preferences + * to be saved in a cookie on the users machine. + */ +function setClientSettings() { + // Create an object to store the user's preferences + let cookie_dictionary = new Object() + + // Loop through all select tags and add their values to the cookie dictionary + document.querySelectorAll('select').forEach((select_tag) => { + switch (select_tag.name) { + case 'themes': + cookie_dictionary['theme'] = select_tag.value + break + case 'colorschemes': + cookie_dictionary['colorscheme'] = select_tag.value + break + case 'animations': + cookie_dictionary['animation'] = select_tag.value || null + break + case 'safe_search_levels': + cookie_dictionary['safe_search_level'] = Number(select_tag.value) + break + } + }) + + // Loop through all engine checkboxes and add their values to the cookie dictionary + let engines = [] + + document.querySelectorAll('.engine').forEach((engine_checkbox) => { + if (engine_checkbox.checked) { + engines.push(engine_checkbox.parentNode.parentNode.innerText.trim()) + } + }) + + cookie_dictionary['engines'] = engines + + // Set the expiration date for the cookie to 1 year from the current date + let expiration_date = new Date() + expiration_date.setFullYear(expiration_date.getFullYear() + 1) + + // Save the cookie to the user's machine + document.cookie = `appCookie=${JSON.stringify( + cookie_dictionary, + )}; expires=${expiration_date.toUTCString()}` + + // Display a success message to the user + document.querySelector('.message').innerText = + '✅ The settings have been saved sucessfully!!' + + // Clear the success message after 10 seconds + setTimeout(() => { + document.querySelector('.message').innerText = '' + }, 10000) +} + +/** + * This functions gets the saved cookies if it is present on the user's machine If it + * is available then it is parsed and converted to an object which is then used to + * retrieve the preferences that the user had selected previously and is then loaded in the + * website otherwise the function does nothing and the default server side settings are loaded. + */ +function getClientSettings() { + // Get the appCookie from the user's machine + let cookie = decodeURIComponent(document.cookie) + + // If the cookie is not empty, parse it and use it to set the user's preferences + if (cookie.length) { + let cookie_value = cookie + .split(';') + .map((item) => item.split('=')) + .reduce((acc, [_, v]) => (acc = JSON.parse(v)) && acc, {}) + + let links = Array.from(document.querySelectorAll('link')) + + // A check to determine whether the animation link exists under the head tag or not. + // If it does not exists then create and add a new animation link under the head tag + // and update the other link tags href according to the settings provided by the user + // via the UI. On the other hand if it does exist then just update all the link tags + // href according to the settings provided by the user via the UI. + if (!links.some((item) => item.href.includes('static/animations'))) { + if (cookie_value['animation']) { + let animation_link = document.createElement('link') + animation_link.href = `static/animations/${cookie_value['animation']}.css` + animation_link.rel = 'stylesheet' + animation_link.type = 'text/css' + document.querySelector('head').appendChild(animation_link) + } + // Loop through all link tags and update their href values to match the user's preferences + links.forEach((item) => { + if (item.href.includes('static/themes')) { + item.href = `static/themes/${cookie_value['theme']}.css` + } else if (item.href.includes('static/colorschemes')) { + item.href = `static/colorschemes/${cookie_value['colorscheme']}.css` + } + }) + } else { + // Loop through all link tags and update their href values to match the user's preferences + links.forEach((item) => { + if (item.href.includes('static/themes')) { + item.href = `static/themes/${cookie_value['theme']}.css` + } else if (item.href.includes('static/colorschemes')) { + item.href = `static/colorschemes/${cookie_value['colorscheme']}.css` + } else if ( + item.href.includes('static/animations') && + cookie_value['animation'] + ) { + item.href = `static/colorschemes/${cookie_value['animation']}.css` + } + }) + if (!cookie_value['animation']) { + document + .querySelector('head') + .removeChild( + links.filter((item) => item.href.includes('static/animations')), + ) + } + } + } +} diff --git a/public/static/themes/simple.css b/public/static/themes/simple.css index 770cb04..8f5fcc7 100644 --- a/public/static/themes/simple.css +++ b/public/static/themes/simple.css @@ -35,19 +35,19 @@ button { /* styles for the index page */ -.search_container { +.search-container { display: flex; - flex-direction: row; + flex-direction: column; gap: 5rem; justify-content: center; align-items: center; } -.search_container svg { +.search-container svg { color: var(--logo-color); } -.search_container div { +.search-container div { display: flex; } @@ -102,10 +102,10 @@ button { } .search_bar button img { - position: absolute; - left: 50%; - top: 50%; - transform: translate(-50%, -50%); + position:absolute; + left:50%; + top:50%; + transform:translate(-50%, -50%); } .search_bar button:active { @@ -336,7 +336,7 @@ footer div { .results_aggregated { display: flex; flex-direction: column; - justify-content: space-between; + justify-content: space-between; margin: 2rem 0; content-visibility: auto; } @@ -593,7 +593,7 @@ footer div { font-size: 2.5rem; } -.settings>h1 { +.settings > h1 { margin-bottom: 4rem; margin-left: 2rem; } @@ -603,7 +603,7 @@ footer div { margin: 0.3rem 0 1rem; } -.settings>hr { +.settings > hr { margin-left: 2rem; } @@ -796,15 +796,15 @@ footer div { transition: .2s; } -input:checked+.slider { +input:checked + .slider { background-color: var(--color-three); } -input:focus+.slider { +input:focus + .slider { box-shadow: 0 0 1px var(--color-three); } -input:checked+.slider::before { +input:checked + .slider::before { transform: translateX(2.6rem); } @@ -817,7 +817,7 @@ input:checked+.slider::before { border-radius: 50%; } -@media screen and (width <=1136px) { +@media screen and (width <= 1136px) { .hero-text-container { width: unset; } @@ -827,7 +827,7 @@ input:checked+.slider::before { } } -@media screen and (width <=706px) { +@media screen and (width <= 706px) { .about-container article .logo-container svg { width: clamp(200px, 290px, 815px); } @@ -851,7 +851,7 @@ input:checked+.slider::before { .features { grid-template-columns: 1fr; } - + .feature-list { padding: 35px 0; } @@ -859,4 +859,4 @@ input:checked+.slider::before { .feature-card { border-radius: 0; } -} \ No newline at end of file +} diff --git a/src/config.rs b/src/config.rs index 1c8da5c..d7cf3b1 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,12 +4,14 @@ use figment::{providers::Serialized, Figment}; use serde::{Deserialize, Serialize}; /// Struct holding config Options -#[derive(Debug, Deserialize, Serialize, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { /// It stores the parsed port number option on which the server should launch. pub port: u16, /// It stores the parsed ip address option on which the server should launch pub binding_ip: String, + /// It stores the theming options for the website. + pub style: Style, /// Memory cache invalidation time pub cache_expiry_time: u64, /// It stores the option to whether enable or disable logs. @@ -19,7 +21,7 @@ pub struct Config { /// It toggles whether to use adaptive HTTP windows pub adaptive_window: bool, /// It stores all the engine names that were enabled by the user. - pub upstream_search_engines: crate::engines::Engines, + pub upstream_search_engines: Vec, /// It stores the time (secs) which controls the server request timeout. pub request_timeout: u8, /// Set the keep-alive time for client connections to the HTTP server @@ -30,6 +32,19 @@ pub struct Config { pub pool_idle_connection_timeout: u8, } +/// A struct holding style config +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct Style { + /// It stores the parsed theme option used to set a theme for the website. + pub theme: String, + /// It stores the parsed colorscheme option used to set a colorscheme for the + /// theme being used. + pub colorscheme: String, + /// It stores the parsed animation option used to set an animation for the + /// theme being used. + pub animation: Option, +} + /// Configuration options for the rate limiter middleware. pub struct RateLimiter { /// The number of request that are allowed within a provided time limit. @@ -43,11 +58,24 @@ impl Default for Config { Self { port: 8080, binding_ip: "127.0.0.1".into(), + style: Style { + theme: "simple".into(), + colorscheme: "catppuccin-mocha".into(), + animation: Some("simple-frosted-glow".into()), + }, cache_expiry_time: 600, logging: true, debug: false, adaptive_window: false, - upstream_search_engines: Default::default(), + upstream_search_engines: vec![ + "bing".into(), + "brave".into(), + "duckduckgo".into(), + "librex".into(), + "mojeek".into(), + "searx".into(), + "startpage".into(), + ], request_timeout: 2, tcp_connection_keep_alive: 10, pool_idle_connection_timeout: 30, diff --git a/src/engines/bing.rs b/src/engines/bing.rs index 8465dbc..cdfb884 100644 --- a/src/engines/bing.rs +++ b/src/engines/bing.rs @@ -24,19 +24,18 @@ pub struct Bing { parser: SearchResultParser, } -impl Default for Bing { +impl Bing { /// Creates the Bing parser. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( ".b_results", ".b_algo", "h2 a", ".tpcn a.tilk", ".b_caption p", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/brave.rs b/src/engines/brave.rs index b816f1d..771ac96 100644 --- a/src/engines/brave.rs +++ b/src/engines/brave.rs @@ -20,19 +20,18 @@ pub struct Brave { parser: SearchResultParser, } -impl Default for Brave { +impl Brave { /// Creates the Brave parser. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( "#results h4", "#results [data-pos]", "a > .url", "a", ".snippet-description", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/duckduckgo.rs b/src/engines/duckduckgo.rs index 71e5ef5..f6743a1 100644 --- a/src/engines/duckduckgo.rs +++ b/src/engines/duckduckgo.rs @@ -23,19 +23,18 @@ pub struct DuckDuckGo { parser: SearchResultParser, } -impl Default for DuckDuckGo { +impl DuckDuckGo { /// Creates the DuckDuckGo parser. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( ".no-results", ".results>.result", ".result__title>.result__a", ".result__url", ".result__snippet", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/librex.rs b/src/engines/librex.rs index 86053a6..5a0fffe 100644 --- a/src/engines/librex.rs +++ b/src/engines/librex.rs @@ -20,23 +20,22 @@ pub struct LibreX { parser: SearchResultParser, } -impl Default for LibreX { +impl LibreX { /// Creates a new instance of LibreX with a default configuration. /// /// # Returns /// /// Returns a `Result` containing `LibreX` if successful, otherwise an `EngineError`. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( ".text-result-container>p", ".text-result-container", ".text-result-wrapper>a>h2", ".text-result-wrapper>a", ".text-result-wrapper>span", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/mod.rs b/src/engines/mod.rs index 80f1ea7..a93c9c2 100644 --- a/src/engines/mod.rs +++ b/src/engines/mod.rs @@ -3,12 +3,6 @@ //! provide a standard functions to be implemented for all the upstream search engine handling //! code. Moreover, it also provides a custom error for the upstream search engine handling code. -use std::sync::Arc; - -use serde::{Deserialize, Serialize}; - -use crate::models::engine_models::EngineHandler; - pub mod bing; pub mod brave; pub mod duckduckgo; @@ -17,79 +11,3 @@ pub mod mojeek; pub mod search_result_parser; pub mod searx; pub mod startpage; - -/// Struct that keeps track of search engines -#[derive(Debug, Serialize, Deserialize, Clone, Copy)] -pub struct Engines { - bing: bool, - brave: bool, - duckduckgo: bool, - librex: bool, - mojeek: bool, - searx: bool, - startpage: bool, -} - -impl Default for Engines { - fn default() -> Self { - Self { - bing: true, - brave: true, - duckduckgo: true, - librex: true, - mojeek: true, - searx: true, - startpage: true, - } - } -} - -impl From<&Engines> for Vec { - fn from(value: &Engines) -> Self { - let mut v = vec![]; - if value.duckduckgo { - let engine = crate::engines::duckduckgo::DuckDuckGo::default(); - v.push(EngineHandler::new("duckduckgo", Arc::new(engine))); - } - if value.searx { - let engine = crate::engines::searx::Searx::default(); - v.push(EngineHandler::new("searx", Arc::new(engine))); - } - if value.brave { - let engine = crate::engines::brave::Brave::default(); - v.push(EngineHandler::new("brave", Arc::new(engine))); - } - if value.startpage { - let engine = crate::engines::startpage::Startpage::default(); - v.push(EngineHandler::new("startpage", Arc::new(engine))); - } - if value.librex { - let engine = crate::engines::librex::LibreX::default(); - v.push(EngineHandler::new("librex", Arc::new(engine))); - } - if value.mojeek { - let engine = crate::engines::mojeek::Mojeek::default(); - v.push(EngineHandler::new("mojeek", Arc::new(engine))); - } - if value.bing { - let engine = crate::engines::bing::Bing::default(); - v.push(EngineHandler::new("bing", Arc::new(engine))); - } - v - } -} - -impl Engines { - /// Returns a list of all engines - pub fn list(&self) -> Box<[&'static str]> { - Box::new([ - "duckduckgo", - "searx", - "brave", - "startpage", - "librex", - "mojeek", - "bing", - ]) - } -} diff --git a/src/engines/mojeek.rs b/src/engines/mojeek.rs index 4da13ec..f9cf59b 100644 --- a/src/engines/mojeek.rs +++ b/src/engines/mojeek.rs @@ -23,19 +23,18 @@ pub struct Mojeek { parser: SearchResultParser, } -impl Default for Mojeek { +impl Mojeek { /// Creates the Mojeek parser. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( ".result-col", ".results-standard li", "a span.url", "h2 a.title", "p.s", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/searx.rs b/src/engines/searx.rs index 842c17d..6fef473 100644 --- a/src/engines/searx.rs +++ b/src/engines/searx.rs @@ -19,19 +19,18 @@ pub struct Searx { parser: SearchResultParser, } -impl Default for Searx { +impl Searx { /// creates a Searx parser - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( "#urls>.dialog-error>p", ".result", "h3>a", "h3>a", ".content", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/engines/startpage.rs b/src/engines/startpage.rs index 405b900..9c8b070 100644 --- a/src/engines/startpage.rs +++ b/src/engines/startpage.rs @@ -23,19 +23,18 @@ pub struct Startpage { parser: SearchResultParser, } -impl Default for Startpage { +impl Startpage { /// Creates the Startpage parser. - fn default() -> Self { - Self { + pub fn new() -> Result { + Ok(Self { parser: SearchResultParser::new( ".no-results", ".w-gl__result__main", ".w-gl__result-second-line-container>.w-gl__result-title>h3", ".w-gl__result-url", ".w-gl__description", - ) - .expect("somehow you changed the static stings in the binary i guess"), - } + )?, + }) } } diff --git a/src/main.rs b/src/main.rs index 63efb07..0763538 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ //! and register all the routes for the `crabbysearch` meta search engine website. #![forbid(unsafe_code, clippy::panic)] -#![deny(missing_docs, clippy::perf)] +#![deny(missing_docs, clippy::missing_docs_in_private_items, clippy::perf)] #![warn(clippy::cognitive_complexity, rust_2018_idioms)] pub mod cache; @@ -50,7 +50,7 @@ async fn main() { config.port, ); - let listener = TcpListener::bind((config.binding_ip.clone(), config.port)) + let listener = TcpListener::bind((config.binding_ip.as_str(), config.port)) .expect("could not create TcpListener"); let public_folder_path: &str = file_path(FileType::Theme).unwrap(); @@ -86,6 +86,7 @@ async fn main() { .service(router::index) // index page .service(server::routes::search::search) // search page .service(router::about) // about page + .service(router::settings) // settings page .default_service(web::route().to(router::not_found)) // error page }) // Start server on 127.0.0.1 with the user provided port number. for example 127.0.0.1:8080. diff --git a/src/models/aggregation_models.rs b/src/models/aggregation_models.rs index 4cad531..c3c210b 100644 --- a/src/models/aggregation_models.rs +++ b/src/models/aggregation_models.rs @@ -113,6 +113,14 @@ pub struct SearchResults { /// Stores the information on which engines failed with their engine name /// and the type of error that caused it. pub engine_errors_info: Vec, + /// Stores the flag option which holds the check value that the following + /// search query was disallowed when the safe search level set to 4 and it + /// was present in the `Blocklist` file. + pub disallowed: bool, + /// Stores the flag option which holds the check value that the following + /// search query was filtered when the safe search level set to 3 and it + /// was present in the `Blocklist` file. + pub filtered: bool, /// Stores the safe search level `safesearch` provided in the search url. pub safe_search_level: u8, /// Stores the flag option which holds the check value that whether any search engines were @@ -135,12 +143,23 @@ impl SearchResults { Self { results, engine_errors_info: engine_errors_info.to_owned(), - + disallowed: Default::default(), + filtered: Default::default(), safe_search_level: Default::default(), no_engines_selected: Default::default(), } } + /// A setter function that sets disallowed to true. + pub fn set_disallowed(&mut self) { + self.disallowed = true; + } + + /// A setter function that sets the filtered to true. + pub fn set_filtered(&mut self, filtered: bool) { + self.filtered = filtered; + } + /// A getter function that gets the value of `engine_errors_info`. pub fn engine_errors_info(&mut self) -> Vec { std::mem::take(&mut self.engine_errors_info) diff --git a/src/models/engine_models.rs b/src/models/engine_models.rs index b7022b6..26cb21d 100644 --- a/src/models/engine_models.rs +++ b/src/models/engine_models.rs @@ -2,9 +2,9 @@ //! the upstream search engines with the search query provided by the user. use super::aggregation_models::SearchResult; -use error_stack::{Result, ResultExt}; +use error_stack::{Report, Result, ResultExt}; use reqwest::Client; -use std::{fmt, sync::Arc}; +use std::fmt; /// A custom error type used for handle engine associated errors. #[derive(Debug)] @@ -150,15 +150,20 @@ pub trait SearchEngine: Sync + Send { } /// A named struct which stores the engine struct with the name of the associated engine. -#[derive(Clone)] pub struct EngineHandler { /// It stores the engine struct wrapped in a box smart pointer as the engine struct implements /// the `SearchEngine` trait. - engine: Arc, + engine: Box, /// It stores the name of the engine to which the struct is associated to. name: &'static str, } +impl Clone for EngineHandler { + fn clone(&self) -> Self { + Self::new(self.name).unwrap() + } +} + impl EngineHandler { /// Parses an engine name into an engine handler. /// @@ -169,13 +174,53 @@ impl EngineHandler { /// # Returns /// /// It returns an option either containing the value or a none if the engine is unknown - pub fn new(name: &'static str, engine: Arc) -> Self { - Self { name, engine } + pub fn new(engine_name: &str) -> Result { + let engine: (&'static str, Box) = + match engine_name.to_lowercase().as_str() { + "duckduckgo" => { + let engine = crate::engines::duckduckgo::DuckDuckGo::new()?; + ("duckduckgo", Box::new(engine)) + } + "searx" => { + let engine = crate::engines::searx::Searx::new()?; + ("searx", Box::new(engine)) + } + "brave" => { + let engine = crate::engines::brave::Brave::new()?; + ("brave", Box::new(engine)) + } + "startpage" => { + let engine = crate::engines::startpage::Startpage::new()?; + ("startpage", Box::new(engine)) + } + "librex" => { + let engine = crate::engines::librex::LibreX::new()?; + ("librex", Box::new(engine)) + } + "mojeek" => { + let engine = crate::engines::mojeek::Mojeek::new()?; + ("mojeek", Box::new(engine)) + } + "bing" => { + let engine = crate::engines::bing::Bing::new()?; + ("bing", Box::new(engine)) + } + _ => { + return Err(Report::from(EngineError::NoSuchEngineFound( + engine_name.to_string(), + ))) + } + }; + + Ok(Self { + engine: engine.1, + name: engine.0, + }) } /// This function converts the EngineHandler type into a tuple containing the engine name and /// the associated engine struct. - pub fn into_name_engine(self) -> (&'static str, Arc) { + pub fn into_name_engine(self) -> (&'static str, Box) { (self.name, self.engine) } } diff --git a/src/models/server_models.rs b/src/models/server_models.rs index c73352c..a536683 100644 --- a/src/models/server_models.rs +++ b/src/models/server_models.rs @@ -1,13 +1,17 @@ //! This module provides the models to parse cookies and search parameters from the search //! engine website. +use std::borrow::Cow; use serde::Deserialize; + +use crate::config::Style; + /// A named struct which deserializes all the user provided search parameters and stores them. #[derive(Deserialize)] pub struct SearchParams { /// It stores the search parameter option `q` (or query in simple words) /// of the search url. - pub query: Option, + pub q: Option, /// It stores the search parameter `page` (or pageno in simple words) /// of the search url. pub page: Option, @@ -15,3 +19,27 @@ pub struct SearchParams { /// search url. pub safesearch: Option, } + +/// A named struct which is used to deserialize the cookies fetched from the client side. +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct Cookie<'a> { + /// It stores the theme name used in the website. + pub theme: Cow<'a, str>, + /// It stores the colorscheme name used for the website theme. + pub colorscheme: Cow<'a, str>, + /// It stores the user selected upstream search engines selected from the UI. + pub engines: Cow<'a, Vec>>, +} + +impl<'a> Cookie<'a> { + /// server_models::Cookie contructor function + pub fn build(style: &'a Style, mut engines: Vec>) -> Self { + engines.sort(); + Self { + theme: Cow::Borrowed(&style.theme), + colorscheme: Cow::Borrowed(&style.colorscheme), + engines: Cow::Owned(engines), + } + } +} diff --git a/src/results/aggregator.rs b/src/results/aggregator.rs index a7116c6..7386bb2 100644 --- a/src/results/aggregator.rs +++ b/src/results/aggregator.rs @@ -147,7 +147,9 @@ pub async fn aggregate( }; } - let results: Vec = result_map.iter().map(|(_, value)| value.clone()).collect(); + let mut results: Vec = + result_map.iter().map(|(_, value)| value.clone()).collect(); + results.sort_by(|a, b| a.description.len().cmp(&b.description.len())); Ok(SearchResults::new(results, &engine_errors_info)) } diff --git a/src/server/router.rs b/src/server/router.rs index e37c93e..94c974d 100644 --- a/src/server/router.rs +++ b/src/server/router.rs @@ -2,24 +2,39 @@ //! meta search engine website and provide appropriate response to each route/page //! when requested. -use crate::handler::{file_path, FileType}; -use actix_web::{get, http::header::ContentType, HttpRequest, HttpResponse}; +use crate::{ + config::Config, + handler::{file_path, FileType}, +}; +use actix_web::{get, http::header::ContentType, web, HttpRequest, HttpResponse}; use tokio::fs::read_to_string; /// Handles the route of index page or main page of the `crabbysearch` meta search engine website. #[get("/")] -pub async fn index() -> Result> { - Ok(HttpResponse::Ok() - .content_type(ContentType::html()) - .body(crate::templates::views::index::index().0)) +pub async fn index(config: web::Data) -> Result> { + Ok(HttpResponse::Ok().content_type(ContentType::html()).body( + crate::templates::views::index::index( + &config.style.colorscheme, + &config.style.theme, + &config.style.animation, + ) + .0, + )) } /// Handles the route of any other accessed route/page which is not provided by the /// website essentially the 404 error page. -pub async fn not_found() -> Result> { - Ok(HttpResponse::Ok() - .content_type(ContentType::html()) - .body(crate::templates::views::not_found::not_found().0)) +pub async fn not_found( + config: web::Data, +) -> Result> { + Ok(HttpResponse::Ok().content_type(ContentType::html()).body( + crate::templates::views::not_found::not_found( + &config.style.colorscheme, + &config.style.theme, + &config.style.animation, + ) + .0, + )) } /// Handles the route of robots.txt page of the `crabbysearch` meta search engine website. @@ -34,8 +49,29 @@ pub async fn robots_data(_req: HttpRequest) -> Result Result> { - Ok(HttpResponse::Ok() - .content_type(ContentType::html()) - .body(crate::templates::views::about::about().0)) +pub async fn about(config: web::Data) -> Result> { + Ok(HttpResponse::Ok().content_type(ContentType::html()).body( + crate::templates::views::about::about( + &config.style.colorscheme, + &config.style.theme, + &config.style.animation, + ) + .0, + )) +} + +/// Handles the route of settings page of the `crabbysearch` meta search engine website. +#[get("/settings")] +pub async fn settings( + config: web::Data, +) -> Result> { + Ok(HttpResponse::Ok().content_type(ContentType::html()).body( + crate::templates::views::settings::settings( + &config.style.colorscheme, + &config.style.theme, + &config.style.animation, + //&config.upstream_search_engines, + )? + .0, + )) } diff --git a/src/server/routes/search.rs b/src/server/routes/search.rs index d2f34af..95013ab 100644 --- a/src/server/routes/search.rs +++ b/src/server/routes/search.rs @@ -3,14 +3,15 @@ use crate::{ cache::Cache, config::Config, - engines::Engines, models::{ - aggregation_models::SearchResults, engine_models::EngineHandler, - server_models::SearchParams, + aggregation_models::SearchResults, + engine_models::EngineHandler, + server_models::{self, SearchParams}, }, results::aggregator::aggregate, }; use actix_web::{get, http::header::ContentType, web, HttpRequest, HttpResponse}; +use std::borrow::Cow; use tokio::join; /// Handles the route of search page of the `crabbysearch` meta search engine website and it takes @@ -35,23 +36,29 @@ pub async fn search( ) -> Result> { let params = web::Query::::from_query(req.query_string())?; - if params.query.as_ref().is_some_and(|q| q.trim().is_empty()) || params.query.is_none() { - log::info!("wha!"); + if params.q.as_ref().is_some_and(|q| q.trim().is_empty()) || params.q.is_none() { return Ok(HttpResponse::TemporaryRedirect() .insert_header(("location", "/")) .finish()); } - let query = params.query.as_ref().unwrap().trim(); + let query = params.q.as_ref().unwrap().trim(); let cookie = req.cookie("appCookie"); - log::info!("{cookie:?}"); - // Get search settings using the user's cookie or from the server's config - let search_settings: crate::engines::Engines = cookie - .and_then(|cookie_value| serde_json::from_str(&cookie_value.value().to_lowercase()).ok()) - .unwrap_or_default(); + let search_settings: server_models::Cookie<'_> = cookie + .and_then(|cookie_value| serde_json::from_str(cookie_value.value()).ok()) + .unwrap_or_else(|| { + server_models::Cookie::build( + &config.style, + config + .upstream_search_engines + .iter() + .map(|e| Cow::Borrowed(e.as_str())) + .collect(), + ) + }); // Closure wrapping the results function capturing local references let get_results = |page| results(config.clone(), cache.clone(), query, page, &search_settings); @@ -101,9 +108,16 @@ pub async fn search( cache.cache_results(&results_list, &cache_keys); } - Ok(HttpResponse::Ok() - .content_type(ContentType::html()) - .body(crate::templates::views::search::search(query, &results.0).0)) + Ok(HttpResponse::Ok().content_type(ContentType::html()).body( + crate::templates::views::search::search( + &config.style.colorscheme, + &config.style.theme, + &config.style.animation, + query, + &results.0, + ) + .0, + )) } /// Fetches the results for a query and page. It First checks the redis cache, if that @@ -126,11 +140,16 @@ async fn results( cache: web::Data, query: &str, page: u32, - upstream: &Engines, + search_settings: &server_models::Cookie<'_>, ) -> Result<(SearchResults, String), Box> { // eagerly parse cookie value to evaluate safe search level - let cache_key = format!("search?q={}&page={}&engines={:?}", query, page, upstream); + let cache_key = format!( + "search?q={}&page={}&engines={}", + query, + page, + search_settings.engines.join(",") + ); // fetch the cached results json. let response = cache.cached_results(&cache_key); @@ -143,15 +162,32 @@ async fn results( // default selected upstream search engines from the config file otherwise // parse the non-empty cookie and grab the user selected engines from the // UI and use that. - let results: SearchResults = match false { - false => aggregate(query, page, config, &Vec::::from(upstream)).await?, + let mut results: SearchResults = match search_settings.engines.is_empty() { + false => { + aggregate( + query, + page, + config, + &search_settings + .engines + .iter() + .filter_map(|engine| EngineHandler::new(engine).ok()) + .collect::>(), + ) + .await? + } true => { let mut search_results = SearchResults::default(); search_results.set_no_engines_selected(); search_results } }; - + let (engine_errors_info, results_empty_check, no_engines_selected) = ( + results.engine_errors_info().is_empty(), + results.results().is_empty(), + results.no_engines_selected(), + ); + results.set_filtered(engine_errors_info & results_empty_check & !no_engines_selected); cache.cache_results(&[results.clone()], &[cache_key.clone()]); Ok((results, cache_key)) } diff --git a/src/templates/partials/bar.rs b/src/templates/partials/bar.rs index 1ad9288..a7cd39a 100644 --- a/src/templates/partials/bar.rs +++ b/src/templates/partials/bar.rs @@ -1,6 +1,6 @@ //! A module that handles `bar` partial for the `search_bar` partial and the home/index/main page in the `crabbysearch` frontend. -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; /// A functions that handles the html code for the bar for the `search_bar` partial and the /// home/index/main page in the search engine frontend. @@ -12,42 +12,12 @@ use maud::{html, Markup}; /// # Returns /// /// It returns the compiled html code for the search bar as a result. -pub fn bar(query: &str, engines: Vec<(&'static str, bool)>) -> Markup { +pub fn bar(query: &str) -> Markup { html!( - .search_container { - form action="/search" method="get" { - div class="search_bar" { - input type="search" name="query" value=(query) placeholder="Type to search"; - input type="hidden" name="page" value="1"; - button type="submit" { - img src="./images/magnifying_glass.svg" alt="Info icon for error box"; - } - } - div class="search_bar" { - .engine_selection { - @for (name, selected) in engines{ - @if selected { - .toggle_btn{ - span {(name)} - label class="switch"{ - input type="checkbox" class="engine" checked; - span class="slider round"{} - } - } - } - @else { - .toggle_btn { - (name) - label class="switch"{ - input type="checkbox" class="engine"; - span class="slider round"{} - } - } - } - } - } - } + (PreEscaped("
")) + input type="search" name="search-box" value=(query) placeholder="Type to search"; + button type="submit" onclick="searchWeb()" { + img src="./images/magnifying_glass.svg" alt="Info icon for error box"; } - } ) } diff --git a/src/templates/partials/header.rs b/src/templates/partials/header.rs index 38e4de4..b88e5cb 100644 --- a/src/templates/partials/header.rs +++ b/src/templates/partials/header.rs @@ -1,6 +1,7 @@ //! A module that handles the header for all the pages in the `crabbysearch` frontend. -use maud::{html, Markup, DOCTYPE}; +use crate::templates::partials::navbar::navbar; +use maud::{html, Markup, PreEscaped, DOCTYPE}; /// A function that handles the html code for the header for all the pages in the search engine frontend. /// @@ -12,7 +13,7 @@ use maud::{html, Markup, DOCTYPE}; /// # Returns /// /// It returns the compiled html markup code for the header as a result. -pub fn header() -> Markup { +pub fn header(colorscheme: &str, theme: &str, animation: &Option) -> Markup { html!( (DOCTYPE) html lang="en" @@ -21,12 +22,17 @@ pub fn header() -> Markup { title{"crabbysearch"} meta charset="UTF-8"; meta name="viewport" content="width=device-width, initial-scale=1"; - link href=("static/colorschemes/monokai.css") rel="stylesheet" type="text/css"; - link href=("static/themes/simple.css") rel="stylesheet" type="text/css"; + link href=(format!("static/colorschemes/{colorscheme}.css")) rel="stylesheet" type="text/css"; + link href=(format!("static/themes/{theme}.css")) rel="stylesheet" type="text/css"; + @if animation.is_some() { + link href=(format!("static/animations/{}.css", animation.as_ref().unwrap())) rel="stylesheet" type="text/css"; + } } + (PreEscaped("")) header{ h1{a href="/"{"crabbysearch"}} + (navbar()) } ) } diff --git a/src/templates/partials/mod.rs b/src/templates/partials/mod.rs index b2c71da..815909c 100644 --- a/src/templates/partials/mod.rs +++ b/src/templates/partials/mod.rs @@ -3,3 +3,6 @@ pub mod bar; pub mod footer; pub mod header; +pub mod navbar; +pub mod search_bar; +pub mod settings_tabs; diff --git a/src/templates/partials/navbar.rs b/src/templates/partials/navbar.rs new file mode 100644 index 0000000..92d6450 --- /dev/null +++ b/src/templates/partials/navbar.rs @@ -0,0 +1,19 @@ +//! A module that handles `navbar` partial for the header partial in the `crabbysearch` frontend. + +use maud::{html, Markup}; + +/// A functions that handles the html code for the header partial. +/// +/// # Returns +/// +/// It returns the compiled html code for the navbar as a result. +pub fn navbar() -> Markup { + html!( + nav{ + ul{ + li{a href="about"{"about"}} + li{a href="settings"{"settings"}} + } + } + ) +} diff --git a/src/templates/partials/search_bar.rs b/src/templates/partials/search_bar.rs new file mode 100644 index 0000000..2e7b046 --- /dev/null +++ b/src/templates/partials/search_bar.rs @@ -0,0 +1,76 @@ +//! A module that handles `search bar` partial for the search page in the `crabbysearch` frontend. + +use maud::{html, Markup, PreEscaped}; + +use crate::{models::aggregation_models::EngineErrorInfo, templates::partials::bar::bar}; + +/// A constant holding the named safe search level options for the corresponding values 0, 1 and 2. +const SAFE_SEARCH_LEVELS_NAME: [&str; 3] = ["None", "Low", "Moderate"]; + +/// A functions that handles the html code for the search bar for the search page. +/// +/// # Arguments +/// +/// * `engine_errors_info` - It takes the engine errors list containing errors for each upstream +/// search engine which failed to provide results as an argument. +/// * `safe_search_level` - It takes the safe search level with values from 0-2 as an argument. +/// * `query` - It takes the current search query provided by user as an argument. +/// +/// # Returns +/// +/// It returns the compiled html code for the search bar as a result. +pub fn search_bar( + engine_errors_info: &[EngineErrorInfo], + safe_search_level: u8, + query: &str, +) -> Markup { + html!( + .search_area{ + (bar(query)) + .error_box { + @if !engine_errors_info.is_empty(){ + button onclick="toggleErrorBox()" class="error_box_toggle_button"{ + img src="./images/warning.svg" alt="Info icon for error box"; + } + .dropdown_error_box{ + @for errors in engine_errors_info{ + .error_item{ + span class="engine_name"{(errors.engine)} + span class="engine_name"{(errors.error)} + span class="severity_color" style="background: {{{this.severity_color}}};"{} + } + } + } + } + @else { + button onclick="toggleErrorBox()" class="error_box_toggle_button"{ + img src="./images/info.svg" alt="Warning icon for error box"; + } + .dropdown_error_box { + .no_errors{ + "Everything looks good 🙂!!" + } + } + } + } + (PreEscaped("
")) + .search_options { + @if safe_search_level >= 3 { + (PreEscaped("")) + } + @for (idx, name) in SAFE_SEARCH_LEVELS_NAME.iter().enumerate() { + @if (safe_search_level as usize) == idx { + option value=(idx) selected {(format!("SafeSearch: {name}"))} + } + @else{ + option value=(idx) {(format!("SafeSearch: {name}"))} + } + } + (PreEscaped("")) + } + } + ) +} diff --git a/src/templates/partials/settings_tabs/cookies.rs b/src/templates/partials/settings_tabs/cookies.rs new file mode 100644 index 0000000..f17185b --- /dev/null +++ b/src/templates/partials/settings_tabs/cookies.rs @@ -0,0 +1,25 @@ +//! A module that handles the engines tab for setting page view in the `crabbysearch` frontend. + +use maud::{html, Markup}; + +/// A functions that handles the html code for the cookies tab for the settings page for the search page. +/// +/// # Returns +/// +/// It returns the compiled html markup code for the cookies tab. +pub fn cookies() -> Markup { + html!( + div class="cookies tab"{ + h1{"Cookies"} + p class="description"{ + "This is the cookies are saved on your system and it contains the preferences + you chose in the settings page" + } + input type="text" name="cookie_field" value="" readonly; + p class="description"{ + "The cookies stored are not used by us for any malicious intend or for + tracking you in any way." + } + } + ) +} diff --git a/src/templates/partials/settings_tabs/engines.rs b/src/templates/partials/settings_tabs/engines.rs new file mode 100644 index 0000000..a37f9dd --- /dev/null +++ b/src/templates/partials/settings_tabs/engines.rs @@ -0,0 +1,74 @@ +//! A module that handles the engines tab for setting page view in the `crabbysearch` frontend. + +use std::collections::HashMap; + +use maud::{html, Markup}; + +/// A functions that handles the html code for the engines tab for the settings page for the search page. +/// +/// # Arguments +/// +/// * `engine_names` - It takes the key value pair list of all available engine names and there corresponding +/// selected (enabled/disabled) value as an argument. +/// +/// # Returns +/// +/// It returns the compiled html markup code for the engines tab. +pub fn engines(engine_names: &HashMap) -> Markup { + html!( + div class="engines tab"{ + h1{"Engines"} + h3{"select search engines"} + p class="description"{ + "Select the search engines from the list of engines that you want results from" + } + .engine_selection{ + // Checks whether all the engines are selected or not if they are then the + // checked `select_all` button is rendered otherwise the unchecked version + // is rendered. + @if engine_names.values().all(|selected| *selected){ + .toggle_btn{ + label class="switch"{ + input type="checkbox" class="select_all" onchange="toggleAllSelection()" checked; + span class="slider round"{} + } + "Select All" + } + } + @else{ + .toggle_btn { + label class="switch"{ + input type="checkbox" class="select_all" onchange="toggleAllSelection()"; + span class="slider round"{} + } + "Select All" + } + } + hr; + @for (engine_name, selected) in engine_names{ + // Checks whether the `engine_name` is selected or not if they are then the + // checked `engine` button is rendered otherwise the unchecked version is + // rendered. + @if *selected { + .toggle_btn{ + label class="switch"{ + input type="checkbox" class="engine" checked; + span class="slider round"{} + } + (format!("{}{}",&engine_name[..1].to_uppercase(), &engine_name[1..])) + } + } + @else { + .toggle_btn { + label class="switch"{ + input type="checkbox" class="engine"; + span class="slider round"{} + } + (format!("{}{}",&engine_name[..1], &engine_name[1..])) + } + } + } + } + } + ) +} diff --git a/src/templates/partials/settings_tabs/mod.rs b/src/templates/partials/settings_tabs/mod.rs new file mode 100644 index 0000000..b8f42dc --- /dev/null +++ b/src/templates/partials/settings_tabs/mod.rs @@ -0,0 +1,6 @@ +//! This module provides other modules to handle the partials for the tabs for the settings page +//! view in the `crabbysearch` frontend. + +pub mod cookies; +pub mod engines; +pub mod user_interface; diff --git a/src/templates/partials/settings_tabs/user_interface.rs b/src/templates/partials/settings_tabs/user_interface.rs new file mode 100644 index 0000000..b191a55 --- /dev/null +++ b/src/templates/partials/settings_tabs/user_interface.rs @@ -0,0 +1,96 @@ +//! A module that handles the user interface tab for setting page view in the `crabbysearch` frontend. + +use crate::handler::{file_path, FileType}; +use maud::{html, Markup}; +use std::fs::read_dir; + +/// A helper function that helps in building the list of all available colorscheme/theme/animation +/// names present in the colorschemes, animations and themes folder respectively by excluding the +/// ones that have already been selected via the config file. +/// +/// # Arguments +/// +/// * `style_type` - It takes the style type of the values `theme` and `colorscheme` as an +/// argument. +/// * `selected_style` - It takes the currently selected style value provided via the config file +/// as an argument. +/// +/// # Error +/// +/// Returns a list of colorscheme/theme names as a vector of tuple strings on success otherwise +/// returns a standard error message. +fn style_option_list( + style_type: &str, + selected_style: &str, +) -> Result, Box> { + let mut style_option_names: Vec<(String, String)> = Vec::new(); + for file in read_dir(format!( + "{}static/{}/", + file_path(FileType::Theme)?, + style_type, + ))? { + let style_name = file?.file_name().to_str().unwrap().replace(".css", ""); + if selected_style != style_name { + style_option_names.push((style_name.clone(), style_name.replace('-', " "))); + } + } + + if style_type == "animations" { + style_option_names.push((String::default(), "none".to_owned())) + } + + Ok(style_option_names) +} + +/// A functions that handles the html code for the user interface tab for the settings page for the search page. +/// +/// # Error +/// +/// It returns the compiled html markup code for the user interface tab on success otherwise +/// returns a standard error message. +pub fn user_interface( + theme: &str, + colorscheme: &str, + animation: &Option, +) -> Result> { + Ok(html!( + div class="user_interface tab"{ + h1{"User Interface"} + h3{"select theme"} + p class="description"{ + "Select the theme from the available themes to be used in user interface" + } + select name="themes"{ + // Sets the user selected theme name from the config file as the first option in the selection list. + option value=(theme){(theme.replace('-', " "))} + @for (k,v) in style_option_list("themes", theme)?{ + option value=(k){(v)} + } + } + h3{"select color scheme"} + p class="description"{ + "Select the color scheme for your theme to be used in user interface" + } + select name="colorschemes"{ + // Sets the user selected colorscheme name from the config file as the first option in the selection list. + option value=(colorscheme){(colorscheme.replace('-', " "))} + @for (k,v) in style_option_list("colorschemes", colorscheme)?{ + option value=(k){(v)} + } + } + h3{"select animation"} + p class="description"{ + "Select the animation for your theme to be used in user interface" + } + select name="animations"{ + @let default_animation = &String::default(); + @let animation = animation.as_ref().unwrap_or(default_animation); + // Sets the user selected animation name from the config file as the first option in the selection list. + option value=(animation){(animation.replace('-'," "))} + @for (k,v) in style_option_list("animations", animation)?{ + option value=(k){(v)} + } + } + } + )) +} diff --git a/src/templates/views/about.rs b/src/templates/views/about.rs index 0de3e91..ccc4c4d 100644 --- a/src/templates/views/about.rs +++ b/src/templates/views/about.rs @@ -14,7 +14,16 @@ use crate::templates::partials::{footer::footer, header::header}; /// # Returns /// /// It returns the compiled html markup code as a result. -pub fn about() -> Markup { +pub fn about(colorscheme: &str, theme: &str, animation: &Option) -> Markup { + let logo_svg = r#" + + + + + + + + "#; let feature_lightning = r#" "#; @@ -34,9 +43,12 @@ pub fn about() -> Markup { "#; html!( - (header()) + (header(colorscheme, theme, animation)) main class="about-container"{ article { + div class="logo-container" { + (PreEscaped(logo_svg)) + } div class="text-block" { h3 class="text-block-title" {"Why crabbysearch?"} diff --git a/src/templates/views/index.rs b/src/templates/views/index.rs index ac2a807..bb8a9f0 100644 --- a/src/templates/views/index.rs +++ b/src/templates/views/index.rs @@ -1,6 +1,6 @@ //! A module that handles the view for the index/home/main page in the `crabbysearch` frontend. -use maud::{html, Markup}; +use maud::{html, Markup, PreEscaped}; use crate::templates::partials::{bar::bar, footer::footer, header::header}; @@ -14,8 +14,14 @@ use crate::templates::partials::{bar::bar, footer::footer, header::header}; /// # Returns /// /// It returns the compiled html markup code as a result. -pub fn index() -> Markup { - html!((header())(bar(&String::default(), vec![("bing", true)]))( - footer() - )) +pub fn index(colorscheme: &str, theme: &str, animation: &Option) -> Markup { + html!( + (header(colorscheme, theme, animation)) + main class="search-container"{ + (bar(&String::default())) + (PreEscaped("")) + } + script src="static/index.js"{} + (footer()) + ) } diff --git a/src/templates/views/mod.rs b/src/templates/views/mod.rs index abfc190..a68e9bc 100644 --- a/src/templates/views/mod.rs +++ b/src/templates/views/mod.rs @@ -5,3 +5,4 @@ pub mod about; pub mod index; pub mod not_found; pub mod search; +pub mod settings; diff --git a/src/templates/views/not_found.rs b/src/templates/views/not_found.rs index f5730a4..f893cc9 100644 --- a/src/templates/views/not_found.rs +++ b/src/templates/views/not_found.rs @@ -13,9 +13,9 @@ use maud::{html, Markup}; /// # Returns /// /// It returns the compiled html markup code as a result. -pub fn not_found() -> Markup { +pub fn not_found(colorscheme: &str, theme: &str, animation: &Option) -> Markup { html!( - (header()) + (header(colorscheme, theme, animation)) main class="error_container"{ img src="images/robot-404.svg" alt="Image of broken robot."; .error_content{ diff --git a/src/templates/views/search.rs b/src/templates/views/search.rs index 254025c..d38217d 100644 --- a/src/templates/views/search.rs +++ b/src/templates/views/search.rs @@ -4,7 +4,7 @@ use maud::{html, Markup, PreEscaped}; use crate::{ models::aggregation_models::SearchResults, - templates::partials::{bar::bar, footer::footer, header::header}, + templates::partials::{footer::footer, header::header, search_bar::search_bar}, }; /// A function that handles the html code for the search page view in the search engine frontend. @@ -19,11 +19,17 @@ use crate::{ /// # Returns /// /// It returns the compiled html markup code as a result. -pub fn search(query: &str, search_results: &SearchResults) -> Markup { +pub fn search( + colorscheme: &str, + theme: &str, + animation: &Option, + query: &str, + search_results: &SearchResults, +) -> Markup { html!( - (header()) + (header(colorscheme, theme, animation)) main class="results"{ - (bar(query, vec![("Bing", true)])) + (search_bar(&search_results.engine_errors_info, search_results.safe_search_level, query)) .results_aggregated{ @if !search_results.results.is_empty() { @for result in search_results.results.iter(){ @@ -39,6 +45,40 @@ pub fn search(query: &str, search_results: &SearchResults) -> Markup { } } } + @else if search_results.disallowed{ + .result_disallowed{ + .description{ + p{ + "Your search - "{span class="user_query"{(query)}}" - + has been disallowed." + } + p class="description_paragraph"{"Dear user,"} + p class="description_paragraph"{ + "The query - "{span class="user_query"{(query)}}" - has + been blacklisted via server configuration and hence disallowed by the + server. Henceforth no results could be displayed for your query." + } + } + img src="./images/barricade.png" alt="Image of a Barricade"; + } + } + @else if search_results.filtered { + .result_filtered{ + .description{ + p{ + "Your search - "{span class="user_query"{(query)}}" - + has been filtered." + } + p class="description_paragraph"{"Dear user,"} + p class="description_paragraph"{ + "All the search results contain results that has been configured to be + filtered out via server configuration and henceforth has been + completely filtered out." + } + } + img src="./images/filter.png" alt="Image of a paper inside a funnel"; + } + } @else if search_results.no_engines_selected { .result_engine_not_selected{ .description{ diff --git a/src/templates/views/settings.rs b/src/templates/views/settings.rs new file mode 100644 index 0000000..b293e84 --- /dev/null +++ b/src/templates/views/settings.rs @@ -0,0 +1,58 @@ +//! A module that handles the view for the settings page in the `crabbysearch` frontend. + +use std::collections::HashMap; + +use maud::{html, Markup}; + +use crate::templates::partials::{ + footer::footer, + header::header, + settings_tabs::{cookies::cookies, engines::engines, user_interface::user_interface}, +}; + +/// A function that handles the html code for the settings page view in the search engine frontend. +/// +/// # Arguments +/// +/// * `safe_search_level` - It takes the safe search level as an argument. +/// * `colorscheme` - It takes the colorscheme name as an argument. +/// * `theme` - It takes the theme name as an argument. +/// * `animation` - It takes the animation name as an argument. +/// * `engine_names` - It takes a list of engine names as an argument. +/// +/// # Error +/// +/// This function returns a compiled html markup code on success otherwise returns a standard error +/// message. +pub fn settings( + colorscheme: &str, + theme: &str, + animation: &Option, + //engine_names: &HashMap, +) -> Result> { + Ok(html!( + (header(colorscheme, theme, animation)) + main class="settings"{ + h1{"Settings"} + hr; + .settings_container{ + .sidebar{ + div class="btn active" onclick="setActiveTab(this)"{"general"} + .btn onclick="setActiveTab(this)"{"user interface"} + .btn onclick="setActiveTab(this)"{"engines"} + .btn onclick="setActiveTab(this)"{"cookies"} + } + .main_container{ + (user_interface(theme, colorscheme, animation)?) + //(engines(engine_names)) + (cookies()) + p class="message"{} + button type="submit" onclick="setClientSettings()"{"Save"} + } + } + } + script src="static/settings.js"{} + script src="static/cookies.js"{} + (footer()) + )) +}