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 REDLIB_DEFAULT_THEME=system
# Set the default mascot # Set the default mascot
REDLIB_DEFAULT_MASCOT=none REDLIB_DEFAULT_MASCOT=none
# Enable showing redsunlib colorway by default
REDLIB_DEFAULT_REDSUNLIB_COLORWAY=off
# Set the default front page (options: default, popular, all) # Set the default front page (options: default, popular, all)
REDLIB_DEFAULT_FRONT_PAGE=default REDLIB_DEFAULT_FRONT_PAGE=default
# Set the default layout (options: card, clean, compact) # 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" description = " Alternative private front-end to Reddit"
license = "AGPL-3.0-only" license = "AGPL-3.0-only"
repository = "https://git.stardust.wtf/iridium/redsunlib" repository = "https://git.stardust.wtf/iridium/redsunlib"
version = "0.35.3" version = "0.35.4"
authors = [ authors = [
"Matthew Esposito <matt+cargo@matthew.science>", "Matthew Esposito <matt+cargo@matthew.science>",
"spikecodes <19519553+spikecodes@users.noreply.github.com>", "spikecodes <19519553+spikecodes@users.noreply.github.com>",
] ]
edition = "2021" edition = "2021"
default-run = "redsunlib"
[dependencies] [dependencies]
rinja = { version = "0.3.4", default-features = false } 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 = [ clap = { version = "4.4.11", default-features = false, features = [
"std", "std",
"env", "env",
"derive",
] } ] }
regex = "1.10.2" regex = "1.10.2"
serde = { version = "1.0.193", features = ["derive"] } serde = { version = "1.0.193", features = ["derive"] }
cookie = "0.18.0" cookie = "0.18.0"
futures-lite = "2.2.0" futures-lite = "2.2.0"
hyper = { version = "0.14.28", features = ["full"] } hyper = { version = "0.14.31", features = ["full"] }
hyper-rustls = "0.24.2"
percent-encoding = "2.3.1" percent-encoding = "2.3.1"
route-recognizer = "0.3.1" route-recognizer = "0.3.1"
serde_json = "1.0.108" serde_json = "1.0.133"
tokio = { version = "1.35.1", features = ["full"] } tokio = { version = "1.35.1", features = ["full"] }
time = { version = "0.3.31", features = ["local-offset"] } time = { version = "0.3.31", features = ["local-offset"] }
url = "2.5.0" url = "2.5.0"
@ -44,8 +45,10 @@ pretty_env_logger = "0.5.0"
dotenvy = "0.15.7" dotenvy = "0.15.7"
rss = "2.0.7" rss = "2.0.7"
arc-swap = "1.7.1" arc-swap = "1.7.1"
serde_json_path = "0.6.7" serde_json_path = "0.7.1"
async-recursion = "1.1.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] [dev-dependencies]
@ -56,3 +59,11 @@ sealed_test = "1.0.0"
codegen-units = 1 codegen-units = 1
lto = true lto = true
strip = "symbols" strip = "symbols"
[[bin]]
name = "redsunlib"
path = "src/main.rs"
[[bin]]
name = "scraper"
path = "src/scraper/main.rs"

View File

@ -5,7 +5,7 @@
<br> <br>
![screenshot](https://git.stardust.wtf/attachments/7667e4e2-a32c-4269-9b5f-1d29cb3baf20) ![screenshot](https://git.stardust.wtf/attachments/ccf81f52-e653-4722-94b9-b370c58d6359)
### Disclaimer ### 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 futures_lite::{future::Boxed, FutureExt};
use hyper::client::HttpConnector; use hyper::client::HttpConnector;
use hyper::header::HeaderValue; 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 hyper_rustls::HttpsConnector;
use libflate::gzip; use libflate::gzip;
use log::{error, trace, warn}; 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: &str = "https://www.reddit.com";
const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com"; const ALTERNATIVE_REDDIT_URL_BASE_HOST: &str = "www.reddit.com";
pub static CLIENT: Lazy<Client<HttpsConnector<HttpConnector>>> = Lazy::new(|| { pub static HTTPS_CONNECTOR: Lazy<HttpsConnector<HttpConnector>> =
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build(); Lazy::new(|| hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http2().build());
client::Client::builder().build(https)
}); 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(|| { pub static OAUTH_CLIENT: Lazy<ArcSwap<Oauth>> = Lazy::new(|| {
let client = block_on(Oauth::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())?; let parsed_uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
// Build the hyper client from the HTTPS connector. // 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); 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}"); let url = format!("{base_path}{path}");
// Construct the hyper client from the HTTPS connector. // 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 (token, vendor_id, device_id, user_agent, loid) = {
let client = OAUTH_CLIENT.load_full(); 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. // Build request to Reddit. When making a GET, request gzip compression.
// (Reddit doesn't do brotli yet.) // (Reddit doesn't do brotli yet.)
let builder = Request::builder() let mut headers = vec![
.method(method) ("User-Agent", user_agent),
.uri(&url) ("Client-Vendor-Id", vendor_id),
.header("User-Agent", user_agent) ("X-Reddit-Device-Id", device_id),
.header("Client-Vendor-Id", vendor_id) ("x-reddit-loid", loid),
.header("X-Reddit-Device-Id", device_id) ("Host", host.to_string()),
.header("x-reddit-loid", loid) ("Authorization", format!("Bearer {token}")),
.header("Host", host) ("Accept-Encoding", if method == Method::GET { "gzip".into() } else { "identity".into() }),
.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(
"Cookie", "Cookie",
if quarantine { 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 { } 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 { async move {
match builder { match builder {

View File

@ -96,6 +96,10 @@ pub struct Config {
#[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")] #[serde(alias = "LIBREDDIT_DEFAULT_FILTERS")]
pub(crate) default_filters: Option<String>, 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(rename = "REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
#[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")] #[serde(alias = "LIBREDDIT_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION")]
pub(crate) default_disable_visit_reddit_confirmation: Option<String>, pub(crate) default_disable_visit_reddit_confirmation: Option<String>,
@ -160,6 +164,7 @@ impl Config {
default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"), default_hide_score: parse("REDLIB_DEFAULT_HIDE_SCORE"),
default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"), default_subscriptions: parse("REDLIB_DEFAULT_SUBSCRIPTIONS"),
default_filters: parse("REDLIB_DEFAULT_FILTERS"), default_filters: parse("REDLIB_DEFAULT_FILTERS"),
default_quicklist: parse("REDLIB_DEFAULT_QUICKLIST"),
default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"), default_disable_visit_reddit_confirmation: parse("REDLIB_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION"),
banner: parse("REDLIB_BANNER"), banner: parse("REDLIB_BANNER"),
robots_disable_indexing: parse("REDLIB_ROBOTS_DISABLE_INDEXING"), 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_HIDE_SCORE" => config.default_hide_score.clone(),
"REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(), "REDLIB_DEFAULT_SUBSCRIPTIONS" => config.default_subscriptions.clone(),
"REDLIB_DEFAULT_FILTERS" => config.default_filters.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_DEFAULT_DISABLE_VISIT_REDDIT_CONFIRMATION" => config.default_disable_visit_reddit_confirmation.clone(),
"REDLIB_BANNER" => config.banner.clone(), "REDLIB_BANNER" => config.banner.clone(),
"REDLIB_ROBOTS_DISABLE_INDEXING" => config.robots_disable_indexing.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())); 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] #[test]
#[sealed_test] #[sealed_test]
fn test_pushshift() { fn test_pushshift() {

View File

@ -85,7 +85,7 @@ fn info_html(req: &Request<Body>) -> Result<Response<Body>, Error> {
pub struct InstanceInfo { pub struct InstanceInfo {
package_name: String, package_name: String,
crate_version: String, crate_version: String,
git_commit: String, pub git_commit: String,
deploy_date: String, deploy_date: String,
compile_mode: String, compile_mode: String,
deploy_unix_ts: i64, deploy_unix_ts: i64,
@ -152,6 +152,7 @@ impl InstanceInfo {
["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)], ["Hide HLS notification", &convert(&self.config.default_hide_hls_notification)],
["Subscriptions", &convert(&self.config.default_subscriptions)], ["Subscriptions", &convert(&self.config.default_subscriptions)],
["Filters", &convert(&self.config.default_filters)], ["Filters", &convert(&self.config.default_filters)],
["Quick Access Feeds", &convert(&self.config.default_quicklist)],
]) ])
.with_header_row(["Default preferences"]), .with_header_row(["Default preferences"]),
); );
@ -189,7 +190,8 @@ impl InstanceInfo {
Default use FFmpeg: {:?}\n Default use FFmpeg: {:?}\n
Default hide HLS notification: {:?}\n Default hide HLS notification: {:?}\n
Default subscriptions: {:?}\n Default subscriptions: {:?}\n
Default filters: {:?}\n", Default filters: {:?}\n
Default quicklist: {:?}\n",
self.package_name, self.package_name,
self.crate_version, self.crate_version,
self.git_commit, self.git_commit,
@ -218,6 +220,7 @@ impl InstanceInfo {
self.config.default_hide_hls_notification, self.config.default_hide_hls_notification,
self.config.default_subscriptions, self.config.default_subscriptions,
self.config.default_filters, self.config.default_filters,
self.config.default_quicklist,
) )
} }
StringType::Html => self.to_table(), 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)] #![forbid(unsafe_code)]
#![allow(clippy::cmp_owned)] #![allow(clippy::cmp_owned)]
// Reference local files use cached::proc_macro::cached;
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 clap::{Arg, ArgAction, Command}; use clap::{Arg, ArgAction, Command};
use std::str::FromStr;
use futures_lite::FutureExt; use futures_lite::FutureExt;
use hyper::Uri;
use hyper::{header::HeaderValue, Body, Request, Response}; use hyper::{header::HeaderValue, Body, Request, Response};
mod client;
use client::{canonical_path, proxy};
use log::info; use log::info;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use server::RequestExt; use redsunlib::client::{canonical_path, proxy, CLIENT};
use utils::{error, redirect, ThemeAssets, MascotAssets}; 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; use redsunlib::client::OAUTH_CLIENT;
mod server;
// Create Services // Create Services
@ -257,6 +243,13 @@ async fn main() {
app app
.at("/highlighted.js") .at("/highlighted.js")
.get(|_| resource(include_str!("../static/highlighted.js"), "text/javascript", false).boxed()); .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 // FFmpeg
app app
.at("/ffmpeg/814.ffmpeg.js") .at("/ffmpeg/814.ffmpeg.js")
@ -330,10 +323,12 @@ async fn main() {
.at("/r/u_:name") .at("/r/u_:name")
.get(|r| async move { Ok(redirect(&format!("/user/{}", r.param("name").unwrap_or_default()))) }.boxed()); .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/subscribe").post(|r| subreddit::subscriptions_filters_quicklists(r).boxed());
app.at("/r/:sub/unsubscribe").post(|r| subreddit::subscriptions_filters(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(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(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").get(|r| post::item(r).boxed());
app.at("/r/:sub/comments/:id/:title").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}"); 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..."); trace!("Sending token request...");
// Send 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()?; let resp = client.request(request).await.ok()?;
trace!("Received response with status {} and length {:?}", resp.status(), resp.headers().get("content-length")); 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 // CRATES
use crate::client::json; use crate::client::json;
use crate::config::get_setting; 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 // 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::utils::{self, catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, setting, template, val, Post, Preferences};
use crate::{ use crate::{
client::json, client::json,
server::RequestExt,
subreddit::{can_access_quarantine, quarantine}, subreddit::{can_access_quarantine, quarantine},
RequestExt,
}; };
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;

View File

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

View File

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

View File

@ -1,9 +1,11 @@
#![allow(clippy::cmp_owned)]
use crate::{config, utils}; use crate::{config, utils};
// CRATES // CRATES
use crate::utils::{ 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, 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 cookie::Cookie;
use hyper::{Body, Request, Response}; use hyper::{Body, Request, Response};
use rinja::Template; 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() 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 // Sub, filter, unfilter, quicklist, unquicklist or unsub by setting subscription cookie using response "Set-Cookie" header
pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>, String> { pub async fn subscriptions_filters_quicklists(req: Request<Body>) -> Result<Response<Body>, String> {
let sub = req.param("sub").unwrap_or_default(); let sub = req.param("sub").unwrap_or_default();
let action: Vec<String> = req.uri().path().split('/').map(String::from).collect(); 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 preferences = Preferences::new(&req);
let mut sub_list = preferences.subscriptions; let mut sub_list = preferences.subscriptions;
let mut filters = preferences.filters; let mut filters = preferences.filters;
let mut quicklist = preferences.quicklist;
// Retrieve list of posts for these subreddits to extract display names // 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()) { } else if action.contains(&"unfilter".to_string()) {
// Remove sub name from filtered list // Remove sub name from filtered list
filters.retain(|s| s.to_lowercase() != part.to_lowercase()); 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 // Redirect back to subreddit
@ -324,6 +336,17 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
.into(), .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) Ok(response)
} }

View File

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

View File

@ -1,4 +1,6 @@
#![allow(dead_code)] #![allow(dead_code)]
#![allow(clippy::cmp_owned)]
use crate::config::{self, get_setting}; use crate::config::{self, get_setting};
// //
// CRATES // CRATES
@ -11,6 +13,7 @@ use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use rinja::Template; use rinja::Template;
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use serde::Serialize;
use serde_json::Value; use serde_json::Value;
use serde_json_path::{JsonPath, JsonPathExt}; use serde_json_path::{JsonPath, JsonPathExt};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
@ -46,6 +49,7 @@ pub enum ResourceType {
} }
// Post flair with content, background color and foreground color // Post flair with content, background color and foreground color
#[derive(Serialize)]
pub struct Flair { pub struct Flair {
pub flair_parts: Vec<FlairPart>, pub flair_parts: Vec<FlairPart>,
pub text: String, pub text: String,
@ -54,7 +58,7 @@ pub struct Flair {
} }
// Part of flair, either emoji or text // Part of flair, either emoji or text
#[derive(Clone)] #[derive(Clone, Serialize)]
pub struct FlairPart { pub struct FlairPart {
pub flair_part_type: String, pub flair_part_type: String,
pub value: String, pub value: String,
@ -96,12 +100,14 @@ impl FlairPart {
} }
} }
#[derive(Serialize)]
pub struct Author { pub struct Author {
pub name: String, pub name: String,
pub flair: Flair, pub flair: Flair,
pub distinguished: String, pub distinguished: String,
} }
#[derive(Serialize)]
pub struct Poll { pub struct Poll {
pub poll_options: Vec<PollOption>, pub poll_options: Vec<PollOption>,
pub voting_end_timestamp: (String, String), pub voting_end_timestamp: (String, String),
@ -129,6 +135,7 @@ impl Poll {
} }
} }
#[derive(Serialize)]
pub struct PollOption { pub struct PollOption {
pub id: u64, pub id: u64,
pub text: String, pub text: String,
@ -158,13 +165,14 @@ impl PollOption {
} }
// Post flags with nsfw and stickied // Post flags with nsfw and stickied
#[derive(Serialize)]
pub struct Flags { pub struct Flags {
pub spoiler: bool, pub spoiler: bool,
pub nsfw: bool, pub nsfw: bool,
pub stickied: bool, pub stickied: bool,
} }
#[derive(Debug)] #[derive(Debug, Serialize)]
pub struct Media { pub struct Media {
pub url: String, pub url: String,
pub alt_url: String, pub alt_url: String,
@ -264,6 +272,7 @@ impl Media {
} }
} }
#[derive(Serialize)]
pub struct GalleryMedia { pub struct GalleryMedia {
pub url: String, pub url: String,
pub width: i64, pub width: i64,
@ -304,6 +313,7 @@ impl GalleryMedia {
} }
// Post containing content, metadata and media // Post containing content, metadata and media
#[derive(Serialize)]
pub struct Post { pub struct Post {
pub id: String, pub id: String,
pub title: String, pub title: String,
@ -470,7 +480,7 @@ pub struct Comment {
pub prefs: Preferences, pub prefs: Preferences,
} }
#[derive(Default, Clone)] #[derive(Default, Clone, Serialize)]
pub struct Award { pub struct Award {
pub name: String, pub name: String,
pub icon_url: String, pub icon_url: String,
@ -484,6 +494,7 @@ impl std::fmt::Display for Award {
} }
} }
#[derive(Serialize)]
pub struct Awards(pub Vec<Award>); pub struct Awards(pub Vec<Award>);
impl std::ops::Deref for Awards { impl std::ops::Deref for Awards {
@ -496,7 +507,7 @@ impl std::ops::Deref for Awards {
impl std::fmt::Display for Awards { impl std::fmt::Display for Awards {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 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 available_mascots: Vec<String>,
pub theme: String, pub theme: String,
pub mascot: String, pub mascot: String,
pub redsunlib_colorway: String,
pub front_page: String, pub front_page: String,
pub layout: String, pub layout: String,
pub wide: String, pub wide: String,
@ -603,6 +615,7 @@ pub struct Preferences {
pub show_nsfw: String, pub show_nsfw: String,
pub blur_nsfw: String, pub blur_nsfw: String,
pub hide_hls_notification: String, pub hide_hls_notification: String,
pub video_quality: String,
pub hide_sidebar_and_summary: String, pub hide_sidebar_and_summary: String,
pub use_hls: String, pub use_hls: String,
pub ffmpeg_video_downloads: String, pub ffmpeg_video_downloads: String,
@ -612,6 +625,7 @@ pub struct Preferences {
pub comment_sort: String, pub comment_sort: String,
pub post_sort: String, pub post_sort: String,
pub subscriptions: Vec<String>, pub subscriptions: Vec<String>,
pub quicklist: Vec<String>,
pub filters: Vec<String>, pub filters: Vec<String>,
pub hide_awards: String, pub hide_awards: String,
pub hide_score: String, pub hide_score: String,
@ -649,6 +663,7 @@ impl Preferences {
available_mascots: mascots, available_mascots: mascots,
theme: setting(req, "theme"), theme: setting(req, "theme"),
mascot: setting(req, "mascot"), mascot: setting(req, "mascot"),
redsunlib_colorway: setting(req, "redsunlib_colorway"),
front_page: setting(req, "front_page"), front_page: setting(req, "front_page"),
layout: setting(req, "layout"), layout: setting(req, "layout"),
wide: setting(req, "wide"), wide: setting(req, "wide"),
@ -659,6 +674,7 @@ impl Preferences {
use_hls: setting(req, "use_hls"), use_hls: setting(req, "use_hls"),
ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"), ffmpeg_video_downloads: setting(req, "ffmpeg_video_downloads"),
hide_hls_notification: setting(req, "hide_hls_notification"), hide_hls_notification: setting(req, "hide_hls_notification"),
video_quality: setting(req, "video_quality"),
autoplay_videos: setting(req, "autoplay_videos"), autoplay_videos: setting(req, "autoplay_videos"),
fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()), fixed_navbar: setting_or_default(req, "fixed_navbar", "on".to_string()),
disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"), disable_visit_reddit_confirmation: setting(req, "disable_visit_reddit_confirmation"),
@ -666,6 +682,7 @@ impl Preferences {
post_sort: setting(req, "post_sort"), post_sort: setting(req, "post_sort"),
subscriptions: setting(req, "subscriptions").split('+').map(String::from).filter(|s| !s.is_empty()).collect(), 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(), 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_awards: setting(req, "hide_awards"),
hide_score: setting(req, "hide_score"), 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 { nav #links svg {
display: none; /*display: none;*/
position: relative;
/*What's nine plus ten?*/ top: /*Twenty one*/21%; /*You stupid.*/
} }
nav #redlib { nav #redlib {
@ -429,7 +431,7 @@ a:hover {
} }
svg { svg {
stroke: var(--text); color: var(--text);
} }
img[src=""] { img[src=""] {
@ -523,10 +525,14 @@ aside {
margin-bottom: 20px; margin-bottom: 20px;
} }
#user_actions,
#sub_actions {
display: grid;
grid-template-columns: auto 4fr 1fr;
}
#user_details, #user_details,
#sub_details, #sub_details {
#sub_actions,
#user_actions {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
} }
@ -545,6 +551,8 @@ aside {
#user_subscription, #user_subscription,
#sub_filter, #sub_filter,
#user_filter, #user_filter,
#sub_quicklist,
#user_quicklist,
#sub_rss, #sub_rss,
#user_rss { #user_rss {
margin-top: 20px; margin-top: 20px;
@ -554,9 +562,16 @@ aside {
margin-bottom: 20px; margin-bottom: 20px;
} }
#sub_quicklist button,
#user_quicklist button {
padding: 0px;
}
.subscribe, .subscribe,
.unsubscribe, .unsubscribe,
.filter, .filter,
.unquick,
.quick,
.unfilter { .unfilter {
padding: 10px 20px; padding: 10px 20px;
border-radius: 5px; border-radius: 5px;
@ -568,12 +583,22 @@ aside {
color: var(--foreground); color: var(--foreground);
background-color: var(--accent); background-color: var(--accent);
} }
.quick {
color: var(--foreground);
background-color: var(--accent);
height: 18px;
}
.unsubscribe, .unsubscribe,
.unfilter { .unfilter {
color: var(--text); color: var(--text);
background-color: var(--highlighted); background-color: var(--highlighted);
} }
.unquick {
color: var(--text);
background-color: var(--highlighted);
height: 18px;
}
/* Feeds */ /* Feeds */
@ -1456,6 +1481,7 @@ a.search_subreddit:hover {
.comment:not([id]) .comment_data > .comment_link { .comment:not([id]) .comment_data > .comment_link {
display: -webkit-box; display: -webkit-box;
line-clamp: 1;
-webkit-line-clamp: 1; -webkit-line-clamp: 1;
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
word-break: break-all; word-break: break-all;
@ -1965,6 +1991,13 @@ th {
color: var(--accent); color: var(--accent);
} }
#issue_warning {
color: var(--popup-toreddit-text);
background: var(--popup-background);
border-radius: 5px;
padding: 10px;
}
/* Messages */ /* Messages */
#duplicates_msg h3 { #duplicates_msg h3 {
@ -2261,8 +2294,10 @@ th {
max-width: 20em; max-width: 20em;
} }
.download { .download {
padding-left: 8px; padding-left: 8px;
padding-right: 8px; padding-right: 8px;
padding-top: 3px;
padding-bottom: 1px;
font-size: 20px; font-size: 20px;
font-weight: 900; font-weight: 900;
color: var(--accent); color: var(--accent);
@ -2277,3 +2312,10 @@ th {
.download:active { .download:active {
background-color: var(--background); 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 // @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0
let ffmpeg = null; 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 () { (function () {
if (Hls.isSupported()) { if (Hls.isSupported()) {
@ -13,7 +17,7 @@ let ffmpeg = null;
var autoplay = oldVideo.classList.contains("hls_autoplay"); var autoplay = oldVideo.classList.contains("hls_autoplay");
// If HLS is supported natively then don't use hls.js // 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) { if (autoplay) {
oldVideo.play(); oldVideo.play();
} }
@ -82,7 +86,7 @@ let ffmpeg = null;
var mediaStream = []; var mediaStream = [];
var downloadButton = document.createElement('button'); var downloadButton = document.createElement('button');
downloadButton.classList.add('video-options','download'); downloadButton.classList.add('video-options','download');
downloadButton.innerText = "⏳" downloadButton.innerHTML = loadingsvg
const mergeStreams = async () => { const mergeStreams = async () => {
if (ffmpeg === null) { if (ffmpeg === null) {
ffmpeg = new FFmpeg(); ffmpeg = new FFmpeg();
@ -115,7 +119,7 @@ let ffmpeg = null;
var filename = toSlug(videoElement.parentNode.parentNode.id || document.title) var filename = toSlug(videoElement.parentNode.parentNode.id || document.title)
const data = await ffmpeg.readFile('output.mp4'); 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 return
} }
function saveAs(blob, filename) { // Yeah ok... function saveAs(blob, filename) { // Yeah ok...
@ -157,11 +161,11 @@ let ffmpeg = null;
var isDownloading = false var isDownloading = false
function startDownload() { function startDownload() {
if (!isDownloading) { isDownloading = true } else { return } if (!isDownloading) { isDownloading = true } else { return }
downloadButton.innerText = "⏳" downloadButton.innerHTML = loadingsvg
mergeStreams() mergeStreams()
.then(_ => { .then(_ => {
isDownloading = false isDownloading = false
downloadButton.innerText = "⭳" downloadButton.innerHTML = downloadsvg
}); });
} }
@ -178,7 +182,7 @@ let ffmpeg = null;
waitForLoad(_ => flag === true) waitForLoad(_ => flag === true)
.then(_ => { .then(_ => {
downloadButton.innerText = "⭳" downloadButton.innerHTML = downloadsvg
downloadButton.addEventListener('click', startDownload); downloadButton.addEventListener('click', startDownload);
}); });
} }

View File

@ -27,6 +27,8 @@
<link rel="manifest" type="application/json" href="/manifest.json"> <link rel="manifest" type="application/json" href="/manifest.json">
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico"> <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") }}"> <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 %} {% endblock %}
</head> </head>
<body class=" <body class="
@ -38,27 +40,35 @@
<nav class=" <nav class="
{% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}"> {% if prefs.fixed_navbar == "on" %} fixed_navbar{% endif %}">
<div id="logo"> <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 %} {% block subscriptions %}{% endblock %}
</div> </div>
{% block search %}{% endblock %} {% block search %}{% endblock %}
<div id="links"> <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 %}> <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">
<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>redirect to reddit</title>
<path d="M22 2L12 22"/> <g fill="none">
<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"/> <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> </svg>
<span>reddit</span>
</a> </a>
{% if prefs.disable_visit_reddit_confirmation != "on" %} {% if prefs.disable_visit_reddit_confirmation != "on" %}
{% call utils::visit_reddit_confirmation(url) %} {% call utils::visit_reddit_confirmation(url) %}
{% endif %} {% endif %}
<a id="settings_link" href="/settings"> <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">
<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"> <g fill="none" fill-rule="evenodd">
<title>settings</title> <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"/> <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> </svg>
<span>settings</span>
</a> </a>
</div> </div>
</nav> </nav>

View File

@ -6,9 +6,18 @@
<h1>{{ msg }}</h1> <h1>{{ msg }}</h1>
<h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3> <h3><a href="https://www.redditstatus.com/">Reddit Status</a></h3>
<br /> <br />
<h3>Expected something to work? <a <h3 id="update-status"></h3>
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 <br />
an issue</a></h3> <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 /> <br />
<h3>Head back <a href="/">home</a>?</h3> <h3>Head back <a href="/">home</a>?</h3>
</div> </div>

View File

@ -29,6 +29,11 @@
{% call utils::options(prefs.mascot, prefs.available_mascots, "system") %} {% call utils::options(prefs.mascot, prefs.available_mascots, "system") %}
</select> </select>
</div> </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>
<fieldset> <fieldset>
<legend>Interface</legend> <legend>Interface</legend>
@ -68,6 +73,12 @@
</fieldset> </fieldset>
<fieldset> <fieldset>
<legend>Content</legend> <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"> <div class="prefs-group">
<label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label> <label for="post_sort" title="Applies only to subreddit feeds">Default subreddit post sort:</label>
<select name="post_sort"> <select name="post_sort">
@ -147,7 +158,7 @@
</form> </form>
<div id="settings_note"> <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><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> </div>
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<div class="prefs" id="settings_subs"> <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 }}"> <img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}">
<h1 id="sub_title">{{ sub.title }}</h1> <h1 id="sub_title">{{ sub.title }}</h1>
<p id="sub_name">r/{{ sub.name }}</p> <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> <p id="sub_description">{{ sub.description }}</p>
<div id="sub_details"> <div id="sub_details">
<label>Members</label> <label>Members</label>
@ -134,13 +146,31 @@
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if crate::utils::enable_rss() %} <div id="sub_quicklist">
<div id="sub_rss"> {% if prefs.quicklist.contains(sub.name) %}
<a href="/r/{{ sub.name }}.rss" title="RSS feed for r/{{ sub.name }}"> <form action="/r/{{ sub.name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
<button class="subscribe">RSS feed</button > <button>
</a> <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> </div>
{% endif %}
</div> </div>
</details> </details>
<details class="panel" id="sidebar" open> <details class="panel" id="sidebar" open>

View File

@ -123,6 +123,18 @@ body %}
/> />
<h1 id="user_title">{{ user.title }}</h1> <h1 id="user_title">{{ user.title }}</h1>
<p id="user_name">u/{{ user.name }}</p> <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_description">{{ user.description }}</div>
<div id="user_details"> <div id="user_details">
<label>Karma</label> <label>Karma</label>
@ -166,16 +178,31 @@ body %}
</form> </form>
{% endif %} {% endif %}
</div> </div>
{% if crate::utils::enable_rss() %} <div id="user_quicklist">
<div id="user_rss"> {% if prefs.quicklist.contains(name) %}
<a <form action="/r/{{ name }}/unquicklist?redirect={{ redirect_url }}" method="POST">
href="/u/{{ user.name }}.rss" <button>
title="RSS feed for u/{{ user.name }}" <svg class="unquick" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24">
> <g fill="none">
<button class="subscribe">RSS feed</button> <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" />
</a> <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> </div>
{% endif %}
</div> </div>
</div> </div>
</aside> </aside>

View File

@ -41,6 +41,12 @@
<details id="feeds"> <details id="feeds">
<summary>Feeds</summary> <summary>Feeds</summary>
<div id="feed_list"> <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> <p>MAIN FEEDS</p>
<a href="/">Home</a> <a href="/">Home</a>
<a href="/r/popular">Popular</a> <a href="/r/popular">Popular</a>
@ -48,7 +54,7 @@
{% if prefs.subscriptions.len() > 0 %} {% if prefs.subscriptions.len() > 0 %}
<p>REDDIT FEEDS</p> <p>REDDIT FEEDS</p>
{% for sub in prefs.subscriptions %} {% 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 %} {% endfor %}
{% endif %} {% endif %}
</div> </div>