add settings page

This commit is contained in:
mat 2024-06-29 04:09:31 -05:00
parent cd2827b9fc
commit 6da140f34b
11 changed files with 584 additions and 272 deletions

124
Cargo.lock generated
View File

@ -205,6 +205,30 @@ dependencies = [
"sync_wrapper",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733"
dependencies = [
"axum",
"axum-core",
"bytes",
"cookie",
"futures-util",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"mime",
"pin-project-lite",
"serde",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
@ -354,6 +378,17 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "cookie"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
@ -421,6 +456,15 @@ dependencies = [
"syn 2.0.52",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@ -743,6 +787,15 @@ dependencies = [
"unicode-segmentation",
]
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "html5ever"
version = "0.26.0"
@ -1083,6 +1136,7 @@ dependencies = [
"ammonia",
"async-stream",
"axum",
"axum-extra",
"base64 0.22.0",
"bytes",
"chrono",
@ -1090,6 +1144,7 @@ dependencies = [
"eyre",
"fend-core",
"futures",
"html-escape",
"maud",
"numbat",
"rand",
@ -1101,6 +1156,8 @@ dependencies = [
"tokio",
"tokio-stream",
"toml",
"tower",
"tower-http",
"tracing",
"tracing-subscriber",
"url",
@ -1160,6 +1217,12 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-format"
version = "0.4.4"
@ -1425,6 +1488,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
@ -2095,6 +2164,37 @@ dependencies = [
"once_cell",
]
[[package]]
name = "time"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"num-conv",
"powerfmt",
"serde",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
@ -2219,6 +2319,23 @@ dependencies = [
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-http"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
dependencies = [
"bitflags 2.4.2",
"bytes",
"http 1.0.0",
"http-body 1.0.0",
"http-body-util",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
@ -2239,6 +2356,7 @@ version = "0.1.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef"
dependencies = [
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@ -2373,6 +2491,12 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf8-width"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3"
[[package]]
name = "valuable"
version = "0.1.0"

View File

@ -9,13 +9,8 @@ build = "src/build.rs"
[dependencies]
ammonia = "3.3.0"
async-stream = "0.3.5"
axum = { version = "0.7.4", default-features = false, features = [
"tokio",
"http1",
"http2",
"query",
"json",
] }
axum = { version = "0.7.4", default-features = false, features = ["tokio", "http1", "http2", "query", "json", "form"] }
axum-extra = { version = "0.9.3", features = ["cookie"] }
base64 = "0.22.0"
bytes = "1.5.0"
chrono = "0.4.35"
@ -23,6 +18,7 @@ chrono-tz = { version = "0.8.6", features = ["case-insensitive"] }
eyre = "0.6.12"
fend-core = "1.4.5"
futures = "0.3.30"
html-escape = "0.2.13"
maud = "0.26.0"
numbat = "1.11.0"
rand = "0.8.5"
@ -40,6 +36,8 @@ serde_json = { version = "1.0.114", features = ["preserve_order"] }
tokio = { version = "1.36.0", features = ["rt", "macros"] }
tokio-stream = "0.1.15"
toml = { version = "0.8.12", default-features = false, features = ["parse"] }
tower = "0.4.13"
tower-http = "0.5.2"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
url = "2.5.0"

View File

@ -1,18 +1,25 @@
use std::{collections::HashMap, fs, net::SocketAddr, path::Path, sync::LazyLock};
use std::{
collections::HashMap,
fs,
net::SocketAddr,
path::Path,
sync::{Arc, LazyLock},
};
use serde::Deserialize;
use tracing::info;
use crate::engines::Engine;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct Config {
pub bind: SocketAddr,
/// Whether the JSON API should be accessible.
pub api: bool,
pub ui: UiConfig,
pub image_search: ImageSearchConfig,
pub engines: EnginesConfig,
// wrapped in an arc to make Config cheaper to clone
pub engines: Arc<EnginesConfig>,
}
#[derive(Deserialize, Debug)]
@ -31,23 +38,31 @@ impl Config {
self.ui.overlay(partial.ui.unwrap_or_default());
self.image_search
.overlay(partial.image_search.unwrap_or_default());
self.engines.overlay(partial.engines.unwrap_or_default());
if let Some(partial_engines) = partial.engines {
let mut engines = self.engines.as_ref().clone();
engines.overlay(partial_engines);
self.engines = Arc::new(engines);
}
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct UiConfig {
pub show_engine_list_separator: bool,
pub show_version_info: bool,
/// Settings are always accessible anyways, this just controls whether the
/// link to them in the index page is visible.
pub show_settings_link: bool,
pub site_name: String,
pub stylesheet_url: Option<String>,
pub stylesheet_str: Option<String>,
pub stylesheet_url: String,
pub stylesheet_str: String,
}
#[derive(Deserialize, Debug, Default)]
pub struct PartialUiConfig {
pub show_engine_list_separator: Option<bool>,
pub show_version_info: Option<bool>,
pub show_settings_link: Option<bool>,
pub site_name: Option<String>,
pub stylesheet_url: Option<String>,
pub stylesheet_str: Option<String>,
@ -59,13 +74,20 @@ impl UiConfig {
.show_engine_list_separator
.unwrap_or(self.show_engine_list_separator);
self.show_version_info = partial.show_version_info.unwrap_or(self.show_version_info);
self.show_settings_link = partial
.show_settings_link
.unwrap_or(self.show_settings_link);
self.site_name = partial.site_name.unwrap_or(self.site_name.clone());
self.stylesheet_url = partial.stylesheet_url.or(self.stylesheet_url.clone());
self.stylesheet_str = partial.stylesheet_str.or(self.stylesheet_str.clone());
self.stylesheet_url = partial
.stylesheet_url
.unwrap_or(self.stylesheet_url.clone());
self.stylesheet_str = partial
.stylesheet_str
.unwrap_or(self.stylesheet_str.clone());
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ImageSearchConfig {
pub enabled: bool,
pub show_engines: bool,
@ -87,7 +109,7 @@ impl ImageSearchConfig {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct ImageProxyConfig {
/// Whether we should proxy remote images through our server. This is mostly
/// a privacy feature.
@ -109,7 +131,7 @@ impl ImageProxyConfig {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct EnginesConfig {
pub map: HashMap<Engine, EngineConfig>,
}
@ -152,7 +174,7 @@ impl EnginesConfig {
}
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct EngineConfig {
pub enabled: bool,
/// The priority of this engine relative to the other engines.
@ -206,8 +228,9 @@ impl Default for Config {
show_engine_list_separator: false,
show_version_info: false,
site_name: "metasearch".to_string(),
stylesheet_url: None,
stylesheet_str: None,
show_settings_link: true,
stylesheet_url: "".to_string(),
stylesheet_str: "".to_string(),
},
image_search: ImageSearchConfig {
enabled: false,
@ -217,7 +240,7 @@ impl Default for Config {
max_download_size: 10_000_000,
},
},
engines: EnginesConfig::default(),
engines: Arc::new(EnginesConfig::default()),
}
}
}

View File

@ -1,15 +1,16 @@
const searchInputEl = document.getElementById("search-input");
// add an element with search suggestions after the search input
const suggestionsEl = document.createElement("div");
suggestionsEl.id = "search-input-suggestions";
suggestionsEl.style.visibility = "hidden";
searchInputEl.insertAdjacentElement("afterend", suggestionsEl);
if (searchInputEl) {
// add an element with search suggestions after the search input
const suggestionsEl = document.createElement("div");
suggestionsEl.id = "search-input-suggestions";
suggestionsEl.style.visibility = "hidden";
searchInputEl.insertAdjacentElement("afterend", suggestionsEl);
let lastValue = "";
let nextQueryId = 0;
let lastLoadedQueryId = -1;
async function updateSuggestions() {
let lastValue = "";
let nextQueryId = 0;
let lastLoadedQueryId = -1;
async function updateSuggestions() {
const value = searchInputEl.value;
if (value === "") {
@ -28,9 +29,9 @@ async function updateSuggestions() {
const thisQueryId = nextQueryId;
nextQueryId++;
const res = await fetch(`/autocomplete?q=${encodeURIComponent(value)}`).then(
(res) => res.json()
);
const res = await fetch(
`/autocomplete?q=${encodeURIComponent(value)}`
).then((res) => res.json());
const options = res[1];
// this makes sure we don't load suggestions out of order
@ -40,9 +41,9 @@ async function updateSuggestions() {
lastLoadedQueryId = thisQueryId;
renderSuggestions(options);
}
}
function renderSuggestions(options) {
function renderSuggestions(options) {
if (options.length === 0) {
suggestionsEl.style.visibility = "hidden";
return;
@ -62,28 +63,28 @@ function renderSuggestions(options) {
searchInputEl.form.submit();
});
});
}
}
let focusedSuggestionIndex = -1;
let focusedSuggestionEl = null;
let focusedSuggestionIndex = -1;
let focusedSuggestionEl = null;
function clearFocusedSuggestion() {
function clearFocusedSuggestion() {
if (focusedSuggestionEl) {
focusedSuggestionEl.classList.remove("focused");
focusedSuggestionEl = null;
focusedSuggestionIndex = -1;
}
}
}
function focusSelectionIndex(index) {
function focusSelectionIndex(index) {
clearFocusedSuggestion();
focusedSuggestionIndex = index;
focusedSuggestionEl = suggestionsEl.children[focusedSuggestionIndex];
focusedSuggestionEl.classList.add("focused");
searchInputEl.value = focusedSuggestionEl.textContent;
}
}
document.addEventListener("keydown", (e) => {
document.addEventListener("keydown", (e) => {
// if it's focused then use different keybinds
if (searchInputEl.matches(":focus")) {
if (e.key === "ArrowDown") {
@ -150,16 +151,53 @@ document.addEventListener("keydown", (e) => {
searchInputEl.value.length
);
}
});
});
// update the input suggestions on input
searchInputEl.addEventListener("input", () => {
// update the input suggestions on input
searchInputEl.addEventListener("input", () => {
clearFocusedSuggestion();
updateSuggestions();
});
// and when they click suggestions
searchInputEl.addEventListener("click", updateSuggestions);
// on unfocus hide the suggestions
searchInputEl.addEventListener("blur", (e) => {
});
// and when they click suggestions
searchInputEl.addEventListener("click", updateSuggestions);
// on unfocus hide the suggestions
searchInputEl.addEventListener("blur", (e) => {
suggestionsEl.style.visibility = "hidden";
});
});
}
const customCssEl = document.getElementById("custom-css");
if (customCssEl) {
// tab to indent
// https://stackoverflow.com/a/6637396
customCssEl.addEventListener("keydown", (e) => {
if (e.key == "Tab") {
e.preventDefault();
var start = customCssEl.selectionStart;
var end = customCssEl.selectionEnd;
customCssEl.value =
customCssEl.value.substring(0, start) +
"\t" +
customCssEl.value.substring(end);
customCssEl.selectionStart = customCssEl.selectionEnd = start + 1;
}
});
// ctrl+enter anywhere on the page to submit
const saveEl = document.getElementById("save-settings-button");
document.addEventListener("keydown", (e) => {
if (e.key == "Enter" && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
console.log("click");
saveEl.click();
}
});
// save whether the details are open or not
const customCssDetailsEl = document.getElementById("custom-css-details");
const customCssDetailsOpen = localStorage.getItem("custom-css-details-open");
if (customCssDetailsOpen === "true") customCssDetailsEl.open = true;
customCssDetailsEl.addEventListener("toggle", () => {
localStorage.setItem("custom-css-details-open", customCssDetailsEl.open);
});
}

View File

@ -40,12 +40,19 @@ body {
line-height: 1.2;
height: 100%;
}
.settings-link {
position: absolute;
top: 1em;
right: 1em;
}
.version-info {
position: absolute;
bottom: 16px;
right: 16px;
bottom: 1em;
right: 1em;
}
.results-container {
.main-container {
/* enough space for the infobox */
max-width: 73.5rem;
margin: 0 auto;
@ -62,17 +69,19 @@ main {
/* image search uses 100% width */
max-width: 100%;
}
.results-container.search-images {
.main-container.search-images {
max-width: none;
}
@media screen and (max-width: 74rem) {
/* small screens */
.results-container {
.main-container {
margin: 0 auto;
max-width: 40rem;
}
}
input {
input,
textarea,
select {
font-family: monospace;
background-color: var(--bg-2);
color: var(--fg-1);
@ -107,7 +116,7 @@ blockquote {
}
/* index page */
.main-container {
.main-container.index-page {
display: flex;
flex-direction: column;
min-height: 100%;
@ -122,6 +131,22 @@ h1 {
margin-top: 0;
}
/* settings page */
.settings-page .back-to-index-button {
bottom: 0.5em;
position: relative;
}
.settings-form select {
display: block;
}
#save-settings-button {
margin-top: 1em;
display: block;
}
#custom-css {
tab-size: 2;
}
/* header */
.search-form {
margin-bottom: 1rem;

View File

@ -1,18 +1,13 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use axum::{
extract::{Query, State},
http::StatusCode,
response::IntoResponse,
Json,
};
use axum::{extract::Query, http::StatusCode, response::IntoResponse, Extension, Json};
use tracing::error;
use crate::{config::Config, engines};
pub async fn route(
Query(params): Query<HashMap<String, String>>,
State(config): State<Arc<Config>>,
Extension(config): Extension<Config>,
) -> impl IntoResponse {
let query = params
.get("q")

View File

@ -1,9 +1,10 @@
use std::{collections::HashMap, sync::Arc};
use std::collections::HashMap;
use axum::{
extract::{Query, State},
extract::Query,
http::StatusCode,
response::{IntoResponse, Response},
Extension,
};
use tracing::error;
@ -11,7 +12,7 @@ use crate::{config::Config, engines};
pub async fn route(
Query(params): Query<HashMap<String, String>>,
State(config): State<Arc<Config>>,
Extension(config): Extension<Config>,
) -> Response {
let image_search_config = &config.image_search;
let proxy_config = &image_search_config.proxy;

View File

@ -1,46 +1,32 @@
use std::sync::Arc;
use axum::{extract::State, http::header, response::IntoResponse};
use axum::{http::header, response::IntoResponse, Extension};
use maud::{html, PreEscaped, DOCTYPE};
use crate::config::Config;
use crate::{config::Config, web::head_html};
const BASE_COMMIT_URL: &str = "https://github.com/mat-1/metasearch2/commit/";
const VERSION: &str = std::env!("CARGO_PKG_VERSION");
const COMMIT_HASH: &str = std::env!("GIT_HASH");
const COMMIT_HASH_SHORT: &str = std::env!("GIT_HASH_SHORT");
pub async fn index(State(config): State<Arc<Config>>) -> impl IntoResponse {
let mut html = String::new();
html.push_str(
&html! {
pub async fn get(Extension(config): Extension<Config>) -> impl IntoResponse {
let html = html! {
(PreEscaped("<!-- source code: https://github.com/mat-1/metasearch2 -->\n"))
(DOCTYPE)
html lang="en" {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title { {(config.ui.site_name)} }
link rel="stylesheet" href="/style.css";
@if let Some(stylesheet_url) = &config.ui.stylesheet_url {
link rel="stylesheet" href=(stylesheet_url);
}
@if let Some(stylesheet_str) = &config.ui.stylesheet_str {
link rel="stylesheet" href=(stylesheet_str);
}
script src="/script.js" defer {}
link rel="search" type="application/opensearchdescription+xml" title="metasearch" href="/opensearch.xml";
}
{(head_html(None, &config))}
body {
div."main-container" {
@if config.ui.show_settings_link {
a.settings-link href="/settings" { "Settings" }
}
div.main-container.index-page {
h1 { {(config.ui.site_name)} }
form."search-form" action="/search" method="get" {
form.search-form action="/search" method="get" {
input type="text" name="q" placeholder="Search" id="search-input" autofocus onfocus="this.select()" autocomplete="off";
input type="submit" value="Search";
}
}
@if config.ui.show_version_info {
span."version-info" {
span.version-info {
@if COMMIT_HASH == "unknown" || COMMIT_HASH_SHORT == "unknown" {
"Version "
(VERSION)
@ -57,8 +43,7 @@ pub async fn index(State(config): State<Arc<Config>>) -> impl IntoResponse {
}
}
.into_string(),
);
.into_string();
([(header::CONTENT_TYPE, "text/html; charset=utf-8")], html)
}

View File

@ -1,16 +1,22 @@
pub mod autocomplete;
mod autocomplete;
mod image_proxy;
pub mod index;
pub mod opensearch;
pub mod search;
mod index;
mod opensearch;
mod search;
mod settings;
use std::{convert::Infallible, net::SocketAddr, sync::Arc};
use axum::{
http::header,
routing::{get, MethodRouter},
extract::{Request, State},
http::{header, StatusCode},
middleware::{self, Next},
response::Response,
routing::{get, post, MethodRouter},
Router,
};
use axum_extra::extract::CookieJar;
use maud::{html, Markup, PreEscaped};
use tracing::info;
use crate::config::Config;
@ -18,6 +24,8 @@ use crate::config::Config;
pub async fn run(config: Config) {
let bind_addr = config.bind;
let config = Arc::new(config);
fn static_route<S>(
content: &'static str,
content_type: &'static str,
@ -30,7 +38,10 @@ pub async fn run(config: Config) {
}
let app = Router::new()
.route("/", get(index::index))
.route("/", get(index::get))
.route("/search", get(search::get))
.route("/settings", get(settings::get))
.route("/settings", post(settings::post))
.route(
"/style.css",
static_route(include_str!("assets/style.css"), "text/css; charset=utf-8"),
@ -57,10 +68,13 @@ pub async fn run(config: Config) {
),
)
.route("/opensearch.xml", get(opensearch::route))
.route("/search", get(search::route))
.route("/autocomplete", get(autocomplete::route))
.route("/image-proxy", get(image_proxy::route))
.with_state(Arc::new(config));
.layer(middleware::from_fn_with_state(
config.clone(),
config_middleware,
))
.with_state(config);
info!("Listening on http://{bind_addr}");
@ -72,3 +86,52 @@ pub async fn run(config: Config) {
.await
.unwrap();
}
async fn config_middleware(
State(config): State<Arc<Config>>,
cookies: CookieJar,
mut req: Request,
next: Next,
) -> Result<Response, StatusCode> {
let mut config = config.clone().as_ref().clone();
fn set_from_cookie(config: &mut String, cookies: &CookieJar, name: &str) {
if let Some(cookie) = cookies.get(name) {
let value = cookie.value();
*config = value.to_string();
}
}
set_from_cookie(&mut config.ui.stylesheet_url, &cookies, "stylesheet-url");
set_from_cookie(&mut config.ui.stylesheet_str, &cookies, "stylesheet-str");
// modify the state
req.extensions_mut().insert(config);
Ok(next.run(req).await)
}
pub fn head_html(title: Option<&str>, config: &Config) -> Markup {
html! {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title {
@if let Some(title) = title {
{ (title) }
{ " - " }
}
{(config.ui.site_name)}
}
link rel="stylesheet" href="/style.css";
@if !config.ui.stylesheet_url.is_empty() {
link rel="stylesheet" href=(config.ui.stylesheet_url);
}
@if !config.ui.stylesheet_str.is_empty() {
style { (PreEscaped(html_escape::encode_style(&config.ui.stylesheet_str))) }
}
script src="/script.js" defer {}
link rel="search" type="application/opensearchdescription+xml" title="metasearch" href="/opensearch.xml";
}
}
}

View File

@ -1,15 +1,15 @@
mod all;
mod images;
use std::{collections::HashMap, net::SocketAddr, str::FromStr, sync::Arc};
use std::{collections::HashMap, net::SocketAddr, str::FromStr};
use async_stream::stream;
use axum::{
body::Body,
extract::{ConnectInfo, Query, State},
extract::{ConnectInfo, Query},
http::{header, HeaderMap, StatusCode},
response::IntoResponse,
Json,
Extension, Json,
};
use bytes::Bytes;
use maud::{html, PreEscaped, DOCTYPE};
@ -20,29 +20,10 @@ use crate::{
self, Engine, EngineProgressUpdate, ProgressUpdateData, ResponseForTab, SearchQuery,
SearchTab,
},
web::head_html,
};
fn render_beginning_of_html(search: &SearchQuery) -> String {
let head_html = html! {
head {
meta charset="UTF-8";
meta name="viewport" content="width=device-width, initial-scale=1.0";
title {
(search.query)
" - "
(search.config.ui.site_name)
}
link rel="stylesheet" href="/style.css";
@if let Some(stylesheet_url) = &search.config.ui.stylesheet_url {
link rel="stylesheet" href=(stylesheet_url);
}
@if let Some(stylesheet_str) = &search.config.ui.stylesheet_str {
link rel="stylesheet" href=(stylesheet_str);
}
script src="/script.js" defer {}
link rel="search" type="application/opensearchdescription+xml" title="metasearch" href="/opensearch.xml";
}
};
let form_html = html! {
form."search-form" action="/search" method="get" {
input #"search-input" type="text" name="q" placeholder="Search" value=(search.query) autofocus onfocus="this.select()" autocomplete="off";
@ -62,9 +43,9 @@ fn render_beginning_of_html(search: &SearchQuery) -> String {
html! {
(DOCTYPE)
html lang="en";
(head_html)
{(head_html(Some(&search.query), &search.config))}
body;
div.results-container.{"search-" (search.tab.to_string())};
div.main-container.{"search-" (search.tab.to_string())};
main;
(form_html)
div.progress-updates;
@ -131,9 +112,9 @@ pub fn render_engine_list(engines: &[engines::Engine], config: &Config) -> PreEs
}
}
pub async fn route(
pub async fn get(
Query(params): Query<HashMap<String, String>>,
State(config): State<Arc<Config>>,
Extension(config): Extension<Config>,
headers: HeaderMap,
ConnectInfo(addr): ConnectInfo<SocketAddr>,
) -> axum::response::Response {
@ -182,7 +163,7 @@ pub async fn route(
|| addr.ip().to_string(),
|ip| ip.to_str().unwrap_or_default().to_string(),
),
config: config.clone(),
config: config.clone().into(),
};
let trying_to_use_api =

79
src/web/settings.rs Normal file
View File

@ -0,0 +1,79 @@
use axum::{ http::{header, StatusCode}, response::IntoResponse, Extension, Form};
use axum_extra::extract::{cookie::Cookie, CookieJar};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use serde::Deserialize;
use crate::{config::Config, web::head_html};
pub async fn get(
Extension(config): Extension<Config>,
) -> impl IntoResponse {
let theme_option = |value: &str, name: &str| -> Markup {
let selected = config.ui.stylesheet_url == value;
html! {
option value=(value) selected[selected] {
{ (name) }
}
}
};
let html = html! {
(PreEscaped("<!-- source code: https://github.com/mat-1/metasearch2 -->\n"))
(DOCTYPE)
html lang="en" {
{(head_html(Some("settings"), &config))}
body {
div.main-container.settings-page {
main {
a.back-to-index-button href="/" { "Back" }
h1 { "Settings" }
form.settings-form method="post" {
label for="theme" { "Theme" }
select name="stylesheet-url" selected=(config.ui.stylesheet_url) {
{ (theme_option("", "Ayu Dark")) }
{ (theme_option("/themes/catppuccin-mocha.css", "Catppuccin Mocha")) }
}
br;
// custom css textarea
details #custom-css-details {
summary { "Custom CSS" }
textarea name="stylesheet-str" id="custom-css" rows="10" cols="50" {
{ (config.ui.stylesheet_str) }
}
}
input #save-settings-button type="submit" value="Save";
}
}
}
}
}
}
.into_string();
( [(header::CONTENT_TYPE, "text/html; charset=utf-8")], html)
}
#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Settings {
stylesheet_url: String,
stylesheet_str: String,
}
pub async fn post(
mut jar: CookieJar,
Form(settings): Form<Settings>,
) -> impl IntoResponse {
jar = jar.add(Cookie::new("stylesheet-url", settings.stylesheet_url));
jar = jar.add(Cookie::new("stylesheet-str", settings.stylesheet_str));
(
StatusCode::FOUND,
[(header::LOCATION, "/settings")],
jar
)
}