Compare commits

..

51 Commits

Author SHA1 Message Date
d7cff4203a v0.35.4 2025-01-07 07:28:00 +13:00
86cafe134e actually fixes #10 #11 2025-01-06 16:34:17 +13:00
fedd76877c add "dprksoldier" mascot (closes #15) 2025-01-06 16:03:50 +13:00
fcfd4dd196 Update README.md
fix screenshot without a broken grid
2025-01-06 15:41:22 +13:00
0081205ace fix details grid 2025-01-06 15:13:22 +13:00
482b528f49 video download style change 2025-01-06 14:38:17 +13:00
b6d828727c Update README.md 2025-01-06 13:09:39 +13:00
fe9128a5e2 Merge branch 'quicklist' 2025-01-06 12:44:33 +13:00
836cb15946 minimum viable product 2025-01-06 12:33:49 +13:00
3a5abd6c9d shutup warning 2025-01-06 12:01:48 +13:00
fd0b0968dd Merge pull request 'style-gimmicks' (#13) from style-gimmicks into main
Reviewed-on: #13
2025-01-06 09:42:14 +13:00
e94d07274d gimmick for "style-gimmicks" 2025-01-06 09:38:08 +13:00
85dab5e070 add quicklist to subreddit 2025-01-06 09:32:52 +13:00
5959464bbe fix typo, lol.. 2025-01-06 08:04:41 +13:00
dbb13f73f4 icon change 2025-01-06 07:59:03 +13:00
10c7327c39 colorway toggle 2025-01-06 07:15:24 +13:00
ad39254ddc fix followed user rendering 2024-12-22 04:25:54 +13:00
9856b7fd47 create basic quicklist functionality 2024-12-22 04:04:43 +13:00
e478575706 Add mascot: BoymoderHoodie 2024-12-22 04:02:54 +13:00
dc84d9b503 unfinished checkupdate changes (needs fixing(soon)) 2024-11-21 00:58:15 +13:00
7afc0d006c Merge remote-tracking branch 'upstream/main' 2024-11-21 00:53:33 +13:00
d3ba5f3efb feat(error): add new instance buttom 2024-11-19 16:30:37 -05:00
cb9a2a3c39 fix(client): revert to hyper_rustls :P hi SWE 👋 2024-11-19 15:48:42 -05:00
6ecdedd2ed feat(client): additionally randomize headers 2024-11-19 14:54:06 -05:00
18efb8c714 fix(client): update headers 2024-11-19 14:10:59 -05:00
0bc36d529c Add Quadlet Container File (#319)
* Add Quadlet Container File

* Update README.md with Quadlet instructions
2024-11-19 13:19:48 -05:00
96ebfd2d3a fix(ci): statically build on artifacts 2024-11-19 12:53:36 -05:00
3e1718bfc9 fix(client): ??? no accept language 2024-11-19 12:44:20 -05:00
96e40e8887 style(clippy): small clippy change 2024-11-19 11:40:17 -05:00
f8a9ad363d chore(deps): updates 2024-11-19 11:37:30 -05:00
f7240208f1 fix(tls): vendor native-tls 2024-11-19 11:18:20 -05:00
0714d58efe fix(ci): install new openssl requirements 2024-11-19 11:12:04 -05:00
a96bebb099 fix(client): switch to hyper-tls 2024-11-19 11:08:00 -05:00
6c64ebd56b fix(scraper): additionally grab common words 2024-11-15 16:53:00 -05:00
62717ef6b2 fix: update error template 2024-11-14 11:49:47 -05:00
a301afc383 fix(scraper): truncate to post count 2024-11-13 16:43:41 -05:00
6a18ea17ec Use quotes for kaniko to expand ARG in Dockerfile (#314) 2024-11-10 20:19:40 -05:00
feedc572cd change bin name 2024-11-03 10:36:46 +13:00
a3bc16f7d2 port update checker to gitea compatible version 2024-11-03 10:08:26 +13:00
bd4cb96c0f "fix" scraper 2024-11-03 09:36:11 +13:00
a9c99cc752 Merge remote-tracking branch 'upstream/main' 2024-11-03 09:34:12 +13:00
f03bdcf472 feat: display whether or not the instance is up to date on error (#310) 2024-11-01 18:16:25 -04:00
2fd358f3ed feat(hls): add video quality preference (#306) 2024-11-01 12:28:52 -04:00
5ef57812f8 style: fix clippy 2024-11-01 11:39:05 -04:00
d17d097b12 Fix parts of CI (#304)
* Run cargo fmt, hide clippy::cmp_owned errors

* Bump deps

* Fix failing test

* Update src/client.rs

---------

Co-authored-by: Matthew Esposito <matt@matthew.science>
2024-10-31 22:50:50 -04:00
a96894c743 enables http2 crate feature, replaces http1 protocol with http2 on co… (#305) 2024-10-31 22:48:19 -04:00
9aea9c90a2 fix: reduce to minimum patch, fix clippy 2024-10-31 16:09:35 -04:00
efdf1848ac fix: emergency patch for 403 2024-10-31 16:06:29 -04:00
bc9530821d feat(scraper): add output file 2024-10-30 15:15:38 -04:00
f3d2f0cc59 feat(scraper): add scraper CLI 2024-10-21 20:54:05 -04:00
49ef59e000 chore: make library 2024-10-21 20:46:03 -04:00
30 changed files with 1289 additions and 595 deletions

View File

@ -16,6 +16,8 @@ REDLIB_PUSHSHIFT_FRONTEND=undelete.pullpush.io
REDLIB_DEFAULT_THEME=system
# Set the default mascot
REDLIB_DEFAULT_MASCOT=none
# Enable showing redsunlib colorway by default
REDLIB_DEFAULT_REDSUNLIB_COLORWAY=off
# Set the default front page (options: default, popular, all)
REDLIB_DEFAULT_FRONT_PAGE=default
# Set the default layout (options: card, clean, compact)

1171
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,29 +3,30 @@ name = "redsunlib"
description = " Alternative private front-end to Reddit"
license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.3"
version = "0.35.4"
authors = [
"Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>",
]
edition = "2021"
default-run = "redsunlib"
[dependencies]
rinja = { version = "0.3.4", default-features = false }
cached = { version = "0.51.3", features = ["async"] }
cached = { version = "0.54.0", features = ["async"] }
clap = { version = "4.4.11", default-features = false, features = [
"std",
"env",
"derive",
] }
regex = "1.10.2"
serde = { version = "1.0.193", features = ["derive"] }
cookie = "0.18.0"
futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] }
hyper-rustls = "0.24.2"
hyper = { version = "0.14.31", features = ["full"] }
percent-encoding = "2.3.1"
route-recognizer = "0.3.1"
serde_json = "1.0.108"
serde_json = "1.0.133"
tokio = { version = "1.35.1", features = ["full"] }
time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0"
@ -44,8 +45,10 @@ pretty_env_logger = "0.5.0"
dotenvy = "0.15.7"
rss = "2.0.7"
arc-swap = "1.7.1"
serde_json_path = "0.6.7"
serde_json_path = "0.7.1"
async-recursion = "1.1.1"
common-words-all = { version = "0.0.2", default-features = false, features = ["english", "one"] }
hyper-rustls = { version = "0.24.2", features = [ "http2" ] }
[dev-dependencies]
@ -56,3 +59,11 @@ sealed_test = "1.0.0"
codegen-units = 1
lto = true
strip = "symbols"
[[bin]]
name = "redsunlib"
path = "src/main.rs"
[[bin]]
name = "scraper"
path = "src/scraper/main.rs"

View File

@ -5,7 +5,7 @@
<br>
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20)
![screenshot](https://git.stardust.wtf/attachments/ccf81f52-e653-4722-94b9-b370c58d6359)
### Disclaimer

16
redlib.container Normal file
View File

@ -0,0 +1,16 @@
[Install]
WantedBy=default.target
[Container]
AutoUpdate=registry
ContainerName=redlib
DropCapability=ALL
EnvironmentFile=.env
HealthCmd=["wget","--spider","-q","--tries=1","http://localhost:8080/settings"]
HealthInterval=5m
HealthTimeout=3s
Image=quay.io/redlib/redlib:latest
NoNewPrivileges=true
PublishPort=8080:8080
ReadOnly=true
User=nobody

View File

@ -4,7 +4,7 @@ use futures_lite::future::block_on;
use futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector;
use hyper::header::HeaderValue;
use hyper::{body, body::Buf, client, header, Body, Client, Method, Request, Response, Uri};
use hyper::{body, body::Buf, header, Body, Client, Method, Request, Response, Uri};
use hyper_rustls::HttpsConnector;
use libflate::gzip;
use log::{error, trace, warn};
@ -30,10 +30,10 @@ const REDDIT_SHORT_URL_BASE_HOST: &str = "redd.it";
const ALTERNATIVE_REDDIT_URL_BASE: &str = "https://www.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| {
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
client::Client::builder().build(https)
});
pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| Client::builder().build::<_, Body>(HTTPS_CONNECTOR.clone()));
pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::new());
@ -154,7 +154,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Build the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone();
let client: &Lazy<Client<_, Body>> = &CLIENT;
let mut builder = Request::get(parsed_uri);
@ -216,7 +216,7 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
let url = format!("{base_path}{path}");
// Construct the hyper client from the HTTPS connector.
let client: Client<_, Body> = CLIENT.clone();
let client: &Lazy<Client<_, Body>> = &CLIENT;
let (token, vendor_id, device_id, user_agent, loid) = {
let client = OAUTH_CLIENT.load_full();
@ -231,27 +231,37 @@ fn request(method: &'static Method, path: String, redirect: bool, quarantine: bo
// Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.)
let builder = Request::builder()
.method(method)
.uri(&url)
.header("User-Agent", user_agent)
.header("Client-Vendor-Id", vendor_id)
.header("X-Reddit-Device-Id", device_id)
.header("x-reddit-loid", loid)
.header("Host", host)
.header("Authorization", &format!("Bearer {token}"))
.header("Accept-Encoding", if method == Method::GET { "gzip" } else { "identity" })
.header("Accept-Language", "en-US,en;q=0.5")
.header("Connection", "keep-alive")
.header(
let mut headers = vec![
("User-Agent", user_agent),
("Client-Vendor-Id", vendor_id),
("X-Reddit-Device-Id", device_id),
("x-reddit-loid", loid),
("Host", host.to_string()),
("Authorization", format!("Bearer {token}")),
("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }),
(
"Cookie",
if quarantine {
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D"
"_options=%7B%22pref_quarantine_optin%22%3A%20true%2C%20%22pref_gated_sr_optin%22%3A%20true%7D".into()
} else {
""
"".into()
},
)
.body(Body::empty());
),
("X-Reddit-Width", fastrand::u32(300..500).to_string()),
("X-Reddit-DPR", "2".to_owned()),
("Device-Name", format!("Android {}", fastrand::u8(9..=14))),
];
// shuffle headers: https://github.com/redlib-org/redlib/issues/324
fastrand::shuffle(&mut headers);
let mut builder = Request::builder().method(method).uri(&url);
for (key, value) in headers {
builder = builder.header(key, value);
}
let builder = builder.body(Body::empty());
async move {
match builder {

View File

@ -96,6 +96,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_QUICKLIST")]
#[serde(alias = "LIBREDDIT_DEFAULT_QUICKLIST")]
pub(crate) default_quicklist: Option<String>,
#[serde(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -160,6 +164,7 @@ impl Config {
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_quicklist: parse("REDLIB_DEFAULT_QUICKLIST"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"),
@ -191,6 +196,7 @@ fn get_setting_from_config(name: &str, config: &Config) -> Option<String> {
"REDLIB_DEFAULT_HIDE_SCORE" => config.default_hide_score.clone(),
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"REDLIB_DEFAULT_FILTERS" => config.default_filters.clone(),
"REDLIB_DEFAULT_QUICKLIST" => config.default_quicklist.clone(),
"REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.clone(),
@ -271,6 +277,12 @@ fn test_default_filters() {
assert_eq!(get_setting("REDLIB_DEFAULT_FILTERS"), Some("news+bestof".into()));
}
#[test]
#[sealed_test(env = [("REDLIB_DEFAULT_QUICKLIST", "news+popular")])]
fn test_default_quicklist() {
assert_eq!(get_setting("REDLIB_DEFAULT_QUICKLIST"), Some("news+popular".into()));
}
#[test]
#[sealed_test]
fn test_pushshift() {

View File

@ -85,7 +85,7 @@ fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
pub struct InstanceInfo {
package_name: String,
crate_version: String,
git_commit: String,
pub git_commit: String,
deploy_date: String,
compile_mode: String,
deploy_unix_ts: i64,
@ -152,6 +152,7 @@ impl InstanceInfo {
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)],
["Quick Access Feeds", &convert(&self.config.default_quicklist)],
])
.with_header_row(["Default preferences"]),
);
@ -189,7 +190,8 @@ impl InstanceInfo {
Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n
Default filters: {:?}\n",
Default filters: {:?}\n
Default quicklist: {:?}\n",
self.package_name,
self.crate_version,
self.git_commit,
@ -218,6 +220,7 @@ impl InstanceInfo {
self.config.default_hide_hls_notification,
self.config.default_subscriptions,
self.config.default_filters,
self.config.default_quicklist,
)
}
StringType::Html => self.to_table(),

13
src/lib.rs Normal file
View File

@ -0,0 +1,13 @@
pub mod client;
pub mod config;
pub mod duplicates;
pub mod instance_info;
pub mod oauth;
pub mod oauth_resources;
pub mod post;
pub mod search;
pub mod server;
pub mod settings;
pub mod subreddit;
pub mod user;
pub mod utils;

View File

@ -2,35 +2,21 @@
#![forbid(unsafe_code)]
#![allow(clippy::cmp_owned)]
// Reference local files
mod config;
mod duplicates;
mod instance_info;
mod oauth;
mod oauth_resources;
mod post;
mod search;
mod settings;
mod subreddit;
mod user;
mod utils;
// Import Crates
use cached::proc_macro::cached;
use clap::{Arg, ArgAction, Command};
use std::str::FromStr;
use futures_lite::FutureExt;
use hyper::Uri;
use hyper::{header::HeaderValue, Body, Request, Response};
mod client;
use client::{canonical_path, proxy};
use log::info;
use once_cell::sync::Lazy;
use server::RequestExt;
use utils::{error, redirect, ThemeAssets, MascotAssets};
use redsunlib::client::{canonical_path, proxy, CLIENT};
use redsunlib::server::{self, RequestExt};
use redsunlib::utils::{error, redirect, ThemeAssets, MascotAssets};
use redsunlib::{config, duplicates, headers, instance_info, post, search, settings, subreddit, user};
use crate::client::OAUTH_CLIENT;
mod server;
use redsunlib::client::OAUTH_CLIENT;
// Create Services
@ -257,6 +243,13 @@ async fn main() {
app
.at("/highlighted.js")
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed());
app
.at("/check_update.js")
.get(|_| resource(include_str!("../static/check_update.js"), "text/javascript", false).boxed());
app.at("/commits.json").get(|_| async move { proxy_commit_info().await }.boxed());
app.at("/instances.json").get(|_| async move { proxy_instances().await }.boxed());
// FFmpeg
app
.at("/ffmpeg/814.ffmpeg.js")
@ -330,10 +323,12 @@ async fn main() {
.at("/r/u_:name")
.get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed());
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(r).boxed());
app.at("/r/:sub/filter").post(|r| subreddit::subscriptions_filters(r).boxed());
app.at("/r/:sub/unfilter").post(|r| subreddit::subscriptions_filters(r).boxed());
app.at("/r/:sub/subscribe").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/filter").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unfilter").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/quicklist").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unquicklist").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/comments/:id").get(|r| post::item(r).boxed());
app.at("/r/:sub/comments/:id/:title").get(|r| post::item(r).boxed());
@ -437,3 +432,41 @@ async fn main() {
eprintln!("Server error: {e}");
}
}
pub async fn proxy_commit_info() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/atom+xml")
.body(Body::from(fetch_commit_info().await))
.unwrap_or_default(),
)
}
#[cached(time = 600)]
async fn fetch_commit_info() -> String {
let uri = Uri::from_str("https://git.stardust.wtf/api/v1/repos/iridium/redsunlib/commits?verification=false&stat=false").expect("Invalid URI");
let resp: Body = CLIENT.get(uri).await.expect("Failed to request git.stardust.wtf").into_body();
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
}
pub async fn proxy_instances() -> Result<Response<Body>, String> {
Ok(
Response::builder()
.status(200)
.header("content-type", "application/json")
.body(Body::from(fetch_instances().await))
.unwrap_or_default(),
)
}
#[cached(time = 600)]
async fn fetch_instances() -> String {
let uri = Uri::from_str("https://raw.githubusercontent.com/redlib-org/redlib-instances/refs/heads/main/instances.json").expect("Invalid URI");
let resp: Body = CLIENT.get(uri).await.expect("Failed to request GitHub").into_body();
hyper::body::to_bytes(resp).await.expect("Failed to read body").iter().copied().map(|x| x as char).collect()
}

View File

@ -94,7 +94,7 @@ impl Oauth {
trace!("Sending token request...");
// Send request
let client: client::Client<_, Body> = CLIENT.clone();
let client: &once_cell::sync::Lazy<client::Client<_, Body>> = &CLIENT;
let resp = client.request(request).await.ok()?;
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length"));

View File

@ -1,3 +1,5 @@
#![allow(clippy::cmp_owned)]
// CRATES
use crate::client::json;
use crate::config::get_setting;

132
src/scraper/main.rs Normal file
View File

@ -0,0 +1,132 @@
use std::{collections::HashMap, fmt::Display, io::Write};
use clap::{Parser, ValueEnum};
use common_words_all::{get_top, Language, NgramSize};
use redsunlib::utils::Post;
#[derive(Parser)]
#[command(name = "my_cli")]
#[command(about = "A simple CLI example", long_about = None)]
struct Cli {
#[arg(short = 's', long = "sub")]
sub: String,
#[arg(long = "sort")]
sort: SortOrder,
#[arg(short = 'f', long = "format", value_enum)]
format: Format,
#[arg(short = 'o', long = "output")]
output: Option<String>,
}
#[derive(Debug, Clone, ValueEnum)]
enum SortOrder {
Hot,
Rising,
New,
Top,
Controversial,
}
impl Display for SortOrder {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SortOrder::Hot => write!(f, "hot"),
SortOrder::Rising => write!(f, "rising"),
SortOrder::New => write!(f, "new"),
SortOrder::Top => write!(f, "top"),
SortOrder::Controversial => write!(f, "controversial"),
}
}
}
#[derive(Debug, Clone, ValueEnum)]
enum Format {
Json,
}
#[tokio::main]
async fn main() {
pretty_env_logger::init();
let cli = Cli::parse();
let (sub, sort, format, output) = (cli.sub, cli.sort, cli.format, cli.output);
let initial = format!("/r/{sub}/{sort}.json?&raw_json=1");
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
let mut hashmap = HashMap::new();
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
loop {
print!("\r");
let path = format!("/r/{sub}/{sort}.json?sort={sort}&t=&after={after}&raw_json=1");
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
let old_len = hashmap.len();
// convert to hashmap and extend hashmap
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
let len = new_posts.len();
hashmap.extend(new_posts);
if hashmap.len() - old_len < 3 {
break;
}
let x = hashmap.len() - old_len;
after = new_after;
// Print number of posts fetched
print!("Fetched {len} posts (+{x})",);
std::io::stdout().flush().unwrap();
}
println!("\n\n");
// additionally search if final count not reached
for word in get_top(Language::English, 10_000, NgramSize::One) {
let mut retrieved_posts_from_search = 0;
let initial = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}");
println!("Grabbing posts with word {word}.");
let (posts, mut after) = Post::fetch(&initial, false).await.unwrap();
hashmap.extend(posts.into_iter().map(|post| (post.id.clone(), post)));
'search: loop {
let path = format!("/r/{sub}/search.json?q={word}&restrict_sr=on&include_over_18=on&raw_json=1&sort={sort}&after={after}");
let (new_posts, new_after) = Post::fetch(&path, false).await.unwrap();
if new_posts.is_empty() || new_after.is_empty() {
println!("No more posts for word {word}");
break 'search;
}
retrieved_posts_from_search += new_posts.len();
let old_len = hashmap.len();
let new_posts = new_posts.into_iter().map(|post| (post.id.clone(), post)).collect::<HashMap<String, Post>>();
let len = new_posts.len();
hashmap.extend(new_posts);
let delta = hashmap.len() - old_len;
after = new_after;
// Print number of posts fetched
println!("Fetched {len} posts (+{delta})",);
if retrieved_posts_from_search > 1000 {
println!("Reached 1000 posts from search");
break 'search;
}
}
// Need to save incrementally. atomic save + move
let tmp_file = output.clone().unwrap_or_else(|| format!("{sub}.json.tmp"));
let perm_file = output.clone().unwrap_or_else(|| format!("{sub}.json"));
write_posts(&hashmap.values().collect(), tmp_file.clone());
// move file
std::fs::rename(tmp_file, perm_file).unwrap();
}
println!("\n\n");
println!("Size of hashmap: {}", hashmap.len());
let posts: Vec<&Post> = hashmap.values().collect();
match format {
Format::Json => {
let filename: String = output.unwrap_or_else(|| format!("{sub}.json"));
write_posts(&posts, filename);
}
}
}
fn write_posts(posts: &Vec<&Post>, filename: String) {
let json = serde_json::to_string(&posts).unwrap();
std::fs::write(filename, json).unwrap();
}

View File

@ -1,9 +1,11 @@
#![allow(clippy::cmp_owned)]
// CRATES
use crate::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
use crate::{
client::json,
server::RequestExt,
subreddit::{can_access_quarantine, quarantine},
RequestExt,
};
use hyper::{Body, Request, Response};
use once_cell::sync::Lazy;

View File

@ -1,4 +1,5 @@
#![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use brotli::enc::{BrotliCompress, BrotliEncoderParams};
use cached::proc_macro::cached;
@ -195,6 +196,12 @@ impl Route<'_> {
}
}
impl Default for Server {
fn default() -> Self {
Self::new()
}
}
impl Server {
pub fn new() -> Self {
Self {
@ -723,7 +730,7 @@ mod tests {
CompressionType::Brotli => Box::new(BrotliDecompressor::new(body_cursor, expected_lorem_ipsum.len())),
_ => panic!("no decompressor for {}", expected_encoding.to_string()),
_ => panic!("no decompressor for {}", expected_encoding),
};
let mut decompressed = Vec::<u8>::new();

View File

@ -1,3 +1,5 @@
#![allow(clippy::cmp_owned)]
use std::collections::HashMap;
// CRATES
@ -19,9 +21,10 @@ struct SettingsTemplate {
// CONSTANTS
const PREFS: [&str; 19] = [
const PREFS: [&str; 21] = [
"theme",
"mascot",
"redsunlib_colorway",
"front_page",
"layout",
"wide",
@ -39,6 +42,7 @@ const PREFS: [&str; 19] = [
"hide_awards",
"hide_score",
"disable_visit_reddit_confirmation",
"video_quality",
];
// FUNCTIONS
@ -118,7 +122,7 @@ fn set_cookies_method(req: Request<Body>, remove_cookies: bool) -> Response<Body
let mut response = redirect(&path);
for name in [PREFS.to_vec(), vec!["subscriptions", "filters"]].concat() {
for name in [PREFS.to_vec(), vec!["subscriptions", "filters", "quicklist"]].concat() {
match form.get(name) {
Some(value) => response.insert_cookie(
Cookie::build((name.to_owned(), value.clone()))

View File

@ -1,9 +1,11 @@
#![allow(clippy::cmp_owned)]
use crate::{config, utils};
// CRATES
use crate::utils::{
catch_random, error, filter_posts, format_num, format_url, get_filters, nsfw_landing, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
};
use crate::{client::json, server::ResponseExt, RequestExt};
use crate::{client::json, server::RequestExt, server::ResponseExt};
use cookie::Cookie;
use hyper::{Body, Request, Response};
use rinja::Template;
@ -209,8 +211,8 @@ pub fn can_access_quarantine(req: &Request<Body>, sub: &str) -> bool {
setting(req, &format!("allow_quaran_{}", sub.to_lowercase())).parse().unwrap_or_default()
}
// Sub, filter, unfilter, or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>, String> {
// Sub, filter, unfilter, quicklist, unquicklist or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions_filters_quicklists(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_default();
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect();
@ -227,6 +229,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
let preferences = Preferences::new(&req);
let mut sub_list = preferences.subscriptions;
let mut filters = preferences.filters;
let mut quicklist = preferences.quicklist;
// Retrieve list of posts for these subreddits to extract display names
@ -288,7 +291,16 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
} else if action.contains(&"unfilter".to_string()) {
// Remove sub name from filtered list
filters.retain(|s| s.to_lowercase() != part.to_lowercase());
}
} else if action.contains(&"quicklist".to_string()) && !quicklist.contains(&part.to_owned()) {
// Add each sub name to the filtered list
quicklist.push(part.to_owned());
// Reorder quicklist alphabetically
quicklist.sort_by_key(|a| a.to_lowercase());
} else if action.contains(&"unquicklist".to_string()) {
// Remove sub name from filtered list
quicklist.retain(|s| s.to_lowercase() != part.to_lowercase());
}
}
// Redirect back to subreddit
@ -324,6 +336,17 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
.into(),
);
}
if quicklist.is_empty() {
response.remove_cookie("quicklist".to_string());
} else {
response.insert_cookie(
Cookie::build(("quicklist", quicklist.join("+")))
.path("/")
.http_only(true)
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
.into(),
);
}
Ok(response)
}

View File

@ -1,3 +1,5 @@
#![allow(clippy::cmp_owned)]
// CRATES
use crate::client::json;
use crate::server::RequestExt;

View File

@ -1,4 +1,6 @@
#![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use crate::config::{self, get_setting};
//
// CRATES
@ -11,6 +13,7 @@ use once_cell::sync::Lazy;
use regex::Regex;
use rinja::Template;
use rust_embed::RustEmbed;
use serde::Serialize;
use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet};
@ -46,6 +49,7 @@ pub enum ResourceType {
}
// Post flair with content, background color and foreground color
#[derive(Serialize)]
pub struct Flair {
pub flair_parts: Vec<FlairPart>,
pub text: String,
@ -54,7 +58,7 @@ pub struct Flair {
}
// Part of flair, either emoji or text
#[derive(Clone)]
#[derive(Clone, Serialize)]
pub struct FlairPart {
pub flair_part_type: String,
pub value: String,
@ -96,12 +100,14 @@ impl FlairPart {
}
}
#[derive(Serialize)]
pub struct Author {
pub name: String,
pub flair: Flair,
pub distinguished: String,
}
#[derive(Serialize)]
pub struct Poll {
pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String),
@ -129,6 +135,7 @@ impl Poll {
}
}
#[derive(Serialize)]
pub struct PollOption {
pub id: u64,
pub text: String,
@ -158,13 +165,14 @@ impl PollOption {
}
// Post flags with nsfw and stickied
#[derive(Serialize)]
pub struct Flags {
pub spoiler: bool,
pub nsfw: bool,
pub stickied: bool,
}
#[derive(Debug)]
#[derive(Debug, Serialize)]
pub struct Media {
pub url: String,
pub alt_url: String,
@ -264,6 +272,7 @@ impl Media {
}
}
#[derive(Serialize)]
pub struct GalleryMedia {
pub url: String,
pub width: i64,
@ -304,6 +313,7 @@ impl GalleryMedia {
}
// Post containing content, metadata and media
#[derive(Serialize)]
pub struct Post {
pub id: String,
pub title: String,
@ -470,7 +480,7 @@ pub struct Comment {
pub prefs: Preferences,
}
#[derive(Default, Clone)]
#[derive(Default, Clone, Serialize)]
pub struct Award {
pub name: String,
pub icon_url: String,
@ -484,6 +494,7 @@ impl std::fmt::Display for Award {
}
}
#[derive(Serialize)]
pub struct Awards(pub Vec<Award>);
impl std::ops::Deref for Awards {
@ -496,7 +507,7 @@ impl std::ops::Deref for Awards {
impl std::fmt::Display for Awards {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.iter().fold(Ok(()), |result, award| result.and_then(|()| writeln!(f, "{award}")))
self.iter().try_fold((), |_, award| writeln!(f, "{award}"))
}
}
@ -596,6 +607,7 @@ pub struct Preferences {
pub available_mascots: Vec<String>,
pub theme: String,
pub mascot: String,
pub redsunlib_colorway: String,
pub front_page: String,
pub layout: String,
pub wide: String,
@ -603,6 +615,7 @@ pub struct Preferences {
pub show_nsfw: String,
pub blur_nsfw: String,
pub hide_hls_notification: String,
pub video_quality: String,
pub hide_sidebar_and_summary: String,
pub use_hls: String,
pub ffmpeg_video_downloads: String,
@ -612,6 +625,7 @@ pub struct Preferences {
pub comment_sort: String,
pub post_sort: String,
pub subscriptions: Vec<String>,
pub quicklist: Vec<String>,
pub filters: Vec<String>,
pub hide_awards: String,
pub hide_score: String,
@ -649,6 +663,7 @@ impl Preferences {
available_mascots: mascots,
theme: setting(req, "theme"),
mascot: setting(req, "mascot"),
redsunlib_colorway: setting(req, "redsunlib_colorway"),
front_page: setting(req, "front_page"),
layout: setting(req, "layout"),
wide: setting(req, "wide"),
@ -659,6 +674,7 @@ impl Preferences {
use_hls: setting(req, "use_hls"),
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
hide_hls_notification: setting(req, "hide_hls_notification"),
video_quality: setting(req, "video_quality"),
autoplay_videos: setting(req, "autoplay_videos"),
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
@ -666,6 +682,7 @@ impl Preferences {
post_sort: setting(req, "post_sort"),
subscriptions: setting(req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
filters: setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
quicklist: setting(req, "quicklist").split('+').map(String::from).filter(|s| !s.is_empty()).collect(),
hide_awards: setting(req, "hide_awards"),
hide_score: setting(req, "hide_score"),
}

55
static/check_update.js Normal file
View File

@ -0,0 +1,55 @@
async function checkInstanceUpdateStatus() {
try {
const response = await fetch('/commits.json');
const text = await response.text();
const entries = JSON.parse(text);
const localCommit = document.getElementById('git_commit').dataset.value;
let statusMessage = '';
if (entries.length > 0) {
const commitHashes = Array.from(entries).map(entry => {
return entry.sha
});
const commitIndex = commitHashes.indexOf(localCommit);
if (commitIndex === 0) {
statusMessage = '✅ Instance is up to date.';
} else if (commitIndex > 0) {
statusMessage = `⚠️ This instance is not up to date and is ${commitIndex} commits old. Test and confirm on an up-to-date instance before reporting.`;
document.getElementById('error-318').remove();
} else {
statusMessage = `⚠️ This instance is not up to date and is at least ${commitHashes.length} commits old. Test and confirm on an up-to-date instance before reporting.`;
document.getElementById('error-318').remove();
}
} else {
statusMessage = '⚠️ Unable to fetch commit information.';
}
document.getElementById('update-status').innerText = statusMessage;
} catch (error) {
console.error('Error fetching commits:', error);
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
}
}
async function checkOtherInstances() {
try {
const response = await fetch('/instances.json');
const data = await response.json();
const randomInstance = data.instances[Math.floor(Math.random() * data.instances.length)];
const instanceUrl = randomInstance.url;
// Set the href of the <a> tag to the instance URL with path included
document.getElementById('random-instance').href = instanceUrl + window.location.pathname;
//document.getElementById('random-instance').innerText = "Visit Random Instance";
} catch (error) {
console.error('Error fetching instances:', error);
document.getElementById('update-status').innerText = '⚠️ Error checking update status.';
}
}
// Set the target URL when the page loads
window.addEventListener('load', checkOtherInstances);
checkInstanceUpdateStatus();

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@ -216,7 +216,9 @@ nav #links {
}
nav #links svg {
display: none;
/*display: none;*/
position: relative;
/*What's nine plus ten?*/ top: /*Twenty one*/21%; /*You stupid.*/
}
nav #redlib {
@ -429,7 +431,7 @@ a:hover {
}
svg {
stroke: var(--text);
color: var(--text);
}
img[src=""] {
@ -523,10 +525,14 @@ aside {
margin-bottom: 20px;
}
#user_actions,
#sub_actions {
display: grid;
grid-template-columns: auto 4fr 1fr;
}
#user_details,
#sub_details,
#sub_actions,
#user_actions {
#sub_details {
display: grid;
grid-template-columns: repeat(2, 1fr);
}
@ -545,6 +551,8 @@ aside {
#user_subscription,
#sub_filter,
#user_filter,
#sub_quicklist,
#user_quicklist,
#sub_rss,
#user_rss {
margin-top: 20px;
@ -554,9 +562,16 @@ aside {
margin-bottom: 20px;
}
#sub_quicklist button,
#user_quicklist button {
padding: 0px;
}
.subscribe,
.unsubscribe,
.filter,
.unquick,
.quick,
.unfilter {
padding: 10px 20px;
border-radius: 5px;
@ -568,12 +583,22 @@ aside {
color: var(--foreground);
background-color: var(--accent);
}
.quick {
color: var(--foreground);
background-color: var(--accent);
height: 18px;
}
.unsubscribe,
.unfilter {
color: var(--text);
background-color: var(--highlighted);
}
.unquick {
color: var(--text);
background-color: var(--highlighted);
height: 18px;
}
/* Feeds */
@ -1456,6 +1481,7 @@ a.search_subreddit:hover {
.comment:not([id]) .comment_data > .comment_link {
display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
word-break: break-all;
@ -1965,6 +1991,13 @@ th {
color: var(--accent);
}
#issue_warning {
color: var(--popup-toreddit-text);
background: var(--popup-background);
border-radius: 5px;
padding: 10px;
}
/* Messages */
#duplicates_msg h3 {
@ -2261,8 +2294,10 @@ th {
max-width: 20em;
}
.download {
padding-left: 8px;
padding-right: 8px;
padding-left: 8px;
padding-right: 8px;
padding-top: 3px;
padding-bottom: 1px;
font-size: 20px;
font-weight: 900;
color: var(--accent);
@ -2276,4 +2311,11 @@ th {
.download:active {
background-color: var(--background);
}
.rotate{
animation: spinner 1.5s linear infinite;
padding: 0px;
}
@keyframes spinner {
to { transform: rotate(360deg); }
}

View File

@ -1,5 +1,9 @@
// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
let ffmpeg = null;
let loadingsvg = `<svg class="rotate" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none" fill-rule="evenodd"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M12 4.5a7.5 7.5 0 1 0 0 15a7.5 7.5 0 0 0 0-15M1.5 12C1.5 6.201 6.201 1.5 12 1.5S22.5 6.201 22.5 12S17.799 22.5 12 22.5S1.5 17.799 1.5 12" opacity="0.1"/><path fill="currentColor" d="M12 4.5a7.46 7.46 0 0 0-5.187 2.083a1.5 1.5 0 0 1-2.075-2.166A10.46 10.46 0 0 1 12 1.5a1.5 1.5 0 0 1 0 3"/></g></svg>`;
let downloadsvg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><g fill="none"><path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z"/><path fill="currentColor" d="M20 15a1 1 0 0 1 1 1v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4a1 1 0 1 1 2 0v4h14v-4a1 1 0 0 1 1-1M12 2a1 1 0 0 1 1 1v10.243l2.536-2.536a1 1 0 1 1 1.414 1.414l-4.066 4.066a1.25 1.25 0 0 1-1.768 0L7.05 12.121a1 1 0 1 1 1.414-1.414L11 13.243V3a1 1 0 0 1 1-1"/></g></svg>`;
(function () {
if (Hls.isSupported()) {
@ -13,7 +17,7 @@ let ffmpeg = null;
var autoplay = oldVideo.classList.contains("hls_autoplay");
// If HLS is supported natively then don't use hls.js
if (oldVideo.canPlayType(source.type) === "probably") {
if (oldVideo.canPlayType(source.type) === "probably" && !downloadsEnabled) {
if (autoplay) {
oldVideo.play();
}
@ -82,7 +86,7 @@ let ffmpeg = null;
var mediaStream = [];
var downloadButton = document.createElement('button');
downloadButton.classList.add('video-options','download');
downloadButton.innerText = "⏳"
downloadButton.innerHTML = loadingsvg
const mergeStreams = async () => {
if (ffmpeg === null) {
ffmpeg = new FFmpeg();
@ -115,7 +119,7 @@ let ffmpeg = null;
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
const data = await ffmpeg.readFile('output.mp4');
saveAs(new Blob([data.buffer]),filename, {type: 'video/mp4'});
saveAs(new Blob([data.buffer], {type: 'video/mp4'}),filename);
return
}
function saveAs(blob, filename) { // Yeah ok...
@ -157,11 +161,11 @@ let ffmpeg = null;
var isDownloading = false
function startDownload() {
if (!isDownloading) { isDownloading = true } else { return }
downloadButton.innerText = "⏳"
downloadButton.innerHTML = loadingsvg
mergeStreams()
.then(_ => {
isDownloading = false
downloadButton.innerText = "⭳"
downloadButton.innerHTML = downloadsvg
});
}
@ -178,7 +182,7 @@ let ffmpeg = null;
waitForLoad(_ => flag === true)
.then(_ => {
downloadButton.innerText = "⭳"
downloadButton.innerHTML = downloadsvg
downloadButton.addEventListener('click', startDownload);
});
}

View File

@ -27,6 +27,8 @@
<link rel="manifest" type="application/json" href="/manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" type="text/css" href="/style.css?v={{ env!("CARGO_PKG_VERSION") }}">
<!-- Video quality -->
<div id="video_quality" data-value="{{ prefs.video_quality }}"></div>
{% endblock %}
</head>
<body class="
@ -38,27 +40,35 @@
<nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<div id="logo">
<a id="redlib" href="/"><span id="lib">red</span><span id="reddit">sun</span><span id="lib">lib.</span></a>
<a id="redlib" href="/">
<span id="lib" {% if prefs.redsunlib_colorway == "on" %}style="color: #ff8585;"{% endif %}">red</span><span id="reddit" {% if prefs.redsunlib_colorway == "on" %}style="color: #ffbfbf;"{% endif %}>sun</span><span id="lib" {% if prefs.redsunlib_colorway == "on" %}style="color: #ff8585;"{% endif %}>lib.</span>
</a>
{% block subscriptions %}{% endblock %}
</div>
{% block search %}{% endblock %}
<div id="links">
<a id="reddit_link" {% if prefs.disable_visit_reddit_confirmation != "on" %}href="#popup"{% else %}href="https://www.reddit.com{{ url }}" rel="nofollow"{% endif %}>
<span>reddit</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">
<path d="M22 2L12 22"/>
<path d="M2 6.70587C3.33333 8.07884 3.33333 11.5971 3.33333 11.5971M3.33333 19.647V11.5971M3.33333 11.5971C3.33333 11.5971 5.125 7.47817 8 7.47817C10.875 7.47817 12 8.85114 12 8.85114"/>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<title>redirect to reddit</title>
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M6.301 6a4 4 0 0 1 3.312 1.756l.118.186l4.253 7.087a2 2 0 0 0 1.553.965L15.7 16h1.194l.02-.415l.022-.36l.012-.159c.027-.346.352-.557.631-.41l.306.164l.36.203l.198.117l.43.263l.229.147l.463.31l.21.147l.377.273l.315.24l.133.104c.236.188.225.566-.023.762l-.28.217l-.34.252l-.4.282l-.456.305l-.462.291l-.416.249l-.365.205l-.307.165c-.275.143-.572-.036-.598-.36l-.025-.347l-.024-.415l-.01-.23H15.7a4 4 0 0 1-3.312-1.756l-.118-.186l-4.253-7.087a2 2 0 0 0-1.553-.965L6.3 8H4a1 1 0 0 1-.117-1.993L4 6zm3.714 7.643a1 1 0 0 1 .342 1.371l-.626 1.044A4 4 0 0 1 6.301 18H4a1 1 0 1 1 0-2h2.301a2 2 0 0 0 1.715-.971l.627-1.043a1 1 0 0 1 1.371-.344Zm7.563-8.988l.306.165l.36.203l.198.117l.43.263l.229.147l.463.31l.21.147l.377.273l.315.24l.133.104c.236.188.225.566-.023.762l-.28.217l-.34.252q-.186.135-.4.282l-.456.305l-.462.291l-.416.249l-.365.206l-.307.164c-.275.143-.572-.036-.598-.36l-.025-.347l-.024-.415l-.01-.23H15.7a2 2 0 0 0-1.627.836l-.088.135l-.626 1.043a1 1 0 0 1-1.77-.925l.055-.104l.626-1.043a4 4 0 0 1 3.209-1.936l.22-.006h1.195l.02-.415l.022-.36l.012-.159c.027-.346.352-.557.631-.41Z" />
</g>
</svg>
<span>reddit</span>
</a>
{% if prefs.disable_visit_reddit_confirmation != "on" %}
{% call utils::visit_reddit_confirmation(url) %}
{% endif %}
<a id="settings_link" href="/settings">
<span>settings</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">
<title>settings</title>
<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 xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<title>settings</title>
<path d="m12.593 23.258l-.011.002l-.071.035l-.02.004l-.014-.004l-.071-.035q-.016-.005-.024.005l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.017-.018m.265-.113l-.013.002l-.185.093l-.01.01l-.003.011l.018.43l.005.012l.008.007l.201.093q.019.005.029-.008l.004-.014l-.034-.614q-.005-.018-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.004-.011l.017-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M14.035 2.809c.37-.266.89-.39 1.401-.203a10 10 0 0 1 2.982 1.725c.417.35.57.861.524 1.313c-.075.753.057 1.48.42 2.106c.32.557.802.997 1.39 1.307l.225.11c.414.187.782.576.875 1.113a10 10 0 0 1 0 3.44c-.083.484-.39.847-.753 1.051l-.122.063c-.69.31-1.254.79-1.616 1.416c-.362.627-.494 1.353-.419 2.106c.045.452-.107.964-.524 1.313a10 10 0 0 1-2.982 1.725a1.51 1.51 0 0 1-1.4-.203C13.42 20.75 12.723 20.5 12 20.5s-1.42.249-2.035.691a1.51 1.51 0 0 1-1.401.203a10 10 0 0 1-2.982-1.725a1.51 1.51 0 0 1-.524-1.313c.075-.753-.058-1.48-.42-2.106a3.4 3.4 0 0 0-1.39-1.307l-.225-.11a1.51 1.51 0 0 1-.875-1.113a10 10 0 0 1 0-3.44c.083-.484.39-.847.753-1.051l.122-.062c.69-.311 1.254-.79 1.616-1.417c.361-.626.494-1.353.419-2.106a1.51 1.51 0 0 1 .524-1.313a10 10 0 0 1 2.982-1.725a1.51 1.51 0 0 1 1.4.203c.615.442 1.312.691 2.036.691s1.42-.249 2.035-.691m.957 1.769c-.866.57-1.887.922-2.992.922s-2.126-.353-2.992-.922A8 8 0 0 0 7.068 5.7c.06 1.033-.145 2.093-.697 3.05c-.553.956-1.368 1.663-2.293 2.128a8 8 0 0 0 0 2.242c.925.465 1.74 1.172 2.293 2.13c.552.955.757 2.015.697 3.048a8 8 0 0 0 1.94 1.123c.866-.57 1.887-.922 2.992-.922s2.126.353 2.992.922a8 8 0 0 0 1.94-1.122c-.06-1.034.145-2.094.697-3.05c.552-.957 1.368-1.664 2.293-2.13a8 8 0 0 0 0-2.24c-.925-.466-1.74-1.173-2.293-2.13c-.552-.956-.757-2.016-.697-3.05a8 8 0 0 0-1.94-1.122ZM12 8a4 4 0 1 1 0 8a4 4 0 0 1 0-8m0 2a2 2 0 1 0 0 4a2 2 0 0 0 0-4" />
</g>
</svg>
<span>settings</span>
</a>
</div>
</nav>

View File

@ -6,10 +6,19 @@
<h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br />
<h3>Expected something to work? <a
href="https://github.com/redlib-org/redlib/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=%F0%9F%90%9B+Bug+Report%3A+{{ msg }}">Report
an issue</a></h3>
<h3 id="update-status"></h3>
<br />
<h3 id="update-status"></h3>
<br>
<div id="git_commit" data-value="{{ crate::instance_info::INSTANCE_INFO.git_commit }}"></div>
<script src="/check_update.js"></script>
<h3>Expected something to work? Try a random <a id="random-instance">upstream instance.</a></h3>
<br />
<h3 id="issue_warning" >!! Do <b>NOT</b> open an issue on the <a href="https://github.com/redlib-org/redlib/">redlib repository</a> with a redsunlib specific issue !!</h3>
<br />
<p id="error-318">If you're getting a "Failed to parse page JSON data" error, please check <a href="https://github.com/redlib-org/redlib/issues/318" target="_blank">#318</a></p>
<br />
<h3>Head back <a href="/">home</a>?</h3>
</div>
{% endblock %}
{% endblock %}

View File

@ -29,6 +29,11 @@
{% call utils::options(prefs.mascot, prefs.available_mascots, "system") %}
</select>
</div>
<div class="prefs-group">
<label for="redsunlib_colorway">Force redsunlib colorway</label>
<input type="hidden" value="off" name="redsunlib_colorway">
<input type="checkbox" name="redsunlib_colorway" {% if prefs.redsunlib_colorway == "on" %}checked{% endif %}>
</div>
</fieldset>
<fieldset>
<legend>Interface</legend>
@ -68,6 +73,12 @@
</fieldset>
<fieldset>
<legend>Content</legend>
<div class="prefs-group">
<label for="video_quality">Video quality:</label>
<select name="video_quality" id="video_quality">
{% call utils::options(prefs.video_quality, ["best", "medium", "worst"], "best") %}
</select>
</div>
<div class="prefs-group">
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
<select name="post_sort">
@ -147,7 +158,7 @@
</form>
<div id="settings_note">
<p><b>Note:</b> settings and subscriptions are saved in browser cookies. Clearing your cookies will reset them.</p><br>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
<p>You can restore your current settings and subscriptions after clearing your cookies using <a href="/settings/restore/?theme={{ prefs.theme }}&mascot={{ prefs.mascot }}&redsunlib_colorway={{ prefs.redsunlib_colorway }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&post_sort={{ prefs.post_sort }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&ffmpeg_video_downloads={{ prefs.ffmpeg_video_downloads }}&hide_hls_notification={{ prefs.hide_hls_notification }}&hide_awards={{ prefs.hide_awards }}&fixed_navbar={{ prefs.fixed_navbar }}&hide_sidebar_and_summary={{ prefs.hide_sidebar_and_summary}}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}&quicklist={{ prefs.quicklist.join("%2B") }}">this link</a>.</p>
</div>
{% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs">

View File

@ -104,6 +104,18 @@
<img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}">
<h1 id="sub_title">{{ sub.title }}</h1>
<p id="sub_name">r/{{ sub.name }}</p>
{% if crate::utils::enable_rss() %}
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M18 3a3 3 0 0 1 2.995 2.824L21 6v12a3 3 0 0 1-2.824 2.995L18 21H6a3 3 0 0 1-2.995-2.824L3 18V6a3 3 0 0 1 2.824-2.995L6 3zM8.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M8 10.5a1 1 0 1 0 0 2a3.5 3.5 0 0 1 3.5 3.5a1 1 0 1 0 2 0A5.5 5.5 0 0 0 8 10.5M8.5 7q-.285 0-.566.019a1 1 0 0 0 .132 1.995a6.5 6.5 0 0 1 6.92 6.92a1 1 0 1 0 1.995.132A8.5 8.5 0 0 0 8.5 7" />
</g>
</svg>
</button >
</a>
{% endif %}
<p id="sub_description">{{ sub.description }}</p>
<div id="sub_details">
<label>Members</label>
@ -134,13 +146,31 @@
</form>
{% endif %}
</div>
{% if crate::utils::enable_rss() %}
<div id="sub_rss">
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}">
<button class="subscribe">RSS feed</button >
</a>
<div id="sub_quicklist">
{% if prefs.quicklist.contains(sub.name) %}
<form action="/r/{{ sub.name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="unquick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M16 2a3 3 0 0 1 3 3v11h-3a3 3 0 0 0-2.997 2.87L12 18.202l-4.668 3.112C6.335 21.978 5 21.264 5 20.066V5a3 3 0 0 1 3-3zm6 16a1 1 0 0 1 .117 1.993L22 20h-6a1 1 0 0 1-.117-1.993L16 18z" />
</g>
</svg>
</button>
</form>
{% else %}
<form action="/r/{{ sub.name }}/quicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="quick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M10 2a3 3 0 0 0-3 3a3 3 0 0 0-3 3v13.018c0 1.226 1.39 1.934 2.382 1.213l4.118-2.995l4.118 2.995c.991.721 2.382.013 2.382-1.213v-2.236l.618.45c.991.72 2.382.012 2.382-1.214V5a3 3 0 0 0-3-3zm7 14.309l1 .727V5a1 1 0 0 0-1-1h-7a1 1 0 0 0-1 1h5a3 3 0 0 1 3 3z" />
</g>
</svg>
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
</details>
<details class="panel" id="sidebar" open>

View File

@ -123,6 +123,18 @@ body %}
/>
<h1 id="user_title">{{ user.title }}</h1>
<p id="user_name">u/{{ user.name }}</p>
{% if crate::utils::enable_rss() %}
<a href="/r/{{ user.name }}.rss" title="RSS feed for r/{{ user.name }}">
<button>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M18 3a3 3 0 0 1 2.995 2.824L21 6v12a3 3 0 0 1-2.824 2.995L18 21H6a3 3 0 0 1-2.995-2.824L3 18V6a3 3 0 0 1 2.824-2.995L6 3zM8.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3M8 10.5a1 1 0 1 0 0 2a3.5 3.5 0 0 1 3.5 3.5a1 1 0 1 0 2 0A5.5 5.5 0 0 0 8 10.5M8.5 7q-.285 0-.566.019a1 1 0 0 0 .132 1.995a6.5 6.5 0 0 1 6.92 6.92a1 1 0 1 0 1.995.132A8.5 8.5 0 0 0 8.5 7" />
</g>
</svg>
</button >
</a>
{% endif %}
<div id="user_description">{{ user.description }}</div>
<div id="user_details">
<label>Karma</label>
@ -166,16 +178,31 @@ body %}
</form>
{% endif %}
</div>
{% if crate::utils::enable_rss() %}
<div id="user_rss">
<a
href="/u/{{ user.name }}.rss"
title="RSS feed for u/{{ user.name }}"
>
<button class="subscribe">RSS feed</button>
</a>
<div id="user_quicklist">
{% if prefs.quicklist.contains(name) %}
<form action="/r/{{ name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="unquick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M16 2a3 3 0 0 1 3 3v11h-3a3 3 0 0 0-2.997 2.87L12 18.202l-4.668 3.112C6.335 21.978 5 21.264 5 20.066V5a3 3 0 0 1 3-3zm6 16a1 1 0 0 1 .117 1.993L22 20h-6a1 1 0 0 1-.117-1.993L16 18z" />
</g>
</svg>
</button>
</form>
{% else %}
<form action="/r/{{ name }}/quicklist?redirect={{ redirect_url }}" method="POST">
<button>
<svg class="quick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
<g fill="none" fill-rule="evenodd">
<path d="m12.594 23.258l-.012.002l-.071.035l-.02.004l-.014-.004l-.071-.036q-.016-.004-.024.006l-.004.01l-.017.428l.005.02l.01.013l.104.074l.015.004l.012-.004l.104-.074l.012-.016l.004-.017l-.017-.427q-.004-.016-.016-.018m.264-.113l-.014.002l-.184.093l-.01.01l-.003.011l.018.43l.005.012l.008.008l.201.092q.019.005.029-.008l.004-.014l-.034-.614q-.005-.019-.02-.022m-.715.002a.02.02 0 0 0-.027.006l-.006.014l-.034.614q.001.018.017.024l.015-.002l.201-.093l.01-.008l.003-.011l.018-.43l-.003-.012l-.01-.01z" />
<path fill="currentColor" d="M10 2a3 3 0 0 0-3 3a3 3 0 0 0-3 3v13.018c0 1.226 1.39 1.934 2.382 1.213l4.118-2.995l4.118 2.995c.991.721 2.382.013 2.382-1.213v-2.236l.618.45c.991.72 2.382.012 2.382-1.214V5a3 3 0 0 0-3-3zm7 14.309l1 .727V5a1 1 0 0 0-1-1h-7a1 1 0 0 0-1 1h5a3 3 0 0 1 3 3z" />
</g>
</svg>
</button>
</form>
{% endif %}
</div>
{% endif %}
</div>
</div>
</aside>

View File

@ -41,6 +41,12 @@
<details id="feeds">
<summary>Feeds</summary>
<div id="feed_list">
{% if prefs.quicklist.len() > 0 %}
<p>QUICK ACCESS FEEDS</p>
{% for sub in prefs.quicklist %}
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{% if sub.starts_with("u_") -%}{%let sub = format!("u/{}", &sub[2..]) -%}{{ sub }}{% else -%}{{ sub }}{% endif -%}</a>
{% endfor %}
{% endif %}
<p>MAIN FEEDS</p>
<a href="/">Home</a>
<a href="/r/popular">Popular</a>
@ -48,7 +54,7 @@
{% if prefs.subscriptions.len() > 0 %}
<p>REDDIT FEEDS</p>
{% for sub in prefs.subscriptions %}
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a>
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{% if sub.starts_with("u_") -%}{%let sub = format!("u/{}", &sub[2..]) -%}{{ sub }}{% else -%}{{ sub }}{% endif -%}</a>
{% endfor %}
{% endif %}
</div>