Compare commits

..

17 Commits

Author SHA1 Message Date
c15f305be0 v0.25.3 2023-01-01 23:54:35 -07:00
222d216854 Merge pull request #673 from spenserblack/code-in-new-tab 2023-01-01 23:08:15 -05:00
6d8aaba8bb Merge pull request #676 from ellieeet123/master 2023-01-01 19:23:30 -05:00
6cf3748642 Fix for #675
/:id route now accepts 7 character post IDs.
2023-01-01 17:06:58 -06:00
9c938c6210 build: enable LTO, set codegen-unit to 1 and strip the binary (#467)
Co-authored-by: Spike <19519553+spikecodes@users.noreply.github.com>
2023-01-01 14:33:31 -07:00
a49d399f72 Link to libreddit/libreddit and open in new tab
This sets the target of the "code" link to `_blank`, so that it will
open in a new tab in browsers. Because the GitHub page is a different
context from libreddit, and accessing the repository doesn't imply that
the user is finished browsing libreddit, this seemed reasonable. This
also changes the link from `spikecodes/libreddit` to
`libreddit/libreddit`.
2023-01-01 13:43:33 -05:00
9178b50b73 fix a11y and HTML issues on settings page (#662)
- connect labels with corresponding form controls
- use fieldsets to group form sections
- don't nest details/summary element into label
2023-01-01 01:56:09 -07:00
b5d04f1a50 v0.25.2 2022-12-31 21:34:15 -07:00
9e434e7db6 Search - add support for raw reddit links (#663)
* Search - add support for raw reddit links

If a search query starts with 'https://www.reddit.com/' or 'https://old.reddit.com/',
this prefix will be truncated and the query will be processed normally.
For example, a search query 'https://www.reddit.com/r/rust' will redirect to
r/rust.

* Search - support a wider variety of reddit links.

Add once cell dependency for static regex support (avoid compiling the
same regex multiple times).
All search queries are now matched against a regex (provided by @Daniel-Valentine)
that determines if it is a reddit link. If it is, the prefix specifying
the reddit instance will be truncated from the query that will then be
processed normally.
For example, the query 'https://www.reddit.com/r/rust' will be treated
the same way as the query 'r/rust'.
2022-12-31 20:57:42 -07:00
ab30b8bbec Bugfix: 'all posts are hidden because NSFW' when no posts where found (#666)
* Fix 'all_posts_hidden_nsfw' when there are no posts.

If a search query yielded no results and the user set nsfw posts to be
hidden, libreddit would show 'All posts are hidden because they are NSFW.
Enable "Show NSFW posts" in settings to view'. This is fixed by
verifying tnat posts.len > 0 before setting 'all_posts_hidden_nsfw' to
true.

* Add a message when no posts were found.

* Delete 2
2022-12-31 19:11:59 -07:00
37d1939dc0 Fix #658.
Dimensions for embedded video in post are explicitly set only when defined by Reddit.

c/o: NKIPSC <15067635+NKIPSC@users.noreply.github.com>
2022-12-13 21:15:28 -07:00
08a20b89a6 Merge branch 'cache-determine-compressor' 2022-12-10 18:35:38 -07:00
5d518cfc18 Cache result of server::determine_compressor. 2022-12-04 17:56:02 -07:00
7e752b3d81 Fix Docker credential secrets 2022-12-04 11:07:18 -08:00
87729d0daa Use new libreddit org for GitLab and Docker links 2022-12-04 11:05:19 -08:00
dc06ae3b29 Automatically-update Docker Repo description 2022-12-04 11:01:28 -08:00
225380b7d9 Fix workflow to push to new Libreddit Docker repo 2022-12-04 10:57:19 -08:00
18 changed files with 192 additions and 143 deletions

View File

@ -33,6 +33,6 @@ jobs:
file: ./Dockerfile.arm file: ./Dockerfile.arm
platforms: linux/arm64 platforms: linux/arm64
push: true push: true
tags: spikecodes/libreddit:arm tags: libreddit/libreddit:arm
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@ -36,6 +36,6 @@ jobs:
file: ./Dockerfile.armv7 file: ./Dockerfile.armv7
platforms: linux/arm/v7 platforms: linux/arm/v7
push: true push: true
tags: spikecodes/libreddit:armv7 tags: libreddit/libreddit:armv7
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

View File

@ -26,6 +26,12 @@ jobs:
with: with:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
- name: Docker Hub Description
uses: peter-evans/dockerhub-description@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: libreddit/libreddit
- name: Build and push - name: Build and push
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
@ -33,6 +39,6 @@ jobs:
file: ./Dockerfile file: ./Dockerfile
platforms: linux/amd64 platforms: linux/amd64
push: true push: true
tags: spikecodes/libreddit:latest tags: libreddit/libreddit:latest
cache-from: type=gha cache-from: type=gha
cache-to: type=gha,mode=max cache-to: type=gha,mode=max

3
Cargo.lock generated
View File

@ -664,7 +664,7 @@ dependencies = [
[[package]] [[package]]
name = "libreddit" name = "libreddit"
version = "0.25.0" version = "0.25.3"
dependencies = [ dependencies = [
"askama", "askama",
"async-recursion", "async-recursion",
@ -677,6 +677,7 @@ dependencies = [
"hyper-rustls", "hyper-rustls",
"libflate", "libflate",
"lipsum", "lipsum",
"once_cell",
"percent-encoding", "percent-encoding",
"regex", "regex",
"route-recognizer", "route-recognizer",

View File

@ -3,7 +3,7 @@ name = "libreddit"
description = " Alternative private front-end to Reddit" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0" license = "AGPL-3.0"
repository = "https://github.com/spikecodes/libreddit" repository = "https://github.com/spikecodes/libreddit"
version = "0.25.0" version = "0.25.3"
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"] authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
edition = "2021" edition = "2021"
@ -27,6 +27,12 @@ url = "2.3.1"
rust-embed = { version = "6.4.2", features = ["include-exclude"] } rust-embed = { version = "6.4.2", features = ["include-exclude"] }
libflate = "1.2.0" libflate = "1.2.0"
brotli = { version = "3.3.4", features = ["std"] } brotli = { version = "3.3.4", features = ["std"] }
once_cell = "1.16.0"
[dev-dependencies] [dev-dependencies]
lipsum = "0.8.2" lipsum = "0.8.2"
[profile.release]
codegen-units = 1
lto = true
strip = true

View File

@ -39,7 +39,7 @@ Both files are part of the [libreddit-instances](https://github.com/libreddit/li
# About # About
Find Libreddit on 💬 [Matrix](https://matrix.to/#/#libreddit:kde.org), 🐋 [Docker](https://hub.docker.com/r/spikecodes/libreddit), :octocat: [GitHub](https://github.com/libreddit/libreddit), and 🦊 [GitLab](https://gitlab.com/spikecodes/libreddit). Find Libreddit on 💬 [Matrix](https://matrix.to/#/#libreddit:kde.org), 🐋 [Docker](https://hub.docker.com/r/libreddit/libreddit), :octocat: [GitHub](https://github.com/libreddit/libreddit), and 🦊 [GitLab](https://gitlab.com/libreddit/libreddit).
## Built with ## Built with
@ -136,21 +136,21 @@ cargo install libreddit
## 2) Docker ## 2) Docker
Deploy the [Docker image](https://hub.docker.com/r/spikecodes/libreddit) of Libreddit: Deploy the [Docker image](https://hub.docker.com/r/libreddit/libreddit) of Libreddit:
``` ```
docker pull spikecodes/libreddit docker pull libreddit/libreddit
docker run -d --name libreddit -p 8080:8080 spikecodes/libreddit docker run -d --name libreddit -p 8080:8080 libreddit/libreddit
``` ```
Deploy using a different port (in this case, port 80): Deploy using a different port (in this case, port 80):
``` ```
docker pull spikecodes/libreddit docker pull libreddit/libreddit
docker run -d --name libreddit -p 80:8080 spikecodes/libreddit docker run -d --name libreddit -p 80:8080 libreddit/libreddit
``` ```
To deploy on `arm64` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:arm`. To deploy on `arm64` platforms, simply replace `libreddit/libreddit` in the commands above with `libreddit/libreddit:arm`.
To deploy on `armv7` platforms, simply replace `spikecodes/libreddit` in the commands above with `spikecodes/libreddit:armv7`. To deploy on `armv7` platforms, simply replace `libreddit/libreddit` in the commands above with `libreddit/libreddit:armv7`.
## 3) AUR ## 3) AUR

View File

@ -289,7 +289,7 @@ async fn main() {
Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await, Some("best" | "hot" | "new" | "top" | "rising" | "controversial") => subreddit::community(req).await,
// Short link for post // Short link for post
Some(id) if (5..7).contains(&id.len()) => match canonical_path(format!("/{}", id)).await { Some(id) if (5..8).contains(&id.len()) => match canonical_path(format!("/{}", id)).await {
Ok(path_opt) => match path_opt { Ok(path_opt) => match path_opt {
Some(path) => Ok(redirect(path)), Some(path) => Ok(redirect(path)),
None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await, None => error(req, "Post ID is invalid. It may point to a post on a community that has been banned.").await,

View File

@ -7,6 +7,8 @@ use crate::{
}; };
use askama::Template; use askama::Template;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy;
use regex::Regex;
// STRUCTS // STRUCTS
struct SearchParams { struct SearchParams {
@ -44,13 +46,18 @@ struct SearchTemplate {
all_posts_filtered: bool, all_posts_filtered: bool,
/// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW)
all_posts_hidden_nsfw: bool, all_posts_hidden_nsfw: bool,
no_posts: bool,
} }
// Regex matched against search queries to determine if they are reddit urls.
static REDDIT_URL_MATCH: Lazy<Regex> = Lazy::new(|| Regex::new(r"^https?://([^\./]+\.)*reddit.com/").unwrap());
// SERVICES // SERVICES
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" }; let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results); let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
let query = param(&path, "q").unwrap_or_default(); let mut query = param(&path, "q").unwrap_or_default();
query = REDDIT_URL_MATCH.replace(&query, "").to_string();
if query.is_empty() { if query.is_empty() {
return Ok(redirect("/".to_string())); return Ok(redirect("/".to_string()));
@ -103,12 +110,14 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: true, is_filtered: true,
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false,
}) })
} else { } else {
match Post::fetch(&path, quarantined).await { match Post::fetch(&path, quarantined).await {
Ok((mut posts, after)) => { Ok((mut posts, after)) => {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(SearchTemplate { template(SearchTemplate {
posts, posts,
subreddits, subreddits,
@ -127,6 +136,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: false, is_filtered: false,
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts,
}) })
} }
Err(msg) => { Err(msg) => {

View File

@ -243,7 +243,7 @@ impl Server {
match func.await { match func.await {
Ok(mut res) => { Ok(mut res) => {
res.headers_mut().extend(def_headers); res.headers_mut().extend(def_headers);
let _ = compress_response(req_headers, &mut res).await; let _ = compress_response(&req_headers, &mut res).await;
Ok(res) Ok(res)
} }
@ -282,7 +282,7 @@ async fn new_boilerplate(
) -> Result<Response<Body>, String> { ) -> Result<Response<Body>, String> {
match Response::builder().status(status).body(body) { match Response::builder().status(status).body(body) {
Ok(mut res) => { Ok(mut res) => {
let _ = compress_response(req_headers, &mut res).await; let _ = compress_response(&req_headers, &mut res).await;
res.headers_mut().extend(default_headers.clone()); res.headers_mut().extend(default_headers.clone());
Ok(res) Ok(res)
@ -306,7 +306,8 @@ async fn new_boilerplate(
/// Accept-Encoding: gzip, compress, br /// Accept-Encoding: gzip, compress, br
/// Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1 /// Accept-Encoding: br;q=1.0, gzip;q=0.8, *;q=0.1
/// ``` /// ```
fn determine_compressor(accept_encoding: &str) -> Option<CompressionType> { #[cached]
fn determine_compressor(accept_encoding: String) -> Option<CompressionType> {
if accept_encoding.is_empty() { if accept_encoding.is_empty() {
return None; return None;
}; };
@ -473,7 +474,7 @@ fn determine_compressor(accept_encoding: &str) -> Option<CompressionType> {
/// ///
/// This function logs errors to stderr, but only in debug mode. No information /// This function logs errors to stderr, but only in debug mode. No information
/// is logged in release builds. /// is logged in release builds.
async fn compress_response(req_headers: HeaderMap<header::HeaderValue>, res: &mut Response<Body>) -> Result<(), String> { async fn compress_response(req_headers: &HeaderMap<header::HeaderValue>, res: &mut Response<Body>) -> Result<(), String> {
// Check if the data is eligible for compression. // Check if the data is eligible for compression.
if let Some(hdr) = res.headers().get(header::CONTENT_TYPE) { if let Some(hdr) = res.headers().get(header::CONTENT_TYPE) {
match from_utf8(hdr.as_bytes()) { match from_utf8(hdr.as_bytes()) {
@ -503,30 +504,22 @@ async fn compress_response(req_headers: HeaderMap<header::HeaderValue>, res: &mu
return Ok(()); return Ok(());
}; };
// Quick and dirty closure for extracting a header from the request and
// returning it as a &str.
let get_req_header = |k: header::HeaderName| -> Option<&str> {
match req_headers.get(k) {
Some(hdr) => match from_utf8(hdr.as_bytes()) {
Ok(val) => Some(val),
#[cfg(debug_assertions)]
Err(e) => {
dbg_msg!(e);
None
}
#[cfg(not(debug_assertions))]
Err(_) => None,
},
None => None,
}
};
// Check to see which compressor is requested, and if we can use it. // Check to see which compressor is requested, and if we can use it.
let accept_encoding: &str = match get_req_header(header::ACCEPT_ENCODING) { let accept_encoding: String = match req_headers.get(header::ACCEPT_ENCODING) {
Some(val) => val,
None => return Ok(()), // Client requested no compression. None => return Ok(()), // Client requested no compression.
Some(hdr) => match String::from_utf8(hdr.as_bytes().into()) {
Ok(val) => val,
#[cfg(debug_assertions)]
Err(e) => {
dbg_msg!(e);
return Ok(());
}
#[cfg(not(debug_assertions))]
Err(_) => return Ok(()),
},
}; };
let compressor: CompressionType = match determine_compressor(accept_encoding) { let compressor: CompressionType = match determine_compressor(accept_encoding) {
@ -636,18 +629,18 @@ mod tests {
#[test] #[test]
fn test_determine_compressor() { fn test_determine_compressor() {
// Single compressor given. // Single compressor given.
assert_eq!(determine_compressor("unsupported"), None); assert_eq!(determine_compressor("unsupported".to_string()), None);
assert_eq!(determine_compressor("gzip"), Some(CompressionType::Gzip)); assert_eq!(determine_compressor("gzip".to_string()), Some(CompressionType::Gzip));
assert_eq!(determine_compressor("*"), Some(DEFAULT_COMPRESSOR)); assert_eq!(determine_compressor("*".to_string()), Some(DEFAULT_COMPRESSOR));
// Multiple compressors. // Multiple compressors.
assert_eq!(determine_compressor("gzip, br"), Some(CompressionType::Brotli)); assert_eq!(determine_compressor("gzip, br".to_string()), Some(CompressionType::Brotli));
assert_eq!(determine_compressor("gzip;q=0.8, br;q=0.3"), Some(CompressionType::Gzip)); assert_eq!(determine_compressor("gzip;q=0.8, br;q=0.3".to_string()), Some(CompressionType::Gzip));
assert_eq!(determine_compressor("br, gzip"), Some(CompressionType::Brotli)); assert_eq!(determine_compressor("br, gzip".to_string()), Some(CompressionType::Brotli));
assert_eq!(determine_compressor("br;q=0.3, gzip;q=0.4"), Some(CompressionType::Gzip)); assert_eq!(determine_compressor("br;q=0.3, gzip;q=0.4".to_string()), Some(CompressionType::Gzip));
// Invalid q-values. // Invalid q-values.
assert_eq!(determine_compressor("gzip;q=NAN"), None); assert_eq!(determine_compressor("gzip;q=NAN".to_string()), None);
} }
#[test] #[test]
@ -672,9 +665,9 @@ mod tests {
] { ] {
// Determine what the expected encoding should be based on both the // Determine what the expected encoding should be based on both the
// specific encodings we accept. // specific encodings we accept.
let expected_encoding: CompressionType = match determine_compressor(accept_encoding) { let expected_encoding: CompressionType = match determine_compressor(accept_encoding.to_string()) {
Some(s) => s, Some(s) => s,
None => panic!("determine_compressor(accept_encoding) => None"), None => panic!("determine_compressor(accept_encoding.to_string()) => None"),
}; };
// Build headers with our Accept-Encoding. // Build headers with our Accept-Encoding.
@ -691,8 +684,8 @@ mod tests {
.unwrap(); .unwrap();
// Perform the compression. // Perform the compression.
if let Err(e) = block_on(compress_response(req_headers, &mut res)) { if let Err(e) = block_on(compress_response(&req_headers, &mut res)) {
panic!("compress_response(req_headers, &mut res) => Err(\"{}\")", e); panic!("compress_response(&req_headers, &mut res) => Err(\"{}\")", e);
}; };
// If the content was compressed, we expect the Content-Encoding // If the content was compressed, we expect the Content-Encoding
@ -739,9 +732,8 @@ mod tests {
}; };
let mut decompressed = Vec::<u8>::new(); let mut decompressed = Vec::<u8>::new();
match io::copy(&mut decoder, &mut decompressed) { if let Err(e) = io::copy(&mut decoder, &mut decompressed) {
Ok(_) => {} panic!("{}", e);
Err(e) => panic!("{}", e),
}; };
assert!(decompressed.eq(&expected_lorem_ipsum)); assert!(decompressed.eq(&expected_lorem_ipsum));

View File

@ -26,6 +26,7 @@ struct SubredditTemplate {
all_posts_filtered: bool, all_posts_filtered: bool,
/// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW)
all_posts_hidden_nsfw: bool, all_posts_hidden_nsfw: bool,
no_posts: bool,
} }
#[derive(Template)] #[derive(Template)]
@ -114,12 +115,14 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: true, is_filtered: true,
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false,
}) })
} else { } else {
match Post::fetch(&path, quarantined).await { match Post::fetch(&path, quarantined).await {
Ok((mut posts, after)) => { Ok((mut posts, after)) => {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(SubredditTemplate { template(SubredditTemplate {
sub, sub,
posts, posts,
@ -131,6 +134,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: false, is_filtered: false,
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts,
}) })
} }
Err(msg) => match msg.as_str() { Err(msg) => match msg.as_str() {

View File

@ -26,6 +26,7 @@ struct UserTemplate {
all_posts_filtered: bool, all_posts_filtered: bool,
/// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW) /// Whether all posts were hidden because they are NSFW (and user has disabled show NSFW)
all_posts_hidden_nsfw: bool, all_posts_hidden_nsfw: bool,
no_posts: bool,
} }
// FUNCTIONS // FUNCTIONS
@ -61,13 +62,15 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: true, is_filtered: true,
all_posts_filtered: false, all_posts_filtered: false,
all_posts_hidden_nsfw: false, all_posts_hidden_nsfw: false,
no_posts: false,
}) })
} else { } else {
// Request user posts/comments from Reddit // Request user posts/comments from Reddit
match Post::fetch(&path, false).await { match Post::fetch(&path, false).await {
Ok((mut posts, after)) => { Ok((mut posts, after)) => {
let (_, all_posts_filtered) = filter_posts(&mut posts, &filters); let (_, all_posts_filtered) = filter_posts(&mut posts, &filters);
let all_posts_hidden_nsfw = posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on"; let no_posts = posts.is_empty();
let all_posts_hidden_nsfw = !no_posts && (posts.iter().all(|p| p.flags.nsfw) && setting(&req, "show_nsfw") != "on");
template(UserTemplate { template(UserTemplate {
user, user,
posts, posts,
@ -80,6 +83,7 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
is_filtered: false, is_filtered: false,
all_posts_filtered, all_posts_filtered,
all_posts_hidden_nsfw, all_posts_hidden_nsfw,
no_posts,
}) })
} }
// If there is an error show error page // If there is an error show error page

View File

@ -1118,22 +1118,16 @@ summary.comment_data {
} }
.prefs { .prefs {
display: flex; padding: 10px 20px 20px;
flex-direction: column;
justify-content: space-between;
padding: 20px;
background: var(--post); background: var(--post);
border-radius: 5px; border-radius: 5px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.prefs > div { .prefs fieldset {
display: flex; border: 0;
justify-content: space-between; padding: 10px 0;
width: 100%; margin: 0 0 5px;
height: 35px;
align-items: center;
margin-top: 7px;
} }
.prefs legend { .prefs legend {
@ -1141,11 +1135,25 @@ summary.comment_data {
border-bottom: 1px solid var(--highlighted); border-bottom: 1px solid var(--highlighted);
font-size: 18px; font-size: 18px;
padding-bottom: 10px; padding-bottom: 10px;
margin-bottom: 7px;
width: 100%;
float: left; /* places the legend inside the (invisible) border, instead of vertically centered on top border*/
} }
.prefs legend:not(:first-child) { .prefs-group {
padding-top: 10px; display: flex;
margin-top: 15px; width: 100%;
height: 35px;
align-items: center;
margin-top: 7px;
}
.prefs-group > *:not(:last-child) {
margin-right: 1ch;
}
.prefs-group > *:last-child {
margin-left: auto;
} }
.prefs select { .prefs select {
@ -1163,7 +1171,8 @@ aside.prefs {
background: var(--highlighted); background: var(--highlighted);
padding: 10px 15px; padding: 10px 15px;
border-radius: 5px; border-radius: 5px;
margin-top: 20px; margin-top: 5px;
width: 100%
} }
input[type="submit"] { input[type="submit"] {

View File

@ -48,7 +48,7 @@
<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/> <circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
</svg> </svg>
</a> </a>
<a id="code" href="https://github.com/spikecodes/libreddit"> <a id="code" href="https://github.com/libreddit/libreddit" target="_blank" rel="noopener noreferrer">
<span>code</span> <span>code</span>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<title>code</title> <title>code</title>

View File

@ -61,6 +61,10 @@
<span class="listing_warn">All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</span> <span class="listing_warn">All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</span>
{% endif %} {% endif %}
{% if no_posts %}
<center>No posts were found.</center>
{% endif %}
{% if all_posts_filtered %} {% if all_posts_filtered %}
<span class="listing_warn">(All content on this page has been filtered)</span> <span class="listing_warn">(All content on this page has been filtered)</span>
{% else if is_filtered %} {% else if is_filtered %}

View File

@ -11,74 +11,79 @@
<div id="settings"> <div id="settings">
<form action="/settings" method="POST"> <form action="/settings" method="POST">
<div class="prefs"> <div class="prefs">
<legend>Appearance</legend> <fieldset>
<div id="theme"> <legend>Appearance</legend>
<label for="theme">Theme:</label> <div class="prefs-group">
<select name="theme"> <label for="theme">Theme:</label>
{% call utils::options(prefs.theme, prefs.available_themes, "system") %} <select name="theme" id="theme">
</select> {% call utils::options(prefs.theme, prefs.available_themes, "system") %}
</div> </select>
<legend>Interface</legend> </div>
<div id="front_page"> </fieldset>
<label for="front_page">Front page:</label> <fieldset>
<select name="front_page"> <legend>Interface</legend>
{% call utils::options(prefs.front_page, ["default", "popular", "all"], "default") %} <div class="prefs-group">
</select> <label for="front_page">Front page:</label>
</div> <select name="front_page" id="front_page">
<div id="layout"> {% call utils::options(prefs.front_page, ["default", "popular", "all"], "default") %}
<label for="layout">Layout:</label> </select>
<select name="layout"> </div>
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %} <div class="prefs-group">
</select> <label for="layout">Layout:</label>
</div> <select name="layout" id="layout">
<div id="wide"> {% call utils::options(prefs.layout, ["card", "clean", "compact"], "card") %}
<label for="wide">Wide UI:</label> </select>
<input type="hidden" value="off" name="wide"> </div>
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}> <div class="prefs-group">
</div> <label for="wide">Wide UI:</label>
<legend>Content</legend> <input type="hidden" value="off" name="wide">
<div id="post_sort"> <input type="checkbox" name="wide" id="wide" {% if prefs.wide == "on" %}checked{% endif %}>
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label> </div>
<select name="post_sort"> </fieldset>
{% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") %} <fieldset>
</select> <legend>Content</legend>
</div> <div class="prefs-group">
<div id="comment_sort"> <label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
<label for="comment_sort">Default comment sort:</label> <select name="post_sort">
<select name="comment_sort"> {% call utils::options(prefs.post_sort, ["hot", "new", "top", "rising", "controversial"], "hot") %}
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %} </select>
</select> </div>
</div> <div class="prefs-group">
<div id="show_nsfw"> <label for="comment_sort">Default comment sort:</label>
<label for="show_nsfw">Show NSFW posts:</label> <select name="comment_sort" id="comment_sort">
<input type="hidden" value="off" name="show_nsfw"> {% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
<input type="checkbox" name="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}> </select>
</div> </div>
<div id="blur_nsfw"> <div class="prefs-group">
<label for="blur_nsfw">Blur NSFW previews:</label> <label for="show_nsfw">Show NSFW posts:</label>
<input type="hidden" value="off" name="blur_nsfw"> <input type="hidden" value="off" name="show_nsfw">
<input type="checkbox" name="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}> <input type="checkbox" name="show_nsfw" id="show_nsfw" {% if prefs.show_nsfw == "on" %}checked{% endif %}>
</div> </div>
<div id="autoplay_videos"> <div class="prefs-group">
<label for="autoplay_videos">Autoplay videos</label> <label for="blur_nsfw">Blur NSFW previews:</label>
<input type="hidden" value="off" name="autoplay_videos"> <input type="hidden" value="off" name="blur_nsfw">
<input type="checkbox" name="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}> <input type="checkbox" name="blur_nsfw" id="blur_nsfw" {% if prefs.blur_nsfw == "on" %}checked{% endif %}>
</div> </div>
<div id="use_hls"> <div class="prefs-group">
<label for="use_hls">Use HLS for videos <label for="autoplay_videos">Autoplay videos</label>
<input type="hidden" value="off" name="autoplay_videos">
<input type="checkbox" name="autoplay_videos" id="autoplay_videos" {% if prefs.autoplay_videos == "on" %}checked{% endif %}>
</div>
<div class="prefs-group">
<label for="use_hls">Use HLS for videos</label>
<details id="feeds"> <details id="feeds">
<summary>Why?</summary> <summary>Why?</summary>
<div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Libreddit JS-free or utilize this feature.</div> <div id="feed_list" class="helper">Reddit videos require JavaScript (via HLS.js) to be enabled to be played with audio. Therefore, this toggle lets you either use Libreddit JS-free or utilize this feature.</div>
</details> </details>
</label> <input type="hidden" value="off" name="use_hls">
<input type="hidden" value="off" name="use_hls"> <input type="checkbox" name="use_hls" id="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}>
<input type="checkbox" name="use_hls" {% if prefs.use_hls == "on" %}checked{% endif %}> </div>
</div> <div class="prefs-group">
<div id="hide_hls_notification"> <label for="hide_hls_notification">Hide notification about possible HLS usage</label>
<label for="hide_hls_notification">Hide notification about possible HLS usage</label> <input type="hidden" value="off" name="hide_hls_notification">
<input type="hidden" value="off" name="hide_hls_notification"> <input type="checkbox" name="hide_hls_notification" id="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}>
<input type="checkbox" name="hide_hls_notification" {% if prefs.hide_hls_notification == "on" %}checked{% endif %}> </div>
</div> </fieldset>
<input id="save" type="submit" value="Save"> <input id="save" type="submit" value="Save">
</div> </div>
</form> </form>

View File

@ -50,6 +50,10 @@
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center> <center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
{% endif %} {% endif %}
{% if no_posts %}
<center>No posts were found.</center>
{% endif %}
{% if all_posts_filtered %} {% if all_posts_filtered %}
<center>(All content on this page has been filtered)</center> <center>(All content on this page has been filtered)</center>
{% else %} {% else %}

View File

@ -36,6 +36,10 @@
<center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center> <center>All posts are hidden because they are NSFW. Enable "Show NSFW posts" in settings to view.</center>
{% endif %} {% endif %}
{% if no_posts %}
<center>No posts were found.</center>
{% endif %}
{% if all_posts_filtered %} {% if all_posts_filtered %}
<center>(All content on this page has been filtered)</center> <center>(All content on this page has been filtered)</center>
{% else %} {% else %}

View File

@ -115,7 +115,7 @@
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<script src="/hls.min.js"></script> <script src="/hls.min.js"></script>
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls> <video class="post_media_video short {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls>
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
@ -213,19 +213,19 @@
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "gif" %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls loop {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %} {% else if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "video" %}
{% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %} {% if prefs.use_hls == "on" && !post.media.alt_url.is_empty() %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" controls preload="none"> <video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %} {% if prefs.autoplay_videos == "on" %}hls_autoplay{% endif %}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" controls preload="none">
<source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" /> <source src="{{ post.media.alt_url }}" type="application/vnd.apple.mpegurl" />
<source src="{{ post.media.url }}" type="video/mp4" /> <source src="{{ post.media.url }}" type="video/mp4" />
</video> </video>
</div> </div>
{% else %} {% else %}
<div class="post_media_content"> <div class="post_media_content">
<video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" width="{{ post.media.width }}" height="{{ post.media.height }}" poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video> <video class="post_media_video short {%if post.flags.nsfw && prefs.blur_nsfw=="on" %}post_nsfw_blur{% endif %}" src="{{ post.media.url }}" {% if post.media.width > 0 && post.media.height > 0 %}width="{{ post.media.width }}" height="{{ post.media.height }}"{% endif %} poster="{{ post.media.poster }}" preload="none" controls {% if prefs.autoplay_videos == "on" %}autoplay{% endif %}><a href={{ post.media.url }}>Video</a></video>
</div> </div>
{% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %} {% call render_hls_notification(format!("{}%23{}", &self.url[1..].replace("&", "%26").replace("+", "%2B"), post.id)) %}
{% endif %} {% endif %}