From a6dc7ee0437170330f1cad6baea7c4ac0fe4d75b Mon Sep 17 00:00:00 2001 From: spikecodes <19519553+spikecodes@users.noreply.github.com> Date: Thu, 31 Dec 2020 15:54:13 -0800 Subject: [PATCH] Rewrite + Searching --- Cargo.lock | 21 +++-- Cargo.toml | 5 +- README.md | 5 - src/main.rs | 20 ++-- src/popular.rs | 57 ------------ src/post.rs | 68 +++++--------- src/search.rs | 52 +++++++++++ src/subreddit.rs | 46 ++++----- src/user.rs | 40 ++++---- src/utils.rs | 23 +++-- static/style.css | 118 +++++++++++++++++++----- templates/base.html | 5 +- templates/post.html | 18 ++-- templates/{popular.html => search.html} | 43 ++++++--- templates/subreddit.html | 34 +++---- templates/user.html | 21 ++--- templates/utils.html | 28 ++++++ 17 files changed, 342 insertions(+), 262 deletions(-) delete mode 100644 src/popular.rs create mode 100644 src/search.rs rename templates/{popular.html => search.html} (55%) create mode 100644 templates/utils.html diff --git a/Cargo.lock b/Cargo.lock index 03c62d3..98725c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -272,9 +272,9 @@ dependencies = [ [[package]] name = "addr2line" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423" +checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7" dependencies = [ "gimli", ] @@ -755,11 +755,11 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", "libc", "wasi 0.9.0+wasi-snapshot-preview1", ] @@ -1006,7 +1006,7 @@ checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb" [[package]] name = "libreddit" -version = "0.2.3" +version = "0.2.4" dependencies = [ "actix-web", "askama", @@ -1016,6 +1016,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "url", ] [[package]] @@ -1624,9 +1625,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "smallvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75" +checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0" [[package]] name = "socket2" @@ -1647,9 +1648,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "standback" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8" +checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0" dependencies = [ "version_check 0.9.2", ] diff --git a/Cargo.toml b/Cargo.toml index 22a45c7..e1eb619 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "libreddit" description = " Alternative private front-end to Reddit" license = "AGPL-3.0" repository = "https://github.com/spikecodes/libreddit" -version = "0.2.3" +version = "0.2.4" authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] edition = "2018" @@ -19,4 +19,5 @@ askama = "0.8.0" serde = "1.0.117" serde_json = "1.0" chrono = "0.4.19" -async-recursion = "0.3.1" \ No newline at end of file +async-recursion = "0.3.1" +url = "2.2.0" \ No newline at end of file diff --git a/README.md b/README.md index bb1eebb..0f83a0e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,6 @@ Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the - [About](#about) - [Elsewhere](#elsewhere) - [Info](#info) - - [In Progress](#in-progress) - [Teddit Comparison](#how-does-it-compare-to-teddit) - [Comparison](#comparison) - [Speed](#speed) @@ -66,9 +65,6 @@ Libreddit hopes to provide an easier way to browse Reddit, without the ads, trac Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below. -### In Progress -- Searching - ### How does it compare to Teddit? Teddit is another awesome open source project designed to provide an alternative frontend to Reddit. There is no connection between the two and you're welcome to use whichever one you favor. Competition fosters innovation and Teddit's release has motivated me to build Libreddit into an even more polished product. @@ -76,7 +72,6 @@ Teddit is another awesome open source project designed to provide an alternative If you are looking to compare, the biggest differences I have noticed are: - Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective. - Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db). -- Unlike Teddit (at the time of writing this), Libreddit does not require a Reddit API key to host. ## Comparison diff --git a/src/main.rs b/src/main.rs index c60170b..d52e226 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,9 @@ use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServer}; // Reference local files -mod popular; mod post; mod proxy; +mod search; mod subreddit; mod user; mod utils; @@ -42,6 +42,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() + // .default_service(web::get().to(subreddit::page)) // TRAILING SLASH MIDDLEWARE .wrap(NormalizePath::default()) // GENERAL SERVICES @@ -50,17 +51,22 @@ async fn main() -> std::io::Result<()> { .route("/robots.txt/", web::get().to(robots)) // PROXY SERVICE .route("/proxy/{url:.*}/", web::get().to(proxy::handler)) + // SEARCH SERVICES + .route("/search/", web::get().to(search::page)) + .route("r/{sub}/search/", web::get().to(search::page)) // USER SERVICES - .route("/u/{username}/", web::get().to(user::page)) - .route("/user/{username}/", web::get().to(user::page)) + .route("/u/{username}/", web::get().to(user::profile)) + .route("/user/{username}/", web::get().to(user::profile)) // SUBREDDIT SERVICES .route("/r/{sub}/", web::get().to(subreddit::page)) + .route("/r/{sub}/{sort}/", web::get().to(subreddit::page)) // POPULAR SERVICES - .route("/", web::get().to(popular::page)) + .route("/", web::get().to(subreddit::page)) + .route("/{sort:best|hot|new|top|rising}/", web::get().to(subreddit::page)) // POST SERVICES - .route("/{id:.{5,6}}/", web::get().to(post::short)) - .route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::page)) - .route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::comment)) + .route("/{id:.{5,6}}/", web::get().to(post::item)) + .route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::item)) + .route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::item)) }) .bind(address.clone()) .expect(format!("Cannot bind to the address: {}", address).as_str()) diff --git a/src/popular.rs b/src/popular.rs deleted file mode 100644 index e27f28e..0000000 --- a/src/popular.rs +++ /dev/null @@ -1,57 +0,0 @@ -// CRATES -use crate::utils::{fetch_posts, ErrorTemplate, Params, Post}; -use actix_web::{http::StatusCode, web, HttpResponse, Result}; -use askama::Template; - -// STRUCTS -#[derive(Template)] -#[template(path = "popular.html", escape = "none")] -struct PopularTemplate { - posts: Vec, - sort: (String, String), - ends: (String, String), -} - -// RENDER -async fn render(sort: Option, t: Option, ends: (Option, Option)) -> Result { - let sorting = sort.unwrap_or("hot".to_string()); - let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before - - let timeframe = match &t { Some(val) => format!("&t={}", val), None => String::new() }; - - // Build the Reddit JSON API url - let url = match ends.0 { - Some(val) => format!("r/popular/{}.json?before={}&count=25{}", sorting, val, timeframe), - None => match ends.1 { - Some(val) => format!("r/popular/{}.json?after={}&count=25{}", sorting, val, timeframe), - None => format!("r/popular/{}.json?{}", sorting, timeframe), - }, - }; - - let items_result = fetch_posts(url, String::new()).await; - - if items_result.is_err() { - let s = ErrorTemplate { - message: items_result.err().unwrap().to_string(), - } - .render() - .unwrap(); - Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)) - } else { - let items = items_result.unwrap(); - - let s = PopularTemplate { - posts: items.0, - sort: (sorting, t.unwrap_or(String::new())), - ends: (before, items.1), - } - .render() - .unwrap(); - Ok(HttpResponse::Ok().content_type("text/html").body(s)) - } -} - -// SERVICES -pub async fn page(params: web::Query) -> Result { - render(params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await -} diff --git a/src/post.rs b/src/post.rs index 5edd023..c63e28f 100644 --- a/src/post.rs +++ b/src/post.rs @@ -1,6 +1,6 @@ // CRATES -use crate::utils::{format_num, format_url, request, val, Comment, ErrorTemplate, Flair, Flags, Params, Post}; -use actix_web::{http::StatusCode, web, HttpResponse, Result}; +use crate::utils::{format_num, format_url, param, request, val, Comment, ErrorTemplate, Flags, Flair, Post}; +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use async_recursion::async_recursion; @@ -16,22 +16,17 @@ struct PostTemplate { sort: String, } -async fn render(id: String, sort: Option, comment_id: Option) -> Result { +pub async fn item(req: HttpRequest) -> Result { + let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string()); + let sort = param(&path, "sort").await; + let id = req.match_info().get("id").unwrap_or("").to_string(); + // Log the post ID being fetched in debug mode #[cfg(debug_assertions)] dbg!(&id); - // Handling sort paramater - let sorting: String = sort.unwrap_or("confidence".to_string()); - - // Build the Reddit JSON API url - let url: String = match comment_id { - None => format!("{}.json?sort={}&raw_json=1", id, sorting), - Some(val) => format!("{}.json?sort={}&comment={}&raw_json=1", id, sorting, val), - }; - // Send a request to the url, receive JSON in response - let req = request(url).await; + let req = request(path.clone()).await; // If the Reddit API returns an error, exit and send error page to user if req.is_err() { @@ -41,37 +36,18 @@ async fn render(id: String, sort: Option, comment_id: Option) -> .render() .unwrap(); return Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)); + } else { + // Otherwise, grab the JSON output from the request + let res = req.unwrap(); + + // Parse the JSON into Post and Comment structs + let post = parse_post(res[0].clone()).await.unwrap(); + let comments = parse_comments(res[1].clone()).await.unwrap(); + + // Use the Post and Comment structs to generate a website to show users + let s = PostTemplate { comments, post, sort }.render().unwrap(); + Ok(HttpResponse::Ok().content_type("text/html").body(s)) } - - // Otherwise, grab the JSON output from the request - let res = req.unwrap(); - - // Parse the JSON into Post and Comment structs - let post = parse_post(res[0].clone()).await; - let comments = parse_comments(res[1].clone()).await; - - // Use the Post and Comment structs to generate a website to show users - let s = PostTemplate { - comments: comments.unwrap(), - post: post.unwrap(), - sort: sorting, - } - .render() - .unwrap(); - Ok(HttpResponse::Ok().content_type("text/html").body(s)) -} - -// SERVICES -pub async fn short(web::Path(id): web::Path, params: web::Query) -> Result { - render(id, params.sort.clone(), None).await -} - -pub async fn comment(web::Path((_sub, id, _title, comment_id)): web::Path<(String, String, String, String)>, params: web::Query) -> Result { - render(id, params.sort.clone(), Some(comment_id)).await -} - -pub async fn page(web::Path((_sub, id)): web::Path<(String, String)>, params: web::Query) -> Result { - render(id, params.sort.clone(), None).await } // UTILITIES @@ -111,7 +87,7 @@ async fn parse_post(json: serde_json::Value) -> Result { let post = Post { title: val(post_data, "title").await, community: val(post_data, "subreddit").await, - body: val(post_data,"selftext_html").await, + body: val(post_data, "selftext_html").await, author: val(post_data, "author").await, author_flair: Flair( val(post_data, "author_flair_text").await, @@ -132,7 +108,7 @@ async fn parse_post(json: serde_json::Value) -> Result { ), flags: Flags { nsfw: post_data["data"]["over_18"].as_bool().unwrap_or(false), - stickied: post_data["data"]["stickied"].as_bool().unwrap_or(false) + stickied: post_data["data"]["stickied"].as_bool().unwrap_or(false), }, media: media.1, time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(), @@ -157,7 +133,7 @@ async fn parse_comments(json: serde_json::Value) -> Result, &'stati } let score = comment["data"]["score"].as_i64().unwrap_or(0); - let body = val(comment, "body_html").await; + let body = val(comment, "body_html").await; let replies: Vec = if comment["data"]["replies"].is_object() { parse_comments(comment["data"]["replies"].clone()).await.unwrap_or(Vec::new()) diff --git a/src/search.rs b/src/search.rs new file mode 100644 index 0000000..486dbd1 --- /dev/null +++ b/src/search.rs @@ -0,0 +1,52 @@ +// CRATES +use crate::utils::{fetch_posts, param, ErrorTemplate, Post}; +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; +use askama::Template; + +// STRUCTS +#[derive(Template)] +#[allow(dead_code)] +#[template(path = "search.html", escape = "none")] +struct SearchTemplate { + posts: Vec, + query: String, + sub: String, + sort: (String, String), + ends: (String, String), +} + +// SERVICES +pub async fn page(req: HttpRequest) -> Result { + let path = format!("{}.json?{}", req.path(), req.query_string()); + let q = param(&path, "q").await; + let sort = if param(&path, "sort").await.is_empty() { + "relevance".to_string() + } else { + param(&path, "sort").await + }; + let sub = req.match_info().get("sub").unwrap_or("").to_string(); + + let posts = fetch_posts(path.clone(), String::new()).await; + + if posts.is_err() { + let s = ErrorTemplate { + message: posts.err().unwrap().to_string(), + } + .render() + .unwrap(); + Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)) + } else { + let items = posts.unwrap(); + + let s = SearchTemplate { + posts: items.0, + query: q, + sub: sub, + sort: (sort, param(&path, "t").await), + ends: (param(&path, "after").await, items.1), + } + .render() + .unwrap(); + Ok(HttpResponse::Ok().content_type("text/html").body(s)) + } +} diff --git a/src/subreddit.rs b/src/subreddit.rs index 57ba189..08fc105 100644 --- a/src/subreddit.rs +++ b/src/subreddit.rs @@ -1,6 +1,6 @@ // CRATES -use crate::utils::{fetch_posts, format_num, format_url, request, val, ErrorTemplate, Params, Post, Subreddit}; -use actix_web::{http::StatusCode, web, HttpResponse, Result}; +use crate::utils::{fetch_posts, format_num, format_url, param, request, val, ErrorTemplate, Post, Subreddit}; +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use askama::Template; use std::convert::TryInto; @@ -15,49 +15,35 @@ struct SubredditTemplate { } // SERVICES -#[allow(dead_code)] -pub async fn page(web::Path(sub): web::Path, params: web::Query) -> Result { - render(sub, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await -} +// web::Path(sub): web::Path, params: web::Query +pub async fn page(req: HttpRequest) -> Result { + let path = format!("{}.json?{}", req.path(), req.query_string()); + let sub = req.match_info().get("sub").unwrap_or("popular").to_string(); + let sort = req.match_info().get("sort").unwrap_or("hot").to_string(); -pub async fn render(sub_name: String, sort: Option, t: Option, ends: (Option, Option)) -> Result { - let sorting = sort.unwrap_or("hot".to_string()); - let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before - - let timeframe = match &t { Some(val) => format!("&t={}", val), None => String::new() }; - - // Build the Reddit JSON API url - let url = match ends.0 { - Some(val) => format!("r/{}/{}.json?before={}&count=25{}", sub_name, sorting, val, timeframe), - None => match ends.1 { - Some(val) => format!("r/{}/{}.json?after={}&count=25{}", sub_name, sorting, val, timeframe), - None => format!("r/{}/{}.json?{}", sub_name, sorting, timeframe), - }, - }; - - let sub_result = if !&sub_name.contains("+") { - subreddit(&sub_name).await + let sub_result = if !&sub.contains("+") && sub != "popular" { + subreddit(&sub).await } else { Ok(Subreddit::default()) }; - let items_result = fetch_posts(url, String::new()).await; + let posts = fetch_posts(path.clone(), String::new()).await; - if sub_result.is_err() || items_result.is_err() { + if posts.is_err() { let s = ErrorTemplate { - message: sub_result.err().unwrap().to_string(), + message: posts.err().unwrap().to_string(), } .render() .unwrap(); Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)) } else { - let sub = sub_result.unwrap(); - let items = items_result.unwrap(); + let sub = sub_result.unwrap_or(Subreddit::default()); + let items = posts.unwrap(); let s = SubredditTemplate { sub: sub, posts: items.0, - sort: (sorting, t.unwrap_or(String::new())), - ends: (before, items.1), + sort: (sort, param(&path, "t").await), + ends: (param(&path, "after").await, items.1), } .render() .unwrap(); diff --git a/src/user.rs b/src/user.rs index edc7d92..5fa2ee2 100644 --- a/src/user.rs +++ b/src/user.rs @@ -1,6 +1,6 @@ // CRATES -use crate::utils::{fetch_posts, format_url, nested_val, request, ErrorTemplate, Params, Post, User}; -use actix_web::{http::StatusCode, web, HttpResponse, Result}; +use crate::utils::{fetch_posts, format_url, nested_val, param, request, ErrorTemplate, Post, User}; +use actix_web::{http::StatusCode, HttpRequest, HttpResponse, Result}; use askama::Template; use chrono::{TimeZone, Utc}; @@ -14,25 +14,19 @@ struct UserTemplate { ends: (String, String), } -async fn render(username: String, sort: Option, t: Option, ends: (Option, Option)) -> Result { - let sorting = sort.unwrap_or("new".to_string()); +pub async fn profile(req: HttpRequest) -> Result { + // Build the Reddit JSON API path + let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string()); - let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before - - let timeframe = match &t { Some(val) => format!("&t={}", val), None => String::new() }; - - // Build the Reddit JSON API url - let url = match ends.0 { - Some(val) => format!("user/{}/.json?sort={}&before={}&count=25&raw_json=1{}", username, sorting, val, timeframe), - None => match ends.1 { - Some(val) => format!("user/{}/.json?sort={}&after={}&count=25&raw_json=1{}", username, sorting, val, timeframe), - None => format!("user/{}/.json?sort={}&raw_json=1{}", username, sorting, timeframe), - }, - }; + // Retrieve other variables from Libreddit request + let sort = param(&path, "sort").await; + let username = req.match_info().get("username").unwrap_or("").to_string(); + // Request user profile data and user posts/comments from Reddit let user = user(&username).await; - let posts = fetch_posts(url, "Comment".to_string()).await; + let posts = fetch_posts(path.clone(), "Comment".to_string()).await; + // If there is an error show error page if user.is_err() || posts.is_err() { let s = ErrorTemplate { message: user.err().unwrap().to_string(), @@ -42,12 +36,12 @@ async fn render(username: String, sort: Option, t: Option, ends: Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s)) } else { let posts_unwrapped = posts.unwrap(); - + let s = UserTemplate { user: user.unwrap(), posts: posts_unwrapped.0, - sort: (sorting, t.unwrap_or(String::new())), - ends: (before, posts_unwrapped.1) + sort: (sort, param(&path, "t").await), + ends: (param(&path, "after").await, posts_unwrapped.1), } .render() .unwrap(); @@ -56,9 +50,9 @@ async fn render(username: String, sort: Option, t: Option, ends: } // SERVICES -pub async fn page(web::Path(username): web::Path, params: web::Query) -> Result { - render(username, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await -} +// pub async fn page(web::Path(username): web::Path, params: web::Query) -> Result { +// render(username, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await +// } // USER async fn user(name: &String) -> Result { diff --git a/src/utils.rs b/src/utils.rs index 7737ff1..9aec9b8 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,6 +3,7 @@ // use chrono::{TimeZone, Utc}; use serde_json::{from_str, Value}; +use url::Url; // use surf::{client, get, middleware::Redirect}; #[cfg(feature = "proxy")] @@ -16,7 +17,7 @@ pub struct Flair(pub String, pub String, pub String); // Post flags with nsfw and stickied pub struct Flags { pub nsfw: bool, - pub stickied: bool + pub stickied: bool, } // Post containing content, metadata and media @@ -72,6 +73,7 @@ pub struct Subreddit { #[derive(serde::Deserialize)] pub struct Params { pub t: Option, + pub q: Option, pub sort: Option, pub after: Option, pub before: Option, @@ -88,6 +90,13 @@ pub struct ErrorTemplate { // FORMATTING // +// Grab a query param from a url +pub async fn param(path: &String, value: &str) -> String { + let url = Url::parse(format!("https://reddit.com/{}", path).as_str()).unwrap(); + let pairs: std::collections::HashMap<_, _> = url.query_pairs().into_owned().collect(); + pairs.get(value).unwrap_or(&String::new()).to_owned() +} + // Direct urls to proxy if proxy is enabled pub async fn format_url(url: String) -> String { if url.is_empty() { @@ -127,9 +136,9 @@ pub async fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String { } // Fetch posts of a user or subreddit -pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec, String), &'static str> { +pub async fn fetch_posts(path: String, fallback_title: String) -> Result<(Vec, String), &'static str> { // Send a request to the url, receive JSON in response - let req = request(url.clone()).await; + let req = request(path.clone()).await; // If the Reddit API returns an error, exit this function if req.is_err() { @@ -178,14 +187,14 @@ pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec Result<(Vec Result { - url = format!("https://www.reddit.com/{}", url); +pub async fn request(path: String) -> Result { + let url = format!("https://www.reddit.com/{}", path); // --- actix-web::client --- // let client = actix_web::client::Client::default(); diff --git a/static/style.css b/static/style.css index ad86548..e402854 100644 --- a/static/style.css +++ b/static/style.css @@ -10,6 +10,11 @@ --black-contrast: 0 1px 3px rgba(0,0,0,0.5); } +::selection { + color: var(--background); + background: var(--accent); +} + * { transition: 0.2s all; margin: 0; @@ -25,10 +30,12 @@ body { nav { display: flex; justify-content: space-between; + align-items: center; color: var(--accent); background: var(--outside); - padding: 15px; + padding: 5px 15px; font-size: 20px; + height: 40px; } nav #lib, nav #github, nav #version { color: white; } @@ -47,6 +54,10 @@ footer { justify-content: center; } +footer > a { + margin-right: 5px; +} + button { background: none; border: none; @@ -76,7 +87,6 @@ aside { max-width: 350px; } - /* User & Subreddit */ #user, #subreddit, #sidebar { @@ -130,53 +140,112 @@ aside { font-size: 15px; } -/* Sorting */ +/* Sorting and Search */ -#sort, #timeframe { +select { background: var(--outside); - box-shadow: var(--black-contrast); +} + +select, #search { border: 0; padding: 0 15px; - margin-bottom: 20px; height: 40px; font-size: 15px; - border-radius: 5px 0 0 5px; appearance: none; + border-radius: 5px 0px 0px 5px; } +#searchbox { + display: flex; + box-shadow: var(--black-contrast); +} + +#searchbox > *, #sort_submit { + background: var(--highlighted); + height: 40px; +} + +#search { + border-right: 2px var(--outside) solid; + min-width: 0; + flex-grow: 1; +} + +#inside { + display: flex; + font-size: 15px; + align-items: center; + border-right: 2px var(--outside) solid; + height: 40px; + padding: 0 10px; +} + +#restrict_sr { margin-right: 5px; } + +input[type="submit"] { + border: 0; + border-radius: 0px 5px 5px 0px; +} + +select:hover { background: var(--foreground); } +input[type="submit"]:hover { color: var(--accent); } + #timeframe { - border-radius: 0; - border-left: 4px solid var(--highlighted); + border-radius: 5px 0px 0px 5px; + margin-left: 10px; } -#sort_submit { +#search_sort { background: var(--highlighted); - border: 0; - font-size: 15px; - height: 40px; - border-radius: 0 5px 5px 0; + border-radius: 5px; + overflow: hidden; } -#sort:hover, #timeframe:hover { background: var(--foreground); } -#sort_submit:hover { color: var(--accent); } +#search_sort > #search { + border: 0; + background: transparent; +} -#sort > div, footer > a { +#search_sort > :not(:first-child), #search_sort > #sort_options { + margin: 0; + border-radius: 0; + border-right: 0; + border-left: 2px solid var(--background); + box-shadow: none; + background: transparent; +} + +#sort_options { + height: 40px; +} + +#sort, #search_sort { + display: flex; + align-items: center; + margin-bottom: 20px; +} + +#sort_options, footer > a { + border-radius: 5px; box-shadow: var(--black-contrast); background: var(--outside); + display: flex; + overflow: hidden; +} + +#sort_options > a, footer > a { color: lightgrey; - border-radius: 5px; - margin-right: 5px; padding: 10px 20px; text-align: center; cursor: pointer; } -#sort > div.selected { +#sort_options > a.selected { background: var(--accent); color: black; } -#sort > div:hover { +#sort_options > a:not(.selected):hover { background: var(--foreground); } @@ -243,6 +312,7 @@ aside { .post_title { font-size: 18px; + line-height: 1.5; } .post_right { @@ -283,7 +353,6 @@ aside { background: var(--accent); color: black; padding: 5px; - margin-right: 5px; border-radius: 5px; font-size: 12px; font-weight: bold; @@ -494,9 +563,8 @@ td, th { } @media screen and (max-width: 800px) { - main { - flex-direction: column-reverse; - } + main { flex-direction: column-reverse; } + nav { flex-direction: column; } aside { margin: 20px 0 0 0; diff --git a/templates/base.html b/templates/base.html index d282ec8..ec9099f 100644 --- a/templates/base.html +++ b/templates/base.html @@ -11,14 +11,13 @@ {% endblock %} - + - {% block navbar %} - {% endblock %} {% block body %}
diff --git a/templates/post.html b/templates/post.html index 1242b61..bf72a2c 100644 --- a/templates/post.html +++ b/templates/post.html @@ -1,5 +1,13 @@ {% extends "base.html" %} +{% import "utils.html" as utils %} + {% block title %}{{ post.title }} - r/{{ post.community }}{% endblock %} + +{% block search %} + {% call utils::search(["/r/", post.community.as_str()].concat(), "") %} +{% endblock %} + +{% block root %}/r/{{ post.community }}{% endblock %}{% block location %}r/{{ post.community }}{% endblock %} {% block head %} {% call super() %} @@ -56,13 +64,9 @@
{{ post.body }}
-
- + {% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
diff --git a/templates/popular.html b/templates/search.html similarity index 55% rename from templates/popular.html rename to templates/search.html index dbe5e59..2f36761 100644 --- a/templates/popular.html +++ b/templates/search.html @@ -1,22 +1,24 @@ {% extends "base.html" %} +{% import "utils.html" as utils %} + {% block content %}
-
- {% if sort.0 == "top" %} + {% if sub != "" %} +
+ + +
+ {% endif %} + {% if sort.0 != "new" %}{% endif %}
{% for post in posts %} + {% if post.title != "Comment" %}

{{ post.score }}

@@ -40,6 +42,21 @@

+ {% else %} +
+
+

{{ post.score }}

+
+
+
+ + COMMENT + {{ post.time }} + +

{{ post.body }}

+
+

+ {% endif %} {% endfor %}