Rewrite + Searching

This commit is contained in:
spikecodes 2020-12-31 15:54:13 -08:00
parent c7282520cd
commit a6dc7ee043
17 changed files with 342 additions and 262 deletions

21
Cargo.lock generated
View File

@ -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",
]

View File

@ -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"
@ -20,3 +20,4 @@ serde = "1.0.117"
serde_json = "1.0"
chrono = "0.4.19"
async-recursion = "0.3.1"
url = "2.2.0"

View File

@ -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

View File

@ -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())

View File

@ -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<Post>,
sort: (String, String),
ends: (String, String),
}
// RENDER
async fn render(sort: Option<String>, t: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
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<Params>) -> Result<HttpResponse> {
render(params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await
}

View File

@ -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<String>, comment_id: Option<String>) -> Result<HttpResponse> {
pub async fn item(req: HttpRequest) -> Result<HttpResponse> {
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<String>, comment_id: Option<String>) ->
.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;
let comments = parse_comments(res[1].clone()).await;
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: comments.unwrap(),
post: post.unwrap(),
sort: sorting,
}
.render()
.unwrap();
let s = PostTemplate { comments, post, sort }.render().unwrap();
Ok(HttpResponse::Ok().content_type("text/html").body(s))
}
// SERVICES
pub async fn short(web::Path(id): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
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<Params>) -> Result<HttpResponse> {
render(id, params.sort.clone(), Some(comment_id)).await
}
pub async fn page(web::Path((_sub, id)): web::Path<(String, String)>, params: web::Query<Params>) -> Result<HttpResponse> {
render(id, params.sort.clone(), None).await
}
}
// UTILITIES
@ -111,7 +87,7 @@ async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
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<Post, &'static str> {
),
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(),

52
src/search.rs Normal file
View File

@ -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<Post>,
query: String,
sub: String,
sort: (String, String),
ends: (String, String),
}
// SERVICES
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
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))
}
}

View File

@ -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<String>, params: web::Query<Params>) -> Result<HttpResponse> {
render(sub, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await
}
// web::Path(sub): web::Path<String>, params: web::Query<Params>
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
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<String>, t: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
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();

View File

@ -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<String>, t: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
let sorting = sort.unwrap_or("new".to_string());
pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
// 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(),
@ -46,8 +40,8 @@ async fn render(username: String, sort: Option<String>, t: Option<String>, ends:
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<String>, t: Option<String>, ends:
}
// SERVICES
pub async fn page(web::Path(username): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
render(username, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await
}
// pub async fn page(web::Path(username): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
// render(username, params.sort.clone(), params.t.clone(), (params.before.clone(), params.after.clone())).await
// }
// USER
async fn user(name: &String) -> Result<User, &'static str> {

View File

@ -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<String>,
pub q: Option<String>,
pub sort: Option<String>,
pub after: Option<String>,
pub before: Option<String>,
@ -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<Post>, String), &'static str> {
pub async fn fetch_posts(path: String, fallback_title: String) -> Result<(Vec<Post>, 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<Pos
),
flags: Flags {
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
stickied: post["data"]["stickied"].as_bool().unwrap_or(false)
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
},
url: val(post, "permalink").await,
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
});
}
dbg!(url);
dbg!(path);
Ok((posts, res["data"]["after"].as_str().unwrap_or("").to_string()))
}
@ -195,8 +204,8 @@ pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec<Pos
//
// Make a request to a Reddit API and parse the JSON response
pub async fn request(mut url: String) -> Result<serde_json::Value, &'static str> {
url = format!("https://www.reddit.com/{}", url);
pub async fn request(path: String) -> Result<serde_json::Value, &'static str> {
let url = format!("https://www.reddit.com/{}", path);
// --- actix-web::client ---
// let client = actix_web::client::Client::default();

View File

@ -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;

View File

@ -13,12 +13,11 @@
{% endblock %}
</head>
<body style="visibility: hidden;">
{% block navbar %}
<nav>
<a href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
{% block search %}{% endblock %}
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
</nav>
{% endblock %}
{% block body %}
<main>

View File

@ -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() %}
<meta name="author" content="u/{{ post.author }}">
@ -56,13 +64,9 @@
<div class="post_body">{{ post.body }}</div>
</div>
</div>
<form>
<select id="sort" name="sort">
<option value="confidence" {% if sort == "confidence" %}selected{% endif %}>Best</option>
<option value="top" {% if sort == "top" %}selected{% endif %}>Top</option>
<option value="new" {% if sort == "new" %}selected{% endif %}>New</option>
<option value="controversial" {% if sort == "controversial" %}selected{% endif %}>Controversial</option>
<option value="old" {% if sort == "old" %}selected{% endif %}>Old</option>
<form id="sort">
<select name="sort">
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
</select><input id="sort_submit" type="submit" value="&rarr;">
</form>

View File

@ -1,22 +1,24 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block content %}
<div id="column_one">
<form>
<select id="sort" name="sort">
<option value="best" {% if sort.0 == "best" %}selected{% endif %}>Best</option>
<option value="hot" {% if sort.0 == "hot" %}selected{% endif %}>Hot</option>
<option value="new" {% if sort.0 == "new" %}selected{% endif %}>New</option>
<option value="top" {% if sort.0 == "top" %}selected{% endif %}>Top</option>
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
<option value="hour" {% if sort.1 == "hour" %}selected{% endif %}>Hour</option>
<option value="day" {% if sort.1 == "day" || sort.1 == "" %}selected{% endif %}>Day</option>
<option value="week" {% if sort.1 == "week" %}selected{% endif %}>Week</option>
<option value="month" {% if sort.1 == "month" %}selected{% endif %}>Month</option>
<option value="year" {% if sort.1 == "year" %}selected{% endif %}>Year</option>
<option value="all" {% if sort.1 == "all" %}selected{% endif %}>All</option>
<form id="search_sort">
<input id="search" type="text" name="q" placeholder="Search" value="{{ query }}">
{% if sub != "" %}
<div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes">
<label for="restrict_sr">in r/{{ sub }}</label>
</div>
{% endif %}
<select id="sort_options" name="sort">
{% call utils::options(sort.0, ["relevance", "hot", "top", "new", "comments"], "") %}
</select>{% if sort.0 != "new" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
</form>
{% for post in posts %}
{% if post.title != "Comment" %}
<div class="post">
<div class="post_left">
<p class="post_score">{{ post.score }}</p>
@ -40,6 +42,21 @@
</div>
<img class="post_thumbnail" src="{{ post.media }}">
</div><br>
{% else %}
<div class="comment">
<div class="comment_left">
<p class="comment_score">{{ post.score }}</p>
<div class="line"></div>
</div>
<details class="comment_right" open>
<summary class="comment_data">
<a class="comment_link" href="{{ post.url }}">COMMENT</a>
<span class="datetime">{{ post.time }}</span>
</summary>
<p class="comment_body">{{ post.body }}</p>
</details>
</div><br>
{% endif %}
{% endfor %}
<footer>

View File

@ -1,25 +1,27 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% if sub.name != "" %}
{% block title %}r/{{ sub.name }}: {{ sub.description }}{% endblock %}
{% endif %}
{% block search %}
{% call utils::search(["/r/", sub.name.as_str()].concat(), "") %}
{% endblock %}
{% block body %}
<main style="max-width: 1000px;">
<main>
<div id="column_one">
<form>
<select id="sort" name="sort">
<option value="hot" {% if sort.0 == "hot" %}selected{% endif %}>Hot</option>
<option value="new" {% if sort.0 == "new" %}selected{% endif %}>New</option>
<option value="top" {% if sort.0 == "top" %}selected{% endif %}>Top</option>
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
<option value="hour" {% if sort.1 == "hour" %}selected{% endif %}>Hour</option>
<option value="day" {% if sort.1 == "day" || sort.1 == "" %}selected{% endif %}>Day</option>
<option value="week" {% if sort.1 == "week" %}selected{% endif %}>Week</option>
<option value="month" {% if sort.1 == "month" %}selected{% endif %}>Month</option>
<option value="year" {% if sort.1 == "year" %}selected{% endif %}>Year</option>
<option value="all" {% if sort.1 == "all" %}selected{% endif %}>All</option>
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
<form id="sort">
<div id="sort_options">
{% if sub.name.is_empty() %}
{% call utils::sort("", ["hot", "new", "top", "rising"], sort.0) %}
{% else %}
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising"], sort.0) %}
{% endif %}
</div>
{% if sort.0 == "top" %}<select id="timeframe" name="t">
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
<input id="sort_submit" type="submit" value="&rarr;">
</select>{% endif %}
</form>
{% for post in posts %}
<div class="post {% if post.flags.stickied %}stickied{% endif %}">

View File

@ -1,20 +1,19 @@
{% extends "base.html" %}
{% import "utils.html" as utils %}
{% block search %}
{% call utils::search("".to_owned(), "", "") %}
{% endblock %}
{% block title %}Libreddit: u/{{ user.name }}{% endblock %}
{% block body %}
<main style="max-width: 1000px;">
<div id="column_one">
<form>
<select id="sort" name="sort">
<option value="hot" {% if sort.0 == "hot" %}selected{% endif %}>Hot</option>
<option value="new" {% if sort.0 == "new" %}selected{% endif %}>New</option>
<option value="top" {% if sort.0 == "top" %}selected{% endif %}>Top</option>
<form id="sort">
<select name="sort">
{% call utils::options(sort.0, ["hot", "new", "top"], "") %}
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
<option value="hour" {% if sort.1 == "hour" %}selected{% endif %}>Hour</option>
<option value="day" {% if sort.1 == "day" %}selected{% endif %}>Day</option>
<option value="week" {% if sort.1 == "week" %}selected{% endif %}>Week</option>
<option value="month" {% if sort.1 == "month" %}selected{% endif %}>Month</option>
<option value="year" {% if sort.1 == "year" %}selected{% endif %}>Year</option>
<option value="all" {% if sort.1 == "all" || sort.1 == "" %}selected{% endif %}>All</option>
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
</select>{% endif %}<input id="sort_submit" type="submit" value="&rarr;">
</form>
{% for post in posts %}

28
templates/utils.html Normal file
View File

@ -0,0 +1,28 @@
{% macro options(current, values, default) -%}
{% for value in values %}
<option value="{{ value }}" {% if current == value || (current == "" && value == default) %}selected{% endif %}>
{{ format!("{}{}", value.get(0..1).unwrap().to_uppercase(), value.get(1..).unwrap()) }}
</option>
{% endfor %}
{%- endmacro %}
{% macro sort(root, methods, selected) -%}
{% for method in methods %}
<a {% if method == selected %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
{{ format!("{}{}", method.get(0..1).unwrap().to_uppercase(), method.get(1..).unwrap()) }}
</a>
{% endfor %}
{%- endmacro %}
{% macro search(root, search) -%}
<form action="{% if root != "/r/" %}{{ root }}{% endif %}/search/" id="searchbox">
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
{% if root != "/r/" %}
<div id="inside">
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked="checked" data-com.bitwarden.browser.user-edited="yes">
<label for="restrict_sr">in {{ root }}</label>
</div>
{% endif %}
<input type="submit" value="&rarr;">
</form>
{%- endmacro %}