Compare commits
124 Commits
Author | SHA1 | Date | |
---|---|---|---|
471d181284 | |||
0e48c66b8c | |||
a0bc1732cf | |||
6d5fd1dbf6 | |||
0f6e73dd87 | |||
151490faf0 | |||
fdf60e7255 | |||
ab102ca32c | |||
998b301229 | |||
d7839899e6 | |||
2385fa33ec | |||
1fd688eeed | |||
65543a43b2 | |||
0099021478 | |||
3a9b2dba32 | |||
59021b9331 | |||
078d6fe25b | |||
373ce55203 | |||
aef0442e9d | |||
21ff8d7b6f | |||
bca2a7e540 | |||
0c014ad41b | |||
32b8637c7e | |||
5ed122d92c | |||
45660816ce | |||
d19e73f059 | |||
18684c934b | |||
cf4c5e1fe8 | |||
7ef4a20aff | |||
292f8fbbb7 | |||
735f79d80b | |||
a85a4278f6 | |||
dbe617d7eb | |||
842d97e9fa | |||
0bf5576427 | |||
dd027bff4b | |||
f95ef51017 | |||
740641cb4e | |||
09c98c8da6 | |||
33c8bdffb9 | |||
5ab88567de | |||
c6627ceece | |||
d9affcdefc | |||
96607256fc | |||
eb9a0dcb4a | |||
89fa0d5489 | |||
22589c8296 | |||
b0540d2c57 | |||
41c4661bbb | |||
d2314580a9 | |||
a4d77926b6 | |||
bbe7024323 | |||
32e1469e11 | |||
2d4ca2379f | |||
374f53eb32 | |||
add7efea3c | |||
065d82a5f5 | |||
1895bbc025 | |||
65f1a2afb2 | |||
6eb9e6f0c0 | |||
eb735a42fe | |||
541c741bde | |||
7a33ed3434 | |||
48d2943f72 | |||
6bbc90bc0d | |||
4d18dc0bb8 | |||
6dbd002acd | |||
bf6245a505 | |||
91746908a1 | |||
bb8273bab4 | |||
62bcc31305 | |||
08683fa5a6 | |||
c58b077330 | |||
f445c42f55 | |||
a0866b251e | |||
aa819544f6 | |||
fac56d7f87 | |||
ef1ad17234 | |||
b8cdc605a2 | |||
ef2f9ad12b | |||
b13874d0db | |||
3d142afd03 | |||
7fcb7fcfed | |||
747d5a7c67 | |||
770c4d3630 | |||
e7b448a282 | |||
c7c787dff1 | |||
59a34a0e85 | |||
6e8cf69227 | |||
3444989f9a | |||
7e96bb3d80 | |||
0adbb1556e | |||
710eecdb9d | |||
8a57fa8a1d | |||
b33d79ed9b | |||
0f506fc41b | |||
c9cd825d55 | |||
e63384e6a6 | |||
3260a4d596 | |||
da5c4603d9 | |||
b50fa6f3ae | |||
aa7b4b2af7 | |||
2b0193f5ea | |||
2185d895c0 | |||
9c1a932214 | |||
8c0269af1c | |||
df89c5076e | |||
f819ad2bc6 | |||
f5884a5270 | |||
c046d00060 | |||
5934e34ea0 | |||
463b44ac52 | |||
b40d21e559 | |||
a422a74747 | |||
4124fa87d3 | |||
1dd0c4ee20 | |||
0dd114c166 | |||
67090e9b08 | |||
d97fb49fde | |||
9263b0657f | |||
a3384cbaa6 | |||
5d26b5c764 | |||
516403ee47 | |||
5ea504e6e8 |
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
24
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: Bug Report | [title]
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: Feature Request | [title]
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
675
Cargo.lock
generated
675
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@ -3,22 +3,18 @@ name = "libreddit"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/spikecodes/libreddit"
|
||||
version = "0.2.5"
|
||||
version = "0.2.7"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[features]
|
||||
default = ["proxy"]
|
||||
proxy = ["actix-web/rustls", "base64"]
|
||||
|
||||
[dependencies]
|
||||
base64 = { version = "0.13.0", optional = true }
|
||||
actix-web = "3.2.0"
|
||||
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
|
||||
askama = "0.8.0"
|
||||
serde = "1.0.117"
|
||||
base64 = "0.13.0"
|
||||
actix-web = { version = "3.3.2", features = ["rustls"] }
|
||||
askama = "0.10.5"
|
||||
ureq = "2.0.1"
|
||||
serde = { version = "1.0.118", default_features = false, features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
chrono = "0.4.19"
|
||||
async-recursion = "0.3.1"
|
||||
url = "2.2.0"
|
||||
regex = "1"
|
||||
regex = "1.4.2"
|
||||
time = "0.2.23"
|
16
README.md
16
README.md
@ -7,7 +7,6 @@ Libre + Reddit = [Libreddit](https://libredd.it)
|
||||
- 🚀 Fast: written in Rust for blazing fast speeds and safety
|
||||
- ☁️ Light: no JavaScript, no ads, no tracking
|
||||
- 🕵 Private: all requests are proxied through the server, including media
|
||||
- 🦺 Safe: does not rely on Reddit OAuth or require a Reddit API Key
|
||||
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
|
||||
|
||||
Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libredd.it/r/unpopularopinion) without being [tracked](#reddit).
|
||||
@ -34,7 +33,7 @@ Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Instances
|
||||
|
||||
@ -55,7 +54,7 @@ A checkmark in the "Cloudflare" category here refers to the use of the reverse p
|
||||
|
||||
### Elsewhere
|
||||
Find Libreddit on...
|
||||
- 💬 Matrix: [#libreddit:matrix.org](https://matrix.to/#/#libreddit:matrix.org)
|
||||
- 💬 Matrix: [#libreddit:kde.org](https://matrix.to/#/#libreddit:matrix.org)
|
||||
- 🐋 Docker: [spikecodes/libreddit](https://hub.docker.com/r/spikecodes/libreddit)
|
||||
- :octocat: GitHub: [spikecodes/libreddit](https://github.com/spikecodes/libreddit)
|
||||
- 🦊 GitLab: [spikecodes/libreddit](https://gitlab.com/spikecodes/libreddit)
|
||||
@ -63,7 +62,7 @@ Find Libreddit on...
|
||||
### Info
|
||||
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||
|
||||
Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below.
|
||||
Libreddit currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/spikecodes/libreddit/issues).
|
||||
|
||||
### How does it compare to Teddit?
|
||||
|
||||
@ -124,11 +123,11 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot
|
||||
|
||||
For transparency, I hope to describe all the ways Libreddit handles user privacy.
|
||||
|
||||
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs and URL paths fetched to aid troubleshooting but nothing else.
|
||||
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs and URL paths fetched to aid with troubleshooting.
|
||||
|
||||
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
|
||||
|
||||
**Cookies:** Libreddit uses no cookies currently but eventually, I plan to add a configuration page where users can store an optional cookie to save their preferred theme, default sorting algorithm, or default layout.
|
||||
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libredd.it/settings). This is not a cross-site cookie and the cookie holds no personal data, only a value of the possible layout.
|
||||
|
||||
**Hosting:** The official instances (`libredd.it` and `libreddit.spike.codes`) are hosted on [Repl.it](https://repl.it/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting and browsing through Tor are welcomed.
|
||||
|
||||
@ -191,11 +190,6 @@ Specify a custom address for the server by passing the `-a` or `--address` argum
|
||||
libreddit --address=0.0.0.0:8111
|
||||
```
|
||||
|
||||
To disable the media proxy built into Libreddit, run:
|
||||
```
|
||||
libreddit --no-default-features
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
```
|
||||
|
114
src/main.rs
114
src/main.rs
@ -1,10 +1,11 @@
|
||||
// Import Crates
|
||||
use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServer};
|
||||
use actix_web::{middleware, web, App, HttpResponse, HttpServer}; // dev::Service
|
||||
|
||||
// Reference local files
|
||||
mod post;
|
||||
mod proxy;
|
||||
mod search;
|
||||
mod settings;
|
||||
mod subreddit;
|
||||
mod user;
|
||||
mod utils;
|
||||
@ -15,25 +16,27 @@ async fn style() -> HttpResponse {
|
||||
}
|
||||
|
||||
async fn robots() -> HttpResponse {
|
||||
HttpResponse::Ok().body(include_str!("../static/robots.txt"))
|
||||
HttpResponse::Ok()
|
||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||
.body("User-agent: *\nAllow: /")
|
||||
}
|
||||
|
||||
#[get("/favicon.ico")]
|
||||
async fn favicon() -> HttpResponse {
|
||||
HttpResponse::Ok().body("")
|
||||
HttpResponse::Ok()
|
||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||
.body(include_bytes!("../static/favicon.ico").as_ref())
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut address = "0.0.0.0:8080".to_string();
|
||||
// let mut https = false;
|
||||
|
||||
if args.len() > 1 {
|
||||
for arg in args {
|
||||
if arg.starts_with("--address=") || arg.starts_with("-a=") {
|
||||
let split: Vec<&str> = arg.split('=').collect();
|
||||
address = split[1].to_string();
|
||||
}
|
||||
for arg in std::env::args().collect::<Vec<String>>() {
|
||||
match arg.split('=').collect::<Vec<&str>>()[0] {
|
||||
"--address" | "-a" => address = arg.split('=').collect::<Vec<&str>>()[1].to_string(),
|
||||
// "--redirect-https" | "-r" => https = true,
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,40 +45,71 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
HttpServer::new(|| {
|
||||
App::new()
|
||||
// TRAILING SLASH MIDDLEWARE
|
||||
.wrap(NormalizePath::default())
|
||||
// DEFAULT SERVICE
|
||||
.default_service(web::get().to(utils::error))
|
||||
// GENERAL SERVICES
|
||||
// Redirect to HTTPS
|
||||
// .wrap_fn(|req, srv| { let fut = srv.call(req); async { let mut res = fut.await?; if https {} Ok(res) } })
|
||||
// Append trailing slash and remove double slashes
|
||||
.wrap(middleware::NormalizePath::default())
|
||||
// Default service in case no routes match
|
||||
.default_service(web::get().to(|| utils::error("Nothing here".to_string())))
|
||||
// Read static files
|
||||
.route("/style.css/", web::get().to(style))
|
||||
.route("/favicon.ico/", web::get().to(HttpResponse::Ok))
|
||||
.route("/favicon.ico/", web::get().to(favicon))
|
||||
.route("/robots.txt/", web::get().to(robots))
|
||||
// PROXY SERVICE
|
||||
// Proxy media through Libreddit
|
||||
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
|
||||
// SEARCH SERVICES
|
||||
.route("/search/", web::get().to(search::find))
|
||||
.route("r/{sub}/search/", web::get().to(search::find))
|
||||
// USER SERVICES
|
||||
.route("/u/{username}/", web::get().to(user::profile))
|
||||
.route("/user/{username}/", web::get().to(user::profile))
|
||||
// WIKI SERVICES
|
||||
.route("/wiki/", web::get().to(subreddit::wiki))
|
||||
.route("/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
|
||||
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||
// SUBREDDIT SERVICES
|
||||
.route("/r/{sub}/", web::get().to(subreddit::page))
|
||||
.route("/r/{sub}/{sort:hot|new|top|rising}/", web::get().to(subreddit::page))
|
||||
// POPULAR SERVICES
|
||||
.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::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))
|
||||
// Browse user profile
|
||||
.service(
|
||||
web::scope("/{scope:user|u}").service(
|
||||
web::scope("/{username}").route("/", web::get().to(user::profile)).service(
|
||||
web::scope("/comments/{id}/{title}")
|
||||
.route("/", web::get().to(post::item))
|
||||
.route("/{comment_id}/", web::get().to(post::item)),
|
||||
),
|
||||
),
|
||||
)
|
||||
// Configure settings
|
||||
.service(web::resource("/settings/").route(web::get().to(settings::get)).route(web::post().to(settings::set)))
|
||||
// Subreddit services
|
||||
.service(
|
||||
web::scope("/r/{sub}")
|
||||
// See posts and info about subreddit
|
||||
.route("/", web::get().to(subreddit::page))
|
||||
.route("/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||
// View post on subreddit
|
||||
.service(
|
||||
web::scope("/comments/{id}/{title}")
|
||||
.route("/", web::get().to(post::item))
|
||||
.route("/{comment_id}/", web::get().to(post::item)),
|
||||
)
|
||||
// Search inside subreddit
|
||||
.route("/search/", web::get().to(search::find))
|
||||
// View wiki of subreddit
|
||||
.service(
|
||||
web::scope("/wiki")
|
||||
.route("/", web::get().to(subreddit::wiki))
|
||||
.route("/{page}/", web::get().to(subreddit::wiki)),
|
||||
),
|
||||
)
|
||||
// Universal services
|
||||
.service(
|
||||
web::scope("")
|
||||
// Front page
|
||||
.route("/", web::get().to(subreddit::page))
|
||||
.route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||
// View Reddit wiki
|
||||
.service(
|
||||
web::scope("/wiki")
|
||||
.route("/", web::get().to(subreddit::wiki))
|
||||
.route("/{page}/", web::get().to(subreddit::wiki)),
|
||||
)
|
||||
// Search all of Reddit
|
||||
.route("/search/", web::get().to(search::find))
|
||||
// Short link for post
|
||||
.route("/{id:.{5,6}}/", web::get().to(post::item)),
|
||||
)
|
||||
})
|
||||
.bind(&address)
|
||||
.unwrap_or_else(|_| panic!("Cannot bind to the address: {}", address))
|
||||
.unwrap_or_else(|e| panic!("Cannot bind to the address {}: {}", address, e))
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
183
src/post.rs
183
src/post.rs
@ -1,11 +1,10 @@
|
||||
// CRATES
|
||||
use crate::utils::{error, format_num, format_url, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use crate::utils::*;
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
|
||||
use async_recursion::async_recursion;
|
||||
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -14,139 +13,165 @@ struct PostTemplate {
|
||||
comments: Vec<Comment>,
|
||||
post: Post,
|
||||
sort: String,
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
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");
|
||||
let id = req.match_info().get("id").unwrap_or("").to_string();
|
||||
pub async fn item(req: HttpRequest) -> HttpResponse {
|
||||
// Build Reddit API path
|
||||
let mut path: String = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||
|
||||
// Set sort to sort query parameter
|
||||
let mut sort: String = param(&path, "sort");
|
||||
|
||||
// Grab default comment sort method from Cookies
|
||||
let default_sort = cookie(&req, "comment_sort");
|
||||
|
||||
// If there's no sort query but there's a default sort, set sort to default_sort
|
||||
if sort.is_empty() && !default_sort.is_empty() {
|
||||
sort = default_sort;
|
||||
path = format!("{}.json?{}&sort={}&raw_json=1", req.path(), req.query_string(), sort);
|
||||
}
|
||||
|
||||
// Log the post ID being fetched in debug mode
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(&id);
|
||||
dbg!(req.match_info().get("id").unwrap_or(""));
|
||||
|
||||
// Send a request to the url, receive JSON in response
|
||||
match request(&path).await {
|
||||
// Otherwise, grab the JSON output from the request
|
||||
Ok(res) => {
|
||||
// Parse the JSON into Post and Comment structs
|
||||
let post = parse_post(&res[0]).await.unwrap();
|
||||
let comments = parse_comments(&res[1]).await.unwrap();
|
||||
let post = parse_post(&res[0]).await;
|
||||
let comments = parse_comments(&res[1]).await;
|
||||
|
||||
// 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))
|
||||
let s = PostTemplate {
|
||||
comments,
|
||||
post,
|
||||
sort,
|
||||
prefs: prefs(req),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
// If the Reddit API returns an error, exit and send error page to user
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
Err(msg) => error(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
// UTILITIES
|
||||
async fn media(data: &serde_json::Value) -> (String, String) {
|
||||
let post_type: &str;
|
||||
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap().to_string())
|
||||
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
|
||||
post_type = "video";
|
||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap().to_string())
|
||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||
post_type = "image";
|
||||
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap().to_string())
|
||||
} else {
|
||||
post_type = "link";
|
||||
data["url"].as_str().unwrap().to_string()
|
||||
};
|
||||
|
||||
(post_type.to_string(), url)
|
||||
}
|
||||
|
||||
// POSTS
|
||||
async fn parse_post(json: &serde_json::Value) -> Result<Post, &'static str> {
|
||||
async fn parse_post(json: &serde_json::Value) -> Post {
|
||||
// Retrieve post (as opposed to comments) from JSON
|
||||
let post_data: &serde_json::Value = &json["data"]["children"][0];
|
||||
let post: &serde_json::Value = &json["data"]["children"][0];
|
||||
|
||||
// Grab UTC time as unix timestamp
|
||||
let unix_time: i64 = post_data["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
||||
// Parse post score
|
||||
let score = post_data["data"]["score"].as_i64().unwrap();
|
||||
let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default());
|
||||
// Parse post score and upvote ratio
|
||||
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
|
||||
// Determine the type of media along with the media URL
|
||||
let media = media(&post_data["data"]).await;
|
||||
let (post_type, media) = media(&post["data"]).await;
|
||||
|
||||
// Build a post using data parsed from Reddit post API
|
||||
let post = Post {
|
||||
title: val(post_data, "title"),
|
||||
community: val(post_data, "subreddit"),
|
||||
body: rewrite_url(&val(post_data, "selftext_html")),
|
||||
author: val(post_data, "author"),
|
||||
author_flair: Flair(
|
||||
val(post_data, "author_flair_text"),
|
||||
val(post_data, "author_flair_background_color"),
|
||||
val(post_data, "author_flair_text_color"),
|
||||
),
|
||||
url: val(post_data, "permalink"),
|
||||
Post {
|
||||
id: val(post, "id"),
|
||||
title: val(post, "title"),
|
||||
community: val(post, "subreddit"),
|
||||
body: rewrite_url(&val(post, "selftext_html")),
|
||||
author: Author {
|
||||
name: val(post, "author"),
|
||||
flair: Flair {
|
||||
flair_parts: parse_rich_flair(
|
||||
val(post, "author_flair_type"),
|
||||
post["data"]["author_flair_richtext"].as_array(),
|
||||
post["data"]["author_flair_text"].as_str(),
|
||||
),
|
||||
background_color: val(post, "author_flair_background_color"),
|
||||
foreground_color: val(post, "author_flair_text_color"),
|
||||
},
|
||||
distinguished: val(post, "distinguished"),
|
||||
},
|
||||
permalink: val(post, "permalink"),
|
||||
score: format_num(score),
|
||||
post_type: media.0,
|
||||
flair: Flair(
|
||||
val(post_data, "link_flair_text"),
|
||||
val(post_data, "link_flair_background_color"),
|
||||
if val(post_data, "link_flair_text_color") == "dark" {
|
||||
upvote_ratio: ratio as i64,
|
||||
post_type,
|
||||
thumbnail: format_url(val(post, "thumbnail").as_str()),
|
||||
flair: Flair {
|
||||
flair_parts: parse_rich_flair(
|
||||
val(post, "link_flair_type"),
|
||||
post["data"]["link_flair_richtext"].as_array(),
|
||||
post["data"]["link_flair_text"].as_str(),
|
||||
),
|
||||
background_color: val(post, "link_flair_background_color"),
|
||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
} else {
|
||||
"white".to_string()
|
||||
},
|
||||
),
|
||||
flags: Flags {
|
||||
nsfw: post_data["data"]["over_18"].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(),
|
||||
};
|
||||
|
||||
Ok(post)
|
||||
flags: Flags {
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
|
||||
},
|
||||
media,
|
||||
domain: val(post, "domain"),
|
||||
rel_time,
|
||||
created,
|
||||
}
|
||||
}
|
||||
|
||||
// COMMENTS
|
||||
#[async_recursion]
|
||||
async fn parse_comments(json: &serde_json::Value) -> Result<Vec<Comment>, &'static str> {
|
||||
async fn parse_comments(json: &serde_json::Value) -> Vec<Comment> {
|
||||
// Separate the comment JSON into a Vector of comments
|
||||
let comment_data = json["data"]["children"].as_array().unwrap();
|
||||
let comment_data = match json["data"]["children"].as_array() {
|
||||
Some(f) => f.to_owned(),
|
||||
None => Vec::new(),
|
||||
};
|
||||
|
||||
let mut comments: Vec<Comment> = Vec::new();
|
||||
|
||||
// For each comment, retrieve the values to build a Comment object
|
||||
for comment in comment_data {
|
||||
let unix_time: i64 = comment["data"]["created_utc"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
if unix_time == 0 {
|
||||
let unix_time = comment["data"]["created_utc"].as_f64().unwrap_or_default();
|
||||
if unix_time == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let (rel_time, created) = time(unix_time);
|
||||
|
||||
let score = comment["data"]["score"].as_i64().unwrap_or(0);
|
||||
let body = rewrite_url(&val(comment, "body_html"));
|
||||
let body = rewrite_url(&val(&comment, "body_html"));
|
||||
|
||||
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
|
||||
parse_comments(&comment["data"]["replies"]).await.unwrap_or_default()
|
||||
parse_comments(&comment["data"]["replies"]).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
comments.push(Comment {
|
||||
id: val(comment, "id"),
|
||||
id: val(&comment, "id"),
|
||||
body,
|
||||
author: val(comment, "author"),
|
||||
author: Author {
|
||||
name: val(&comment, "author"),
|
||||
flair: Flair {
|
||||
flair_parts: parse_rich_flair(
|
||||
val(&comment, "author_flair_type"),
|
||||
comment["data"]["author_flair_richtext"].as_array(),
|
||||
comment["data"]["author_flair_text"].as_str(),
|
||||
),
|
||||
background_color: val(&comment, "author_flair_background_color"),
|
||||
foreground_color: val(&comment, "author_flair_text_color"),
|
||||
},
|
||||
distinguished: val(&comment, "distinguished"),
|
||||
},
|
||||
score: format_num(score),
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
||||
rel_time,
|
||||
created,
|
||||
replies,
|
||||
flair: Flair(
|
||||
val(comment, "author_flair_text"),
|
||||
val(comment, "author_flair_background_color"),
|
||||
val(comment, "author_flair_text_color"),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(comments)
|
||||
comments
|
||||
}
|
||||
|
63
src/proxy.rs
63
src/proxy.rs
@ -1,30 +1,49 @@
|
||||
use actix_web::{client::Client, web, Error, HttpResponse, Result};
|
||||
use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
|
||||
use url::Url;
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use base64::decode;
|
||||
|
||||
pub async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> {
|
||||
if cfg!(feature = "proxy") {
|
||||
#[cfg(feature = "proxy")]
|
||||
let media: String;
|
||||
pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
|
||||
let domains = vec![
|
||||
// THUMBNAILS
|
||||
"a.thumbs.redditmedia.com",
|
||||
"b.thumbs.redditmedia.com",
|
||||
// EMOJI
|
||||
"emoji.redditmedia.com",
|
||||
// ICONS
|
||||
"styles.redditmedia.com",
|
||||
"www.redditstatic.com",
|
||||
// PREVIEWS
|
||||
"preview.redd.it",
|
||||
"external-preview.redd.it",
|
||||
// MEDIA
|
||||
"i.redd.it",
|
||||
"v.redd.it",
|
||||
];
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
let media = url;
|
||||
match decode(b64) {
|
||||
Ok(bytes) => {
|
||||
let media = String::from_utf8(bytes).unwrap_or_default();
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
match decode(url) {
|
||||
Ok(bytes) => media = String::from_utf8(bytes).unwrap(),
|
||||
Err(_e) => return Ok(HttpResponse::Ok().body("")),
|
||||
};
|
||||
match Url::parse(media.as_str()) {
|
||||
Ok(url) => {
|
||||
let domain = url.domain().unwrap_or_default();
|
||||
|
||||
let client = Client::default();
|
||||
client
|
||||
.get(media.replace("&", "&"))
|
||||
.send()
|
||||
.await
|
||||
.map_err(Error::from)
|
||||
.map(|res| HttpResponse::build(res.status()).streaming(res))
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
if domains.contains(&domain) {
|
||||
Client::default().get(media.replace("&", "&")).send().await.map_err(Error::from).map(|res| {
|
||||
HttpResponse::build(res.status())
|
||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||
.header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned())
|
||||
.header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned())
|
||||
.streaming(res)
|
||||
})
|
||||
} else {
|
||||
Err(error::ErrorForbidden("Resource must be from Reddit"))
|
||||
}
|
||||
}
|
||||
_ => Err(error::ErrorBadRequest("Can't parse base64 into URL")),
|
||||
}
|
||||
}
|
||||
_ => Err(error::ErrorBadRequest("Can't decode base64")),
|
||||
}
|
||||
}
|
||||
|
@ -1,44 +1,98 @@
|
||||
// CRATES
|
||||
use crate::utils::{error, fetch_posts, param, Post};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use crate::utils::{error, fetch_posts, param, prefs, request, val, Post, Preferences};
|
||||
use actix_web::{HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
|
||||
// STRUCTS
|
||||
struct SearchParams {
|
||||
q: String,
|
||||
sort: String,
|
||||
t: String,
|
||||
before: String,
|
||||
after: String,
|
||||
restrict_sr: String,
|
||||
}
|
||||
|
||||
// STRUCTS
|
||||
struct Subreddit {
|
||||
name: String,
|
||||
url: String,
|
||||
description: String,
|
||||
subscribers: i64,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[allow(dead_code)]
|
||||
#[template(path = "search.html", escape = "none")]
|
||||
struct SearchTemplate {
|
||||
posts: Vec<Post>,
|
||||
query: String,
|
||||
subreddits: Vec<Subreddit>,
|
||||
sub: String,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
params: SearchParams,
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn find(req: HttpRequest) -> Result<HttpResponse> {
|
||||
pub async fn find(req: HttpRequest) -> HttpResponse {
|
||||
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||
let q = param(&path, "q");
|
||||
let sub = req.match_info().get("sub").unwrap_or("").to_string();
|
||||
|
||||
let sort = if param(&path, "sort").is_empty() {
|
||||
"relevance".to_string()
|
||||
} else {
|
||||
param(&path, "sort")
|
||||
};
|
||||
let sub = req.match_info().get("sub").unwrap_or("").to_string();
|
||||
|
||||
let subreddits = if param(&path, "restrict_sr").is_empty() {
|
||||
search_subreddits(param(&path, "q")).await
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
match fetch_posts(&path, String::new()).await {
|
||||
Ok(posts) => {
|
||||
let s = SearchTemplate {
|
||||
posts: posts.0,
|
||||
query: q,
|
||||
Ok((posts, after)) => HttpResponse::Ok().content_type("text/html").body(
|
||||
SearchTemplate {
|
||||
posts,
|
||||
subreddits,
|
||||
sub,
|
||||
sort: (sort, param(&path, "t")),
|
||||
ends: (param(&path, "after"), posts.1),
|
||||
params: SearchParams {
|
||||
q: param(&path, "q"),
|
||||
sort,
|
||||
t: param(&path, "t"),
|
||||
before: param(&path, "after"),
|
||||
after,
|
||||
restrict_sr: param(&path, "restrict_sr"),
|
||||
},
|
||||
prefs: prefs(req),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
}
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
.unwrap(),
|
||||
),
|
||||
Err(msg) => error(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn search_subreddits(q: String) -> Vec<Subreddit> {
|
||||
let subreddit_search_path = format!("/subreddits/search.json?q={}&limit=3", q.replace(' ', "+"));
|
||||
|
||||
// Send a request to the url
|
||||
match request(&subreddit_search_path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => {
|
||||
match response["data"]["children"].as_array() {
|
||||
// For each subreddit from subreddit list
|
||||
Some(list) => list
|
||||
.iter()
|
||||
.map(|subreddit| Subreddit {
|
||||
name: val(subreddit, "display_name_prefixed"),
|
||||
url: val(subreddit, "url"),
|
||||
description: val(subreddit, "public_description"),
|
||||
subscribers: subreddit["data"]["subscribers"].as_u64().unwrap_or_default() as i64,
|
||||
})
|
||||
.collect::<Vec<Subreddit>>(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
// If the Reddit API returns an error, exit this function
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
59
src/settings.rs
Normal file
59
src/settings.rs
Normal file
@ -0,0 +1,59 @@
|
||||
// CRATES
|
||||
use crate::utils::{prefs, Preferences};
|
||||
use actix_web::{cookie::Cookie, web::Form, HttpMessage, HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "settings.html")]
|
||||
struct SettingsTemplate {
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct SettingsForm {
|
||||
theme: Option<String>,
|
||||
front_page: Option<String>,
|
||||
layout: Option<String>,
|
||||
wide: Option<String>,
|
||||
comment_sort: Option<String>,
|
||||
hide_nsfw: Option<String>,
|
||||
}
|
||||
|
||||
// FUNCTIONS
|
||||
|
||||
// Retrieve cookies from request "Cookie" header
|
||||
pub async fn get(req: HttpRequest) -> HttpResponse {
|
||||
let s = SettingsTemplate { prefs: prefs(req) }.render().unwrap();
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
|
||||
// Set cookies using response "Set-Cookie" header
|
||||
pub async fn set(req: HttpRequest, form: Form<SettingsForm>) -> HttpResponse {
|
||||
let mut res = HttpResponse::Found();
|
||||
|
||||
let names = vec!["theme", "front_page", "layout", "wide", "comment_sort", "hide_nsfw"];
|
||||
let values = vec![&form.theme, &form.front_page, &form.layout, &form.wide, &form.comment_sort, &form.hide_nsfw];
|
||||
|
||||
for (i, name) in names.iter().enumerate() {
|
||||
match values[i] {
|
||||
Some(value) => res.cookie(
|
||||
Cookie::build(name.to_owned(), value)
|
||||
.path("/")
|
||||
.http_only(true)
|
||||
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||
.finish(),
|
||||
),
|
||||
None => match HttpMessage::cookie(&req, name.to_owned()) {
|
||||
Some(cookie) => res.del_cookie(&cookie),
|
||||
None => &mut res,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
res
|
||||
.content_type("text/html")
|
||||
.set_header("Location", "/settings")
|
||||
.body(r#"Redirecting to <a href="/settings">settings</a>..."#)
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
// CRATES
|
||||
use crate::utils::{error, fetch_posts, format_num, format_url, param, request, rewrite_url, val, Post, Subreddit};
|
||||
use crate::utils::*;
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
@ -11,6 +11,7 @@ struct SubredditTemplate {
|
||||
posts: Vec<Post>,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -19,60 +20,74 @@ struct WikiTemplate {
|
||||
sub: String,
|
||||
wiki: String,
|
||||
page: String,
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
// SERVICES
|
||||
pub async fn page(req: HttpRequest) -> Result<HttpResponse> {
|
||||
pub async fn page(req: HttpRequest) -> HttpResponse {
|
||||
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||
let sub = req.match_info().get("sub").unwrap_or("popular").to_string();
|
||||
let default = cookie(&req, "front_page");
|
||||
let sub_name = req
|
||||
.match_info()
|
||||
.get("sub")
|
||||
.unwrap_or(if default.is_empty() { "popular" } else { default.as_str() })
|
||||
.to_string();
|
||||
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
|
||||
|
||||
let sub_result = if !&sub.contains('+') && sub != "popular" {
|
||||
subreddit(&sub).await.unwrap_or_default()
|
||||
} else {
|
||||
Subreddit::default()
|
||||
};
|
||||
|
||||
match fetch_posts(&path, String::new()).await {
|
||||
Ok(items) => {
|
||||
Ok((posts, after)) => {
|
||||
// If you can get subreddit posts, also request subreddit metadata
|
||||
let sub = if !sub_name.contains('+') && sub_name != "popular" && sub_name != "all" {
|
||||
subreddit(&sub_name).await.unwrap_or_default()
|
||||
} else if sub_name.contains('+') {
|
||||
Subreddit {
|
||||
name: sub_name,
|
||||
..Subreddit::default()
|
||||
}
|
||||
} else {
|
||||
Subreddit::default()
|
||||
};
|
||||
|
||||
let s = SubredditTemplate {
|
||||
sub: sub_result,
|
||||
posts: items.0,
|
||||
sub,
|
||||
posts,
|
||||
sort: (sort, param(&path, "t")),
|
||||
ends: (param(&path, "after"), items.1),
|
||||
ends: (param(&path, "after"), after),
|
||||
prefs: prefs(req),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
Err(msg) => error(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn wiki(req: HttpRequest) -> Result<HttpResponse> {
|
||||
let sub = req.match_info().get("sub").unwrap_or("reddit.com");
|
||||
let page = req.match_info().get("page").unwrap_or("index");
|
||||
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||
pub async fn wiki(req: HttpRequest) -> HttpResponse {
|
||||
let sub = req.match_info().get("sub").unwrap_or("reddit.com").to_string();
|
||||
let page = req.match_info().get("page").unwrap_or("index").to_string();
|
||||
let path: String = format!("/r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||
|
||||
match request(&path).await {
|
||||
Ok(res) => {
|
||||
let s = WikiTemplate {
|
||||
sub: sub.to_string(),
|
||||
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap()),
|
||||
page: page.to_string(),
|
||||
sub,
|
||||
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
|
||||
page,
|
||||
prefs: prefs(req),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
Err(msg) => error(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
// SUBREDDIT
|
||||
async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
|
||||
async fn subreddit(sub: &str) -> Result<Subreddit, String> {
|
||||
// Build the Reddit JSON API url
|
||||
let path: String = format!("r/{}/about.json?raw_json=1", sub);
|
||||
let path: String = format!("/r/{}/about.json?raw_json=1", sub);
|
||||
|
||||
// Send a request to the url
|
||||
match request(&path).await {
|
||||
@ -91,7 +106,7 @@ async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
|
||||
title: val(&res, "title"),
|
||||
description: val(&res, "public_description"),
|
||||
info: rewrite_url(&val(&res, "description_html").replace("\\", "")),
|
||||
icon: format_url(icon),
|
||||
icon: format_url(icon.as_str()),
|
||||
members: format_num(members),
|
||||
active: format_num(active),
|
||||
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
||||
|
66
src/user.rs
66
src/user.rs
@ -1,8 +1,8 @@
|
||||
// CRATES
|
||||
use crate::utils::{error, fetch_posts, format_url, nested_val, param, request, Post, User};
|
||||
use crate::utils::{error, fetch_posts, format_url, param, prefs, request, Post, Preferences, User};
|
||||
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
@ -12,9 +12,11 @@ struct UserTemplate {
|
||||
posts: Vec<Post>,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
prefs: Preferences,
|
||||
}
|
||||
|
||||
pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
|
||||
// FUNCTIONS
|
||||
pub async fn profile(req: HttpRequest) -> HttpResponse {
|
||||
// Build the Reddit JSON API path
|
||||
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||
|
||||
@ -22,55 +24,57 @@ pub async fn profile(req: HttpRequest) -> Result<HttpResponse> {
|
||||
let sort = param(&path, "sort");
|
||||
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;
|
||||
// Request user posts/comments from Reddit
|
||||
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
||||
|
||||
match posts {
|
||||
Ok(items) => {
|
||||
Ok((posts, after)) => {
|
||||
// If you can get user posts, also request user data
|
||||
let user = user(&username).await.unwrap_or_default();
|
||||
|
||||
let s = UserTemplate {
|
||||
user: user.unwrap(),
|
||||
posts: items.0,
|
||||
user,
|
||||
posts,
|
||||
sort: (sort, param(&path, "t")),
|
||||
ends: (param(&path, "after"), items.1),
|
||||
ends: (param(&path, "after"), after),
|
||||
prefs: prefs(req),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
// If there is an error show error page
|
||||
Err(msg) => error(msg.to_string()).await,
|
||||
Err(msg) => error(msg).await,
|
||||
}
|
||||
}
|
||||
|
||||
// USER
|
||||
async fn user(name: &str) -> Result<User, &'static str> {
|
||||
async fn user(name: &str) -> Result<User, String> {
|
||||
// Build the Reddit JSON API path
|
||||
let path: String = format!("user/{}/about.json", name);
|
||||
|
||||
let res;
|
||||
let path: String = format!("/user/{}/about.json", name);
|
||||
|
||||
// Send a request to the url
|
||||
match request(&path).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => {
|
||||
res = response;
|
||||
Ok(res) => {
|
||||
// Grab creation date as unix timestamp
|
||||
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
|
||||
// nested_val function used to parse JSON from Reddit APIs
|
||||
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
||||
|
||||
// Parse the JSON output into a User struct
|
||||
Ok(User {
|
||||
name: name.to_string(),
|
||||
title: about("title"),
|
||||
icon: format_url(about("icon_img").as_str()),
|
||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
||||
banner: about("banner_img"),
|
||||
description: about("public_description"),
|
||||
})
|
||||
}
|
||||
// If the Reddit API returns an error, exit this function
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
|
||||
// Grab creation date as unix timestamp
|
||||
let created: i64 = res["data"]["created"].as_f64().unwrap().round() as i64;
|
||||
|
||||
// Parse the JSON output into a User struct
|
||||
Ok(User {
|
||||
name: name.to_string(),
|
||||
title: nested_val(&res, "subreddit", "title"),
|
||||
icon: format_url(nested_val(&res, "subreddit", "icon_img")),
|
||||
karma: res["data"]["total_karma"].as_i64().unwrap(),
|
||||
created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(),
|
||||
banner: nested_val(&res, "subreddit", "banner_img"),
|
||||
description: nested_val(&res, "subreddit", "public_description"),
|
||||
})
|
||||
}
|
||||
|
391
src/utils.rs
391
src/utils.rs
@ -1,22 +1,38 @@
|
||||
//
|
||||
// CRATES
|
||||
//
|
||||
use actix_web::{http::StatusCode, HttpResponse, Result};
|
||||
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use regex::Regex;
|
||||
use serde_json::from_str;
|
||||
use url::Url;
|
||||
// use surf::{client, get, middleware::Redirect};
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
use base64::encode;
|
||||
use regex::Regex;
|
||||
use serde_json::{from_str, Value};
|
||||
use std::collections::HashMap;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use url::Url;
|
||||
|
||||
//
|
||||
// STRUCTS
|
||||
//
|
||||
// Post flair with text, background color and foreground color
|
||||
pub struct Flair(pub String, pub String, pub String);
|
||||
|
||||
// Post flair with content, background color and foreground color
|
||||
pub struct Flair {
|
||||
pub flair_parts: Vec<FlairPart>,
|
||||
pub background_color: String,
|
||||
pub foreground_color: String,
|
||||
}
|
||||
|
||||
// Part of flair, either emoji or text
|
||||
pub struct FlairPart {
|
||||
pub flair_part_type: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub struct Author {
|
||||
pub name: String,
|
||||
pub flair: Flair,
|
||||
pub distinguished: String,
|
||||
}
|
||||
|
||||
// Post flags with nsfw and stickied
|
||||
pub struct Flags {
|
||||
pub nsfw: bool,
|
||||
@ -25,31 +41,36 @@ pub struct Flags {
|
||||
|
||||
// Post containing content, metadata and media
|
||||
pub struct Post {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub community: String,
|
||||
pub body: String,
|
||||
pub author: String,
|
||||
pub author_flair: Flair,
|
||||
pub url: String,
|
||||
pub author: Author,
|
||||
pub permalink: String,
|
||||
pub score: String,
|
||||
pub upvote_ratio: i64,
|
||||
pub post_type: String,
|
||||
pub flair: Flair,
|
||||
pub flags: Flags,
|
||||
pub thumbnail: String,
|
||||
pub media: String,
|
||||
pub time: String,
|
||||
pub domain: String,
|
||||
pub rel_time: String,
|
||||
pub created: String,
|
||||
}
|
||||
|
||||
// Comment with content, post, score and data/time that it was posted
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
pub body: String,
|
||||
pub author: String,
|
||||
pub flair: Flair,
|
||||
pub author: Author,
|
||||
pub score: String,
|
||||
pub time: String,
|
||||
pub rel_time: String,
|
||||
pub created: String,
|
||||
pub replies: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
// User struct containing metadata about user
|
||||
pub struct User {
|
||||
pub name: String,
|
||||
@ -88,31 +109,56 @@ pub struct Params {
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html", escape = "none")]
|
||||
pub struct ErrorTemplate {
|
||||
pub message: String,
|
||||
pub msg: String,
|
||||
pub prefs: Preferences,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Preferences {
|
||||
pub theme: String,
|
||||
pub front_page: String,
|
||||
pub layout: String,
|
||||
pub wide: String,
|
||||
pub hide_nsfw: String,
|
||||
pub comment_sort: String,
|
||||
}
|
||||
|
||||
//
|
||||
// FORMATTING
|
||||
//
|
||||
|
||||
// Build preferences from cookies
|
||||
pub fn prefs(req: HttpRequest) -> Preferences {
|
||||
Preferences {
|
||||
theme: cookie(&req, "theme"),
|
||||
front_page: cookie(&req, "front_page"),
|
||||
layout: cookie(&req, "layout"),
|
||||
wide: cookie(&req, "wide"),
|
||||
hide_nsfw: cookie(&req, "hide_nsfw"),
|
||||
comment_sort: cookie(&req, "comment_sort"),
|
||||
}
|
||||
}
|
||||
|
||||
// Grab a query param from a url
|
||||
pub fn param(path: &str, 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()
|
||||
match Url::parse(format!("https://libredd.it/{}", path).as_str()) {
|
||||
Ok(url) => url.query_pairs().into_owned().collect::<HashMap<_, _>>().get(value).unwrap_or(&String::new()).to_owned(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Parse Cookie value from request
|
||||
pub fn cookie(req: &HttpRequest, name: &str) -> String {
|
||||
actix_web::HttpMessage::cookie(req, name).unwrap_or_else(|| Cookie::new(name, "")).value().to_string()
|
||||
}
|
||||
|
||||
// Direct urls to proxy if proxy is enabled
|
||||
pub fn format_url(url: String) -> String {
|
||||
if url.is_empty() {
|
||||
return String::new();
|
||||
};
|
||||
|
||||
#[cfg(feature = "proxy")]
|
||||
return "/proxy/".to_string() + encode(url).as_str();
|
||||
|
||||
#[cfg(not(feature = "proxy"))]
|
||||
return url.to_string();
|
||||
pub fn format_url(url: &str) -> String {
|
||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||
String::new()
|
||||
} else {
|
||||
format!("/proxy/{}", encode(url).as_str())
|
||||
}
|
||||
}
|
||||
|
||||
// Rewrite Reddit links to Libreddit in body of text
|
||||
@ -123,31 +169,114 @@ pub fn rewrite_url(text: &str) -> String {
|
||||
|
||||
// Append `m` and `k` for millions and thousands respectively
|
||||
pub fn format_num(num: i64) -> String {
|
||||
if num > 1000000 {
|
||||
format!("{}m", num / 1000000)
|
||||
if num > 1_000_000 {
|
||||
format!("{}m", num / 1_000_000)
|
||||
} else if num > 1000 {
|
||||
format!("{}k", num / 1000)
|
||||
format!("{}k", num / 1_000)
|
||||
} else {
|
||||
num.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn media(data: &Value) -> (String, String) {
|
||||
let post_type: &str;
|
||||
// If post is a video, return the video
|
||||
let url = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() {
|
||||
post_type = "video";
|
||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default())
|
||||
} else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() {
|
||||
post_type = "video";
|
||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default())
|
||||
// Handle images, whether GIFs or pics
|
||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||
let preview = data["preview"]["images"][0].clone();
|
||||
match preview["variants"]["mp4"].as_object() {
|
||||
// Return the mp4 if the media is a gif
|
||||
Some(gif) => {
|
||||
post_type = "gif";
|
||||
format_url(gif["source"]["url"].as_str().unwrap_or_default())
|
||||
}
|
||||
// Return the picture if the media is an image
|
||||
None => {
|
||||
post_type = "image";
|
||||
format_url(preview["source"]["url"].as_str().unwrap_or_default())
|
||||
}
|
||||
}
|
||||
} else if data["is_self"].as_bool().unwrap_or_default() {
|
||||
post_type = "self";
|
||||
data["permalink"].as_str().unwrap_or_default().to_string()
|
||||
} else {
|
||||
post_type = "link";
|
||||
data["url"].as_str().unwrap_or_default().to_string()
|
||||
};
|
||||
|
||||
(post_type.to_string(), url)
|
||||
}
|
||||
|
||||
pub fn parse_rich_flair(flair_type: String, rich_flair: Option<&Vec<Value>>, text_flair: Option<&str>) -> Vec<FlairPart> {
|
||||
// Parse type of flair
|
||||
match flair_type.as_str() {
|
||||
// If flair contains emojis and text
|
||||
"richtext" => match rich_flair {
|
||||
Some(rich) => rich
|
||||
.iter()
|
||||
// For each part of the flair, extract text and emojis
|
||||
.map(|part| {
|
||||
let value = |name: &str| part[name].as_str().unwrap_or_default();
|
||||
FlairPart {
|
||||
flair_part_type: value("e").to_string(),
|
||||
value: match value("e") {
|
||||
"text" => value("t").to_string(),
|
||||
"emoji" => format_url(value("u")),
|
||||
_ => String::new(),
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect::<Vec<FlairPart>>(),
|
||||
None => Vec::new(),
|
||||
},
|
||||
// If flair contains only text
|
||||
"text" => match text_flair {
|
||||
Some(text) => vec![FlairPart {
|
||||
flair_part_type: "text".to_string(),
|
||||
value: text.to_string(),
|
||||
}],
|
||||
None => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn time(created: f64) -> (String, String) {
|
||||
let time = OffsetDateTime::from_unix_timestamp(created.round() as i64);
|
||||
let time_delta = OffsetDateTime::now_utc() - time;
|
||||
|
||||
// If the time difference is more than a month, show full date
|
||||
let rel_time = if time_delta > Duration::days(30) {
|
||||
time.format("%b %d '%y")
|
||||
// Otherwise, show relative date/time
|
||||
} else if time_delta.whole_days() > 0 {
|
||||
format!("{}d ago", time_delta.whole_days())
|
||||
} else if time_delta.whole_hours() > 0 {
|
||||
format!("{}h ago", time_delta.whole_hours())
|
||||
} else {
|
||||
format!("{}m ago", time_delta.whole_minutes())
|
||||
};
|
||||
|
||||
(rel_time, time.format("%b %d %Y, %H:%M UTC"))
|
||||
}
|
||||
|
||||
//
|
||||
// JSON PARSING
|
||||
//
|
||||
|
||||
// val() function used to parse JSON from Reddit APIs
|
||||
pub fn val(j: &serde_json::Value, k: &str) -> String {
|
||||
pub fn val(j: &Value, k: &str) -> String {
|
||||
String::from(j["data"][k].as_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
// nested_val() function used to parse JSON from Reddit APIs
|
||||
pub fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
|
||||
String::from(j["data"][n][k].as_str().unwrap_or_default())
|
||||
}
|
||||
|
||||
// Fetch posts of a user or subreddit
|
||||
pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
|
||||
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
||||
pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), String> {
|
||||
let res;
|
||||
let post_list;
|
||||
|
||||
@ -164,107 +293,171 @@ pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post
|
||||
// Fetch the list of posts from the JSON response
|
||||
match res["data"]["children"].as_array() {
|
||||
Some(list) => post_list = list,
|
||||
None => return Err("No posts found"),
|
||||
None => return Err("No posts found".to_string()),
|
||||
}
|
||||
|
||||
let mut posts: Vec<Post> = Vec::new();
|
||||
|
||||
// For each post from posts list
|
||||
for post in post_list {
|
||||
let img = if val(post, "thumbnail").starts_with("https:/") {
|
||||
format_url(val(post, "thumbnail"))
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
|
||||
let (rel_time, created) = time(post["data"]["created_utc"].as_f64().unwrap_or_default());
|
||||
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
let title = val(post, "title");
|
||||
|
||||
// Determine the type of media along with the media URL
|
||||
let (post_type, media) = media(&post["data"]).await;
|
||||
|
||||
posts.push(Post {
|
||||
id: val(post, "id"),
|
||||
title: if title.is_empty() { fallback_title.to_owned() } else { title },
|
||||
community: val(post, "subreddit"),
|
||||
body: rewrite_url(&val(post, "body_html")),
|
||||
author: val(post, "author"),
|
||||
author_flair: Flair(
|
||||
val(post, "author_flair_text"),
|
||||
val(post, "author_flair_background_color"),
|
||||
val(post, "author_flair_text_color"),
|
||||
),
|
||||
author: Author {
|
||||
name: val(post, "author"),
|
||||
flair: Flair {
|
||||
flair_parts: parse_rich_flair(
|
||||
val(post, "author_flair_type"),
|
||||
post["data"]["author_flair_richtext"].as_array(),
|
||||
post["data"]["author_flair_text"].as_str(),
|
||||
),
|
||||
background_color: val(post, "author_flair_background_color"),
|
||||
foreground_color: val(post, "author_flair_text_color"),
|
||||
},
|
||||
distinguished: val(post, "distinguished"),
|
||||
},
|
||||
score: format_num(score),
|
||||
post_type: "link".to_string(),
|
||||
media: img,
|
||||
flair: Flair(
|
||||
val(post, "link_flair_text"),
|
||||
val(post, "link_flair_background_color"),
|
||||
if val(post, "link_flair_text_color") == "dark" {
|
||||
upvote_ratio: ratio as i64,
|
||||
post_type,
|
||||
thumbnail: format_url(val(post, "thumbnail").as_str()),
|
||||
media,
|
||||
domain: val(post, "domain"),
|
||||
flair: Flair {
|
||||
flair_parts: parse_rich_flair(
|
||||
val(post, "link_flair_type"),
|
||||
post["data"]["link_flair_richtext"].as_array(),
|
||||
post["data"]["link_flair_text"].as_str(),
|
||||
),
|
||||
background_color: val(post, "link_flair_background_color"),
|
||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
} else {
|
||||
"white".to_string()
|
||||
},
|
||||
),
|
||||
flags: Flags {
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
|
||||
},
|
||||
url: val(post, "permalink"),
|
||||
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
|
||||
flags: Flags {
|
||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||
stickied: post["data"]["stickied"].as_bool().unwrap_or_default(),
|
||||
},
|
||||
permalink: val(post, "permalink"),
|
||||
rel_time,
|
||||
created,
|
||||
});
|
||||
}
|
||||
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or("").to_string()))
|
||||
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||
}
|
||||
|
||||
//
|
||||
// NETWORKING
|
||||
//
|
||||
|
||||
pub async fn error(message: String) -> Result<HttpResponse> {
|
||||
let msg = if message.is_empty() { "Page not found".to_string() } else { message };
|
||||
let body = ErrorTemplate { message: msg }.render().unwrap_or_default();
|
||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(body))
|
||||
pub async fn error(msg: String) -> HttpResponse {
|
||||
let body = ErrorTemplate {
|
||||
msg,
|
||||
prefs: Preferences::default(),
|
||||
}
|
||||
.render()
|
||||
.unwrap_or_default();
|
||||
HttpResponse::NotFound().content_type("text/html").body(body)
|
||||
}
|
||||
|
||||
// Make a request to a Reddit API and parse the JSON response
|
||||
pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
|
||||
let url = format!("https://www.reddit.com/{}", path);
|
||||
pub async fn request(path: &str) -> Result<Value, String> {
|
||||
let url = format!("https://www.reddit.com{}", path);
|
||||
let user_agent = format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// --- actix-web::client ---
|
||||
// let client = actix_web::client::Client::default();
|
||||
// let res = client
|
||||
// .get(url)
|
||||
// .send()
|
||||
// .await?
|
||||
// .body()
|
||||
// .limit(1000000)
|
||||
// .await?;
|
||||
// Send request using awc
|
||||
// async fn send(url: &str) -> Result<String, (bool, String)> {
|
||||
// let client = actix_web::client::Client::default();
|
||||
// let response = client.get(url).header("User-Agent", format!("web:libreddit:{}", env!("CARGO_PKG_VERSION"))).send().await;
|
||||
|
||||
// let body = std::str::from_utf8(res.as_ref())?; // .as_ref converts Bytes to [u8]
|
||||
// match response {
|
||||
// Ok(mut payload) => {
|
||||
// // Get first number of response HTTP status code
|
||||
// match payload.status().to_string().chars().next() {
|
||||
// // If success
|
||||
// Some('2') => Ok(String::from_utf8(payload.body().limit(20_000_000).await.unwrap_or_default().to_vec()).unwrap_or_default()),
|
||||
// // If redirection
|
||||
// Some('3') => match payload.headers().get("location") {
|
||||
// Some(location) => Err((true, location.to_str().unwrap_or_default().to_string())),
|
||||
// None => Err((false, "Page not found".to_string())),
|
||||
// },
|
||||
// // Otherwise
|
||||
// _ => Err((false, "Page not found".to_string())),
|
||||
// }
|
||||
// }
|
||||
// Err(e) => { dbg!(e); Err((false, "Couldn't send request to Reddit, this instance may be being rate-limited. Try another.".to_string())) },
|
||||
// }
|
||||
// }
|
||||
|
||||
// --- surf ---
|
||||
// let req = get(&url).header("User-Agent", "libreddit");
|
||||
// let client = client().with(Redirect::new(5));
|
||||
// let mut res = client.send(req).await.unwrap();
|
||||
// let success = res.status().is_success();
|
||||
// let body = res.body_string().await.unwrap();
|
||||
// // Print error if debugging then return error based on error message
|
||||
// fn err(url: String, msg: String) -> Result<Value, String> {
|
||||
// // #[cfg(debug_assertions)]
|
||||
// dbg!(format!("{} - {}", url, msg));
|
||||
// Err(msg)
|
||||
// };
|
||||
|
||||
// --- reqwest ---
|
||||
let res = reqwest::get(&url).await.unwrap();
|
||||
// Read the status from the response
|
||||
match res.status().is_success() {
|
||||
true => {
|
||||
// // Parse JSON from body. If parsing fails, return error
|
||||
// fn json(url: String, body: String) -> Result<Value, String> {
|
||||
// match from_str(body.as_str()) {
|
||||
// Ok(json) => Ok(json),
|
||||
// Err(_) => err(url, "Failed to parse page JSON data".to_string()),
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Make request to Reddit using send function
|
||||
// match send(&url).await {
|
||||
// // If success, parse and return body
|
||||
// Ok(body) => json(url, body),
|
||||
// // Follow any redirects
|
||||
// Err((true, location)) => match send(location.as_str()).await {
|
||||
// // If success, parse and return body
|
||||
// Ok(body) => json(url, body),
|
||||
// // Follow any redirects again
|
||||
// Err((true, location)) => err(url, location),
|
||||
// // Return errors if request fails
|
||||
// Err((_, msg)) => err(url, msg),
|
||||
// },
|
||||
// // Return errors if request fails
|
||||
// Err((_, msg)) => err(url, msg),
|
||||
// }
|
||||
|
||||
// Send request using ureq
|
||||
match ureq::get(&url).set("User-Agent", user_agent.as_str()).call() {
|
||||
// If response is success
|
||||
Ok(response) => {
|
||||
// Parse the response from Reddit as JSON
|
||||
match from_str(res.text().await.unwrap_or_default().as_str()) {
|
||||
match from_str(&response.into_string().unwrap()) {
|
||||
Ok(json) => Ok(json),
|
||||
Err(_) => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - Failed to parse page JSON data", url));
|
||||
Err("Failed to parse page JSON data")
|
||||
Err("Failed to parse page JSON data".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
false => {
|
||||
// If response is error
|
||||
Err(ureq::Error::Status(_, _)) => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - Page not found", url));
|
||||
Err("Page not found")
|
||||
Err("Page not found".to_string())
|
||||
}
|
||||
// If failed to send request
|
||||
Err(e) => {
|
||||
#[cfg(debug_assertions)]
|
||||
dbg!(format!("{} - {}", url, e));
|
||||
Err("Couldn't send request to Reddit, this instance may be being rate-limited. Try another.".to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 789 B |
511
static/style.css
511
static/style.css
@ -2,28 +2,33 @@
|
||||
|
||||
:root {
|
||||
--accent: aqua;
|
||||
--background: #0F0F0F;
|
||||
--green: #5cff85;
|
||||
--nsfw: #FF5C5D;
|
||||
--admin: #ea0027;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #0F0F0F;
|
||||
--outside: #1F1F1F;
|
||||
--post: #161616;
|
||||
--highlighted: #333;
|
||||
--black-contrast: 0 1px 3px rgba(0,0,0,0.5);
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--background);
|
||||
color: var(--foreground);
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
* {
|
||||
transition: 0.2s all;
|
||||
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
|
||||
pre, form, fieldset, table, th, td, select, input {
|
||||
margin: 0;
|
||||
color: white;
|
||||
color: var(--text);
|
||||
font-family: sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
nav {
|
||||
@ -35,17 +40,38 @@ nav {
|
||||
padding: 5px 15px;
|
||||
font-size: 20px;
|
||||
min-height: 40px;
|
||||
position: fixed;
|
||||
width: calc(100% - 30px);
|
||||
box-shadow: var(--shadow);
|
||||
top: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
nav #lib, nav #github, nav #version { color: white; }
|
||||
nav * { color: var(--text); }
|
||||
nav #reddit { color: var(--accent); }
|
||||
nav #version { opacity: 25%; }
|
||||
|
||||
#settings_link {
|
||||
font-size: 18px;
|
||||
margin-left: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
max-width: 1000px;
|
||||
padding: 10px 20px;
|
||||
margin: 20px auto;
|
||||
margin: 60px auto 20px auto
|
||||
}
|
||||
|
||||
.wide main {
|
||||
max-width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.wide #column_one {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
#column_one {
|
||||
@ -57,6 +83,7 @@ main {
|
||||
footer {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
footer > a {
|
||||
@ -76,6 +103,7 @@ hr {
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
a:not(.post_right):hover {
|
||||
@ -88,10 +116,33 @@ img[src=""] {
|
||||
|
||||
aside {
|
||||
flex-grow: 1;
|
||||
margin: 20px 20px 0 20px;
|
||||
margin: 20px 20px 0 10px;
|
||||
max-width: 350px;
|
||||
}
|
||||
|
||||
.post, .panel {
|
||||
border: 1px solid var(--highlighted);
|
||||
}
|
||||
|
||||
.dot {
|
||||
font-size: 12px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Light Theme */
|
||||
|
||||
.light {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
--background: #DDD;
|
||||
--outside: #ECECEC;
|
||||
--post: #eee;
|
||||
--highlighted: white;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
/* User & Subreddit */
|
||||
|
||||
#user, #subreddit, #sidebar {
|
||||
@ -99,7 +150,6 @@ aside {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
height: max-content;
|
||||
background: var(--outside);
|
||||
border-radius: 5px;
|
||||
@ -108,15 +158,10 @@ aside {
|
||||
|
||||
#user *, #subreddit * { text-align: center; }
|
||||
|
||||
#subreddit { padding: 0; }
|
||||
#sub_meta { padding: 20px; }
|
||||
#user, #sub_meta, #sidebar_contents { padding: 20px; }
|
||||
|
||||
#sidebar, #sidebar_contents { margin-top: 20px; }
|
||||
|
||||
#sidebar_label {
|
||||
border: 2px solid var(--highlighted);
|
||||
padding: 10px;
|
||||
}
|
||||
#sidebar, #sidebar_contents { margin-top: 10px; }
|
||||
#sidebar_label { padding: 10px; }
|
||||
|
||||
#user_icon, #sub_icon {
|
||||
width: 100px;
|
||||
@ -135,10 +180,12 @@ aside {
|
||||
|
||||
#user_description, #sub_description {
|
||||
margin: 0 20px;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#user_name, #sub_name, #user_icon, #sub_icon, #user_description, #sub_description { margin-bottom: 20px; }
|
||||
#user_name, #user_description:not(:empty), #user_icon
|
||||
#sub_name, #sub_icon, #sub_description:not(:empty) {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#user_details, #sub_details {
|
||||
display: grid;
|
||||
@ -148,7 +195,6 @@ aside {
|
||||
|
||||
#user_details > label, #sub_details > label {
|
||||
color: var(--accent);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
/* Wiki Pages */
|
||||
@ -160,7 +206,6 @@ aside {
|
||||
|
||||
#top {
|
||||
background: var(--highlighted);
|
||||
font-size: 18px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
@ -168,32 +213,42 @@ aside {
|
||||
#top > * {
|
||||
flex-grow: 1;
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
height: 35px;
|
||||
line-height: 35px;
|
||||
}
|
||||
|
||||
#top > div {
|
||||
border-bottom: 2px solid white;
|
||||
border-bottom: 2px solid var(--text);
|
||||
}
|
||||
|
||||
/* Sorting and Search */
|
||||
|
||||
.search_label {
|
||||
max-width: 300px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
select {
|
||||
background: var(--outside);
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
select, #search {
|
||||
border: 0;
|
||||
border: none;
|
||||
padding: 0 15px;
|
||||
height: 40px;
|
||||
font-size: 15px;
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#searchbox {
|
||||
display: flex;
|
||||
box-shadow: var(--black-contrast);
|
||||
box-shadow: var(--shadow);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#searchbox > *, #sort_submit {
|
||||
@ -209,7 +264,6 @@ select, #search {
|
||||
|
||||
#inside {
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
align-items: center;
|
||||
border-right: 2px var(--outside) solid;
|
||||
height: 40px;
|
||||
@ -221,14 +275,20 @@ select, #search {
|
||||
input[type="submit"] {
|
||||
border: 0;
|
||||
border-radius: 0px 5px 5px 0px;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
select:hover { background: var(--foreground); }
|
||||
input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
#timeframe {
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
margin: 0 2px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
#sort_options + #timeframe:not(#search_sort > #timeframe) {
|
||||
margin-left: 10px;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#search_sort {
|
||||
@ -242,6 +302,8 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#search_sort > *, #searchbox > * { font-size: 15px; }
|
||||
|
||||
#search_sort > :not(:first-child), #search_sort > #sort_options {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
@ -263,41 +325,96 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
#sort_options, footer > a {
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--black-contrast);
|
||||
box-shadow: var(--shadow);
|
||||
background: var(--outside);
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
#sort_options > a, footer > a {
|
||||
color: lightgrey;
|
||||
color: var(--text);
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
#sort_options > a.selected {
|
||||
background: var(--accent);
|
||||
color: black;
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#sort_options > a:not(.selected):hover {
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
#search_subreddits {
|
||||
border-radius: 5px;
|
||||
background: var(--post);
|
||||
box-shadow: var(--shadow);
|
||||
transition: 0.2s all;
|
||||
border: 1px solid var(--highlighted);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search_subreddit {
|
||||
padding: 16px 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
a.search_subreddit:hover {
|
||||
text-decoration: none;
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
.search_subreddit:not(:last-child) {
|
||||
border-bottom: 1px solid var(--highlighted);
|
||||
}
|
||||
|
||||
.search_subreddit_header {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.search_subreddit_name {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.search_subreddit_description {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Post */
|
||||
|
||||
.sep {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.thread {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.post {
|
||||
border-radius: 5px;
|
||||
background: var(--post);
|
||||
box-shadow: var(--black-contrast);
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
.post:not(:last-child) { margin-bottom: 10px; }
|
||||
|
||||
.post.highlighted {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.post.highlighted > .post_right {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post:hover {
|
||||
background: var(--foreground);
|
||||
}
|
||||
@ -308,7 +425,6 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
.post_left, .post_right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
@ -316,30 +432,32 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
text-align: center;
|
||||
background: var(--foreground);
|
||||
border-radius: 5px 0 0 5px;
|
||||
flex-direction: column;
|
||||
min-width: 50px;
|
||||
padding: 5px;
|
||||
transition: 0.2s all;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 20px;
|
||||
color: var(--accent);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
color: #FF5C5D;
|
||||
margin-top: 20px;
|
||||
border: 1px solid #FF5C5D;
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
#post_footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
opacity: 0.5;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#post_links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stickied {
|
||||
--accent: #5cff85;
|
||||
border: 1px solid #5cff85;
|
||||
padding: 5px;
|
||||
#post_links > li {
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.post_subreddit {
|
||||
@ -347,14 +465,21 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
}
|
||||
|
||||
.post_title {
|
||||
font-size: 18px;
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.post_text {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.post_right {
|
||||
padding: 20px 25px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.post_right > * {
|
||||
@ -364,6 +489,7 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
.post_media {
|
||||
max-width: 90%;
|
||||
align-self: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.post_body {
|
||||
@ -374,33 +500,92 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
#post_url {
|
||||
color: var(--accent);
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.post_thumbnail {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--foreground);
|
||||
width: 20%;
|
||||
max-width: 140px;
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
flex-shrink: 0;
|
||||
padding: 10px;
|
||||
border-radius: 15px;
|
||||
max-width: 20%;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.post_thumbnail img {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
width: 100%;
|
||||
object-fit: cover;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.post_thumbnail.no_thumbnail {
|
||||
background-color: var(--highlighted)
|
||||
}
|
||||
|
||||
.post_thumbnail svg {
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
stroke: var(--text);
|
||||
}
|
||||
|
||||
.post_thumbnail span {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
background-color: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
grid-area: 1 / 1 / 2 / 2;
|
||||
padding: 5px;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.post_flair {
|
||||
background: var(--accent);
|
||||
color: black;
|
||||
padding: 5px;
|
||||
color: var(--background);
|
||||
padding: 4px;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
background-size: contain;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
color: var(--nsfw);
|
||||
margin-top: 20px;
|
||||
border: 1px solid var(--nsfw);
|
||||
padding: 5px;
|
||||
font-size: 12px;
|
||||
border-radius: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.stickied {
|
||||
--accent: var(--green);
|
||||
border: 1px solid var(--green);
|
||||
}
|
||||
|
||||
/* Comment */
|
||||
|
||||
.comment {
|
||||
margin-top: 15px;
|
||||
margin: 10px 0;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.comment_left, .comment_right {
|
||||
@ -419,14 +604,9 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
.comment_link { text-decoration: underline; }
|
||||
.comment_author { opacity: 0.9; }
|
||||
|
||||
.comment_author.op {
|
||||
color: var(--accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.author_flair {
|
||||
background: var(--highlighted);
|
||||
color: white;
|
||||
color: var(--text);
|
||||
padding: 5px;
|
||||
margin-right: 5px;
|
||||
border-radius: 5px;
|
||||
@ -449,13 +629,13 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
|
||||
.comment_right {
|
||||
word-wrap: anywhere;
|
||||
padding: 10px 25px 10px 5px;
|
||||
padding: 10px 0 10px 5px;
|
||||
flex-grow: 1;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.comment_data > * {
|
||||
margin: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.comment_image {
|
||||
@ -492,8 +672,8 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
opacity: 0.75;
|
||||
.created {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.line {
|
||||
@ -502,22 +682,135 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
.post.comment {
|
||||
background: #000;
|
||||
border: 2px solid var(--foreground);
|
||||
.moderator, .admin { opacity: 1; }
|
||||
.op, .moderator, .admin { font-weight: bold; }
|
||||
|
||||
.op { color: var(--accent); }
|
||||
.moderator { color: var(--green); }
|
||||
.admin { color: var(--admin); }
|
||||
|
||||
/* Layouts */
|
||||
|
||||
.compact .post:not(.highlighted) {
|
||||
border-radius: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.post.comment > .post_left {
|
||||
background: black;
|
||||
.compact .post:first-of-type {
|
||||
border-radius: 5px 5px 0 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compact .post:last-of-type {
|
||||
border-radius: 0 0 5px 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.compact .post.highlighted {
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.compact .post:not(:last-of-type):not(.highlighted):not(.stickied) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.compact .post_left {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.compact .post_header {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.compact .post_title {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.compact .post_text {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.compact .post_thumbnail {
|
||||
width: 75px;
|
||||
height: 75px;
|
||||
}
|
||||
|
||||
.compact footer {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.card_post .post_right {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.card_post:not(.highlighted) .post_media {
|
||||
margin-top: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* Settings */
|
||||
|
||||
#settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#settings_note {
|
||||
font-size: 14px;
|
||||
max-width: 300px;
|
||||
margin-top: 10px;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
#prefs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: var(--post);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#prefs > div {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
height: 35px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#prefs > div:not(:last-of-type) {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#prefs select {
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--shadow);
|
||||
margin-left: 20px;
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
#save {
|
||||
background: var(--highlighted);
|
||||
padding: 10px 15px;
|
||||
border-radius: 5px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
input[type="submit"] {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
}
|
||||
/* Markdown */
|
||||
|
||||
.md > *:not(:first-child) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.md p { font-size: 15px; }
|
||||
.md h1 { font-size: 22px; }
|
||||
.md h2 { font-size: 20px; }
|
||||
.md h3 { font-size: 18px; }
|
||||
@ -526,15 +819,25 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
.md h6 { font-size: 12px; }
|
||||
|
||||
.md blockquote {
|
||||
padding-left: 8px;
|
||||
margin: 4px 0 4px 8px;
|
||||
padding-left: 6px;
|
||||
margin: 4px 0 4px 5px;
|
||||
border-left: 4px solid var(--highlighted);
|
||||
}
|
||||
|
||||
.md a {
|
||||
.md a, .md a * {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.md .md-spoiler-text {
|
||||
background: var(--highlighted);
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
.md .md-spoiler-text:hover {
|
||||
background: var(--foreground);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.md li { margin: 10px 0; }
|
||||
.toc_child { list-style: none; }
|
||||
|
||||
@ -543,7 +846,12 @@ input[type="submit"]:hover { color: var(--accent); }
|
||||
padding: 20px;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--black-contrast);
|
||||
box-shadow: var(--shadow);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.md table {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.md code {
|
||||
@ -572,20 +880,23 @@ td, th {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.post_left {
|
||||
border-radius: 0 0 5px 5px;
|
||||
.post_header {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.post_right {
|
||||
padding: 20px;
|
||||
.post_left {
|
||||
border-radius: 0 0 5px 5px;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.nsfw {
|
||||
margin: 5px 0px 5px 10px;
|
||||
}
|
||||
|
||||
.post_score {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.post_thumbnail {
|
||||
max-width: initial;
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.replies > .comment {
|
||||
@ -593,14 +904,30 @@ td, th {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.datetime {
|
||||
width: 100%;
|
||||
.comment_left {
|
||||
min-width: 45px;
|
||||
padding: 5px 0px;
|
||||
}
|
||||
|
||||
.comment_author { margin-left: 10px; }
|
||||
.comment_score { min-width: 35px; }
|
||||
.comment_data::marker { font-size: 18px; }
|
||||
.created { width: 100%; }
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
main { flex-direction: column-reverse; }
|
||||
nav { flex-direction: column; }
|
||||
main {
|
||||
flex-direction: column-reverse;
|
||||
padding: 10px;
|
||||
margin: 100px 0 10px 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
nav {
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
aside, #subreddit, #user {
|
||||
margin: 0;
|
||||
|
@ -4,21 +4,32 @@
|
||||
{% block head %}
|
||||
<title>{% block title %}Libreddit{% endblock %}</title>
|
||||
<meta http-equiv="Referrer-Policy" content="no-referrer">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';
|
||||
{% if cfg!(not(feature = "proxy")) %}img-src https://*; media-src https://*{% endif %}">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; base-uri 'none'; form-action 'self';">
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<meta name="description" content="View on Libreddit, an alternative private front-end to Reddit.">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<body class="
|
||||
{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}
|
||||
{% if prefs.wide == "on" %} wide{% endif %}
|
||||
{% if prefs.theme == "light" %} light{% endif %}">
|
||||
<!-- NAVIGATION BAR -->
|
||||
<nav>
|
||||
<a id="logo" href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
||||
<p id="logo">
|
||||
<a id="libreddit" href="/">
|
||||
<span id="lib">lib</span><span id="reddit">reddit.</span>
|
||||
</a>
|
||||
<span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span>
|
||||
<a id="settings_link" href="/settings">settings</a>
|
||||
</p>
|
||||
{% block search %}{% endblock %}
|
||||
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||
</nav>
|
||||
|
||||
<!-- MAIN CONTENT -->
|
||||
{% block body %}
|
||||
<main>
|
||||
{% block content %}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Error: {{ message }}{% endblock %}
|
||||
{% block title %}Error: {{ msg }}{% endblock %}
|
||||
{% block sortstyle %}{% endblock %}
|
||||
{% block content %}
|
||||
<h1 style="text-align: center; font-size: 50px;">{{ message }}</h1>
|
||||
<h1 style="text-align: center; font-size: 50px;">{{ msg }}</h1>
|
||||
{% endblock %}
|
@ -10,89 +10,114 @@
|
||||
{% block root %}/r/{{ post.community }}{% endblock %}{% block location %}r/{{ post.community }}{% endblock %}
|
||||
{% block head %}
|
||||
{% call super() %}
|
||||
<meta name="author" content="u/{{ post.author }}">
|
||||
<meta name="author" content="u/{{ post.author.name }}">
|
||||
{% endblock %}
|
||||
|
||||
<!-- OPEN COMMENT MACRO -->
|
||||
{% macro comment(item) -%}
|
||||
|
||||
<div id="{{ item.id }}" class="comment">
|
||||
<div class="comment_left">
|
||||
<p class="comment_score">{{ item.score }}</p>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
<details class="comment_right" open>
|
||||
<summary class="comment_data"><a class="comment_author {% if item.author == post.author %}op{% endif %}" href="/u/{{ item.author }}">u/{{ item.author }}</a>
|
||||
{% if item.flair.0 != "" %}
|
||||
<small class="author_flair">{{ item.flair.0 }}</small>
|
||||
<summary class="comment_data">
|
||||
<a class="comment_author {{ item.author.distinguished }} {% if item.author.name == post.author.name %}op{% endif %}" href="/u/{{ item.author.name }}">u/{{ item.author.name }}</a>
|
||||
{% if item.author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(item.author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
• <span class="datetime">{{ item.time }}</span>
|
||||
<span class="created" title="{{ post.created }}">{{ item.rel_time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ item.body }}</p>
|
||||
|
||||
<div class="comment_body">{{ item.body }}</div>
|
||||
{%- endmacro %}
|
||||
|
||||
<!-- CLOSE COMMENT MACRO -->
|
||||
{% macro close() %}
|
||||
</details></div>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div id="column_one">
|
||||
|
||||
<!-- POST CONTENT -->
|
||||
<div class="post highlighted">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
•
|
||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
{% if post.author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</p>
|
||||
<a href="{{ post.permalink }}" class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<small class="post_flair" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }}">{% call utils::render_flair(post.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
|
||||
<!-- POST MEDIA -->
|
||||
{% if post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type == "video" || post.post_type == "gif" %}
|
||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop></video>
|
||||
{% else if post.post_type == "link" %}
|
||||
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
||||
{% endif %}
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</p>
|
||||
<a href="{{ post.url }}" class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type == "video" %}
|
||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
|
||||
{% else if post.post_type == "link" %}
|
||||
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
||||
{% endif %}
|
||||
<div class="post_body">{{ post.body }}</div>
|
||||
|
||||
<!-- POST BODY -->
|
||||
<div class="post_body">{{ post.body }}</div>
|
||||
<div id="post_footer">
|
||||
<ul id="post_links">
|
||||
<li><a href="/{{ post.id }}">permalink</a></li>
|
||||
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li>
|
||||
</ul>
|
||||
<p>{{ post.upvote_ratio }}% Upvoted</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SORT FORM -->
|
||||
<form id="sort">
|
||||
<select name="sort">
|
||||
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "") %}
|
||||
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||
</select><input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
|
||||
<!-- COMMENTS -->
|
||||
{% for c in comments -%}
|
||||
<div class="thread">
|
||||
{% call comment(c) %}
|
||||
<div class="replies">
|
||||
{% for reply1 in c.replies %}
|
||||
{% call comment(reply1) %}
|
||||
<div class="replies">
|
||||
{% for reply2 in reply1.replies %}
|
||||
{% call comment(reply2) %}
|
||||
<div class="replies">
|
||||
{% for reply3 in reply2.replies %}
|
||||
{% call comment(reply3) %}
|
||||
{% if reply3.replies.len() > 0 %}
|
||||
<a class="deeper_replies" href="{{ post.url }}{{ reply3.id }}">→ More replies</a>
|
||||
{% endif %}
|
||||
</details></div>
|
||||
{% endfor %}
|
||||
</div></details></div>
|
||||
{% endfor %}
|
||||
</div></details></div>
|
||||
<div class="thread">
|
||||
<!-- EACH COMMENT -->
|
||||
{% call comment(c) %}
|
||||
<blockquote class="replies">{% for reply1 in c.replies %}{% call comment(reply1) %}
|
||||
<!-- FIRST-LEVEL REPLIES -->
|
||||
<blockquote class="replies">{% for reply2 in reply1.replies %}{% call comment(reply2) %}
|
||||
<!-- SECOND-LEVEL REPLIES -->
|
||||
<blockquote class="replies">{% for reply3 in reply2.replies %}{% call comment(reply3) %}
|
||||
<!-- THIRD-LEVEL REPLIES -->
|
||||
{% if reply3.replies.len() > 0 %}
|
||||
<!-- LINK TO CONTINUE REPLIES -->
|
||||
<a class="deeper_replies" href="{{ post.permalink }}{{ reply3.id }}">→ More replies</a>
|
||||
{% endif %}
|
||||
{% call close() %}
|
||||
{% endfor %}
|
||||
</div></details></div>
|
||||
</div>
|
||||
</blockquote>{% call close() %}
|
||||
{% endfor %}
|
||||
</blockquote>{% call close() %}
|
||||
{% endfor %}
|
||||
</blockquote>{% call close() %}
|
||||
</div>
|
||||
{%- endfor %}
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -1,49 +1,85 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}Libreddit: search results - {{ query }}{% endblock %}
|
||||
{% block title %}Libreddit: search results - {{ params.q }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div id="column_one">
|
||||
<form id="search_sort">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ query }}">
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q }}">
|
||||
{% 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>
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||
<label for="restrict_sr" class="search_label">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") %}
|
||||
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||
</select>{% if params.sort != "new" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
|
||||
{% if subreddits.len() > 0 %}
|
||||
<div id="search_subreddits">
|
||||
{% for subreddit in subreddits %}
|
||||
<a href="{{ subreddit.url }}" class="search_subreddit">
|
||||
<p class="search_subreddit_header">
|
||||
<span class="search_subreddit_name">{{ subreddit.name }}</span>
|
||||
<span class="dot">•</span>
|
||||
<span class="search_subreddit_members">{{ subreddit.subscribers }} Members</span>
|
||||
</p>
|
||||
<p class="search_subreddit_description">{{ subreddit.description }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for post in posts %}
|
||||
{% if post.title != "Comment" %}
|
||||
<div class="post">
|
||||
|
||||
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
|
||||
{% else if post.title != "Comment" %}
|
||||
<div class="post {% if prefs.layout == "card" && post.post_type == "image" %}card_post{% endif %}">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</p>
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
{% if post.author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<small class="post_flair" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }}">{% call utils::render_flair(post.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% if prefs.layout == "card" && post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type != "self" %}
|
||||
<a class="post_thumbnail {% if post.thumbnail == "" %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media }}{% else %}{{ post.permalink }}{% endif %}">
|
||||
{% if post.thumbnail == "" %}
|
||||
<svg viewBox="0 0 100 106" width="50" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35,15h-15a10,10 0,0,0 0,20h25a10,10 0,0,0 10,-10m-12.5,0a10, 10 0,0,1 10, -10h25a10,10 0,0,1 0,20h-15" fill="none" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<img src="{{ post.thumbnail }}">
|
||||
{% endif %}
|
||||
<span>{% if post.post_type == "link" %}{{ post.domain }}{% else %}{{ post.post_type }}{% endif %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
@ -52,22 +88,26 @@
|
||||
</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>
|
||||
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body }}</p>
|
||||
</details>
|
||||
</div><br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}{% if sort.0 == "top" %}&t={{ sort.1 }}{% endif %}&before={{ ends.0 }}">PREV</a>
|
||||
{% if params.before != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&before={{ params.before }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}{% if sort.0 == "top" %}&t={{ sort.1 }}{% endif %}&after={{ ends.1 }}">NEXT</a>
|
||||
{% if params.after != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&after={{ params.after }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
|
52
templates/settings.html
Normal file
52
templates/settings.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% extends "base.html" %}
|
||||
{% import "utils.html" as utils %}
|
||||
|
||||
{% block title %}Libreddit Settings{% endblock %}
|
||||
|
||||
{% block search %}
|
||||
{% call utils::search("".to_owned(), "", "") %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form id="settings" action="/settings" method="POST">
|
||||
<div id="prefs">
|
||||
<p>Appearance</p>
|
||||
<div id="theme">
|
||||
<label for="theme">Theme:</label>
|
||||
<select name="theme">
|
||||
{% call utils::options(prefs.theme, ["dark", "light"], "dark") %}
|
||||
</select>
|
||||
</div>
|
||||
<p>Interface</p>
|
||||
<div id="front_page">
|
||||
<label for="front_page">Front page:</label>
|
||||
<select name="front_page">
|
||||
{% call utils::options(prefs.front_page, ["popular", "all"], "popular") %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="layout">
|
||||
<label for="layout">Layout:</label>
|
||||
<select name="layout">
|
||||
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "clean") %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="wide">
|
||||
<label for="wide">Wide UI:</label>
|
||||
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
<p>Content</p>
|
||||
<div id="comment_sort">
|
||||
<label for="comment_sort">Default comment sort:</label>
|
||||
<select name="comment_sort">
|
||||
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||
</select>
|
||||
</div>
|
||||
<div id="hide_nsfw">
|
||||
<label for="hide_nsfw">Hide NSFW posts:</label>
|
||||
<input type="checkbox" name="hide_nsfw" {% if prefs.hide_nsfw == "on" %}checked{% endif %}>
|
||||
</div>
|
||||
</div>
|
||||
<p id="settings_note"><b>Note:</b> settings are saved in browser cookies. Clearing your cookie data will reset them.</p>
|
||||
<input id="save" type="submit" value="Save">
|
||||
</form>
|
||||
{% endblock %}
|
@ -17,55 +17,77 @@
|
||||
<form id="sort">
|
||||
<div id="sort_options">
|
||||
{% if sub.name.is_empty() %}
|
||||
{% call utils::sort("", ["hot", "new", "top", "rising"], sort.0) %}
|
||||
{% call utils::sort("", ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||
{% else %}
|
||||
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising"], sort.0) %}
|
||||
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if sort.0 == "top" %}<select id="timeframe" name="t">
|
||||
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
||||
<input id="sort_submit" type="submit" value="→">
|
||||
</select>{% endif %}
|
||||
</form>
|
||||
|
||||
<div id="posts">
|
||||
{% for post in posts %}
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %}">
|
||||
{% if !(post.flags.nsfw && prefs.hide_nsfw == "on") %}
|
||||
<hr class="sep" />
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %} {% if prefs.layout == "card" && post.post_type == "image" %}card_post{% endif %}">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 != "" %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</p>
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<small class="post_flair" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }}">{% call utils::render_flair(post.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% if prefs.layout == "card" && post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type != "self" %}
|
||||
<a class="post_thumbnail {% if post.thumbnail == "" %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media }}{% else %}{{ post.permalink }}{% endif %}">
|
||||
{% if post.thumbnail == "" %}
|
||||
<svg viewBox="0 0 100 106" width="50" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35,15h-15a10,10 0,0,0 0,20h25a10,10 0,0,0 10,-10m-12.5,0a10, 10 0,0,1 10, -10h25a10,10 0,0,1 0,20h-15" fill="none" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<img src="{{ post.thumbnail }}">
|
||||
{% endif %}
|
||||
<span>{% if post.post_type == "link" %}{{ post.domain }}{% else %}{{ post.post_type }}{% endif %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% if sub.name != "" %}
|
||||
{% if sub.name != "" && !sub.name.contains("+") %}
|
||||
<aside>
|
||||
<div id="subreddit">
|
||||
<div class="panel" id="subreddit">
|
||||
{% if sub.wiki %}
|
||||
<div id="top">
|
||||
<div>Posts</div>
|
||||
@ -85,11 +107,11 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<details id="sidebar">
|
||||
<details class="panel" id="sidebar">
|
||||
<summary id="sidebar_label">Sidebar</summary>
|
||||
<div id="sidebar_contents">{{ sub.info }}</div>
|
||||
</details>
|
||||
</aside>
|
||||
{% endif %}
|
||||
</main>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -6,8 +6,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Libreddit{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<main style="max-width: 1000px;">
|
||||
<main>
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<select name="sort">
|
||||
@ -16,33 +17,54 @@
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||
</form>
|
||||
|
||||
<div id="posts">
|
||||
{% for post in posts %}
|
||||
{% if post.title != "Comment" %}
|
||||
<div class='post'>
|
||||
|
||||
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
|
||||
{% else if post.title != "Comment" %}
|
||||
<div class="post {% if post.flags.stickied %}stickied{% endif %} {% if prefs.layout == "card" && post.post_type == "image" %}card_post{% endif %}">
|
||||
<div class="post_left">
|
||||
<p class="post_score">{{ post.score }}</p>
|
||||
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||
</div>
|
||||
<div class="post_right">
|
||||
<p>
|
||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||
{% if post.author_flair.0 != "" %}
|
||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||
{% endif %}
|
||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.0 == "Comment" %}
|
||||
{% else if post.flair.0 == "" %}
|
||||
{% else %}
|
||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
||||
</p>
|
||||
<div class="post_text">
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
{% if post.author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</p>
|
||||
<p class="post_title">
|
||||
{% if post.flair.background_color == "Comment" %}
|
||||
{% else if post.flair.background_color == "" %}
|
||||
{% else %}
|
||||
<small class="post_flair" style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }}">{% call utils::render_flair(post.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% if prefs.layout == "card" && post.post_type == "image" %}
|
||||
<img class="post_media" src="{{ post.media }}"/>
|
||||
{% else if post.post_type != "self" %}
|
||||
<a class="post_thumbnail {% if post.thumbnail == "" %}no_thumbnail{% endif %}" href="{% if post.post_type == "link" %}{{ post.media }}{% else %}{{ post.permalink }}{% endif %}">
|
||||
{% if post.thumbnail == "" %}
|
||||
<svg viewBox="0 0 100 106" width="50" height="53" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M35,15h-15a10,10 0,0,0 0,20h25a10,10 0,0,0 10,-10m-12.5,0a10, 10 0,0,1 10, -10h25a10,10 0,0,1 0,20h-15" fill="none" stroke-width="5" stroke-linecap="round"/>
|
||||
</svg>
|
||||
{% else %}
|
||||
<img src="{{ post.thumbnail }}">
|
||||
{% endif %}
|
||||
<span>{% if post.post_type == "link" %}{{ post.domain }}{% else %}{{ post.post_type }}{% endif %}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<img class="post_thumbnail" src="{{ post.media }}">
|
||||
</div><br>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="comment">
|
||||
<div class="comment_left">
|
||||
@ -51,26 +73,29 @@
|
||||
</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>
|
||||
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body }}</p>
|
||||
</details>
|
||||
</div><br>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
<aside>
|
||||
<div id="user">
|
||||
<div class="panel" id="user">
|
||||
<img id="user_icon" src="{{ user.icon }}">
|
||||
<p id="user_title">{{ user.title }}</p>
|
||||
<p id="user_name">u/{{ user.name }}</p>
|
||||
@ -84,4 +109,4 @@
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
@ -19,10 +19,17 @@
|
||||
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
|
||||
{% if root != "/r/" && !root.is_empty() %}
|
||||
<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>
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr">
|
||||
<label for="restrict_sr" class="search_label">in {{ root }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
<input type="submit" value="→">
|
||||
</form>
|
||||
{%- endmacro %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_flair(flair) -%}
|
||||
{% for flair_part in flair %}
|
||||
{% if flair_part.flair_part_type == "emoji" %}<span class="emoji" style="background-image:url('{{ flair_part.value }}')"></span>
|
||||
{% else if flair_part.flair_part_type == "text" %}<span>{{ flair_part.value }}</span>{% endif %}
|
||||
{% endfor %}
|
||||
{%- endmacro %}
|
||||
|
@ -12,7 +12,7 @@
|
||||
|
||||
{% block body %}
|
||||
<main>
|
||||
<div id="column_one">
|
||||
<div class="panel" id="column_one">
|
||||
<div id="top">
|
||||
<a href="/r/{{ sub }}">Posts</a>
|
||||
<div>Wiki</div>
|
||||
|
Reference in New Issue
Block a user