Compare commits
79 Commits
Author | SHA1 | Date | |
---|---|---|---|
aa54301054 | |||
b4d3f03335 | |||
1a1ff2e600 | |||
4fc07c02b5 | |||
8d58cf61d2 | |||
711e3c205d | |||
0704eb10b8 | |||
ef86c1be86 | |||
8141b74817 | |||
57d304161b | |||
b5f21bcb97 | |||
36c560144a | |||
2bc714d0c5 | |||
ff4a515e24 | |||
93f089c2cf | |||
23569206cc | |||
5f20e8ee27 | |||
a8a8980b98 | |||
fd7d977835 | |||
50f26333cb | |||
f5cd48b07f | |||
50665bbeb3 | |||
d558127306 | |||
0c757023f9 | |||
90828cc71c | |||
7f5bfc04b3 | |||
322aa97a18 | |||
7e07ca3df1 | |||
428dc58e3c | |||
0ec8e4e9a2 | |||
60c7b6b23f | |||
1c8bcf33c1 | |||
3bdc21f90a | |||
c3dade257d | |||
62b2bbb231 | |||
653aee9294 | |||
bb7fb1313d | |||
01bc729a80 | |||
39e6e6bf81 | |||
8c94c0dd17 | |||
1c50c8f30d | |||
3facaefb53 | |||
aec45311cc | |||
47ab857103 | |||
a9ef5bc08b | |||
eb6c5e5e1e | |||
ed11135af8 | |||
3a1af78e26 | |||
345770c64d | |||
9eb42932df | |||
f0a6bdc21b | |||
3eef60d486 | |||
59043456ba | |||
90c7088da2 | |||
9e65a65556 | |||
8cfbde2710 | |||
70ff150ab4 | |||
388779c1f2 | |||
6b605d859f | |||
0ae48c400c | |||
a6ed18d674 | |||
838cdd95d1 | |||
bc95b08ffd | |||
e6190267e4 | |||
3ceeac5fb0 | |||
60eb0137c2 | |||
b6bca68d4e | |||
91bff826f0 | |||
af6606a855 | |||
977cd0763a | |||
fcadd44cb3 | |||
9c325c2cbf | |||
e9038f4fe2 | |||
8b8f55e09a | |||
f1b3749cf0 | |||
0708fdfb37 | |||
cad29e9544 | |||
6b59976fcf | |||
f9b3981448 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
||||
liberapay: spike
|
||||
custom: ['https://www.buymeacoffee.com/spikecodes']
|
||||
|
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 🐛 Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
title: '🐛 Bug Report: '
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
@ -12,7 +12,7 @@ assignees: ''
|
||||
A clear and concise description of what the bug is.
|
||||
-->
|
||||
|
||||
## To reproduce
|
||||
## Steps to reproduce the bug
|
||||
|
||||
<!--
|
||||
Steps to reproduce the behavior:
|
||||
@ -22,12 +22,12 @@ Steps to reproduce the behavior:
|
||||
4. See error
|
||||
-->
|
||||
|
||||
## Expected behavior
|
||||
## What's the expected behavior?
|
||||
<!--
|
||||
A clear and concise description of what you expected to happen.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
## Additional context / screenshot
|
||||
<!--
|
||||
Add any other context about the problem here.
|
||||
-->
|
||||
|
6
.github/ISSUE_TEMPLATE/feature_parity.md
vendored
6
.github/ISSUE_TEMPLATE/feature_parity.md
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: ✨ Feature parity
|
||||
about: Suggest implementing a feature into Libreddit that is found in Reddit.com
|
||||
title: ''
|
||||
title: '✨ Feature parity: '
|
||||
labels: feature parity
|
||||
assignees: ''
|
||||
|
||||
@ -12,7 +12,7 @@ assignees: ''
|
||||
A clear and concise description of what the feature is.
|
||||
-->
|
||||
|
||||
## Describe the implementation into Libreddit
|
||||
## Describe how this could be implemented into Libreddit
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
-->
|
||||
@ -22,7 +22,7 @@ assignees: ''
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
## Additional context / screenshot
|
||||
<!--
|
||||
Add any other context or screenshots about the feature parity request here.
|
||||
-->
|
||||
|
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
6
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,7 +1,7 @@
|
||||
---
|
||||
name: 💡 Feature request
|
||||
about: Suggest a feature for Libreddit that is not found in Reddit
|
||||
title: ''
|
||||
title: '💡 Feature request: '
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
@ -12,7 +12,7 @@ assignees: ''
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
-->
|
||||
|
||||
## Describe the solution you'd like
|
||||
## Describe the feature you would like to be implemented
|
||||
<!--
|
||||
A clear and concise description of what you want to happen.
|
||||
-->
|
||||
@ -22,7 +22,7 @@ assignees: ''
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
-->
|
||||
|
||||
## Additional context
|
||||
## Additional context / screenshot
|
||||
<!--
|
||||
Add any other context or screenshots about the feature request here.
|
||||
-->
|
||||
|
2
.github/workflows/docker-arm.yml
vendored
2
.github/workflows/docker-arm.yml
vendored
@ -34,3 +34,5 @@ jobs:
|
||||
platforms: linux/arm64
|
||||
push: true
|
||||
tags: spikecodes/libreddit:arm
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
2
.github/workflows/docker-armv7.yml
vendored
2
.github/workflows/docker-armv7.yml
vendored
@ -37,3 +37,5 @@ jobs:
|
||||
platforms: linux/arm/v7
|
||||
push: true
|
||||
tags: spikecodes/libreddit:armv7
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
3
.github/workflows/docker.yml
vendored
3
.github/workflows/docker.yml
vendored
@ -34,4 +34,5 @@ jobs:
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: spikecodes/libreddit:latest
|
||||
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
969
Cargo.lock
generated
969
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
28
Cargo.toml
28
Cargo.toml
@ -3,23 +3,25 @@ name = "libreddit"
|
||||
description = " Alternative private front-end to Reddit"
|
||||
license = "AGPL-3.0"
|
||||
repository = "https://github.com/spikecodes/libreddit"
|
||||
version = "0.21.0"
|
||||
version = "0.23.0"
|
||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
askama = { version = "0.11.0", default-features = false }
|
||||
async-recursion = "0.3.2"
|
||||
cached = "0.26.2"
|
||||
clap = { version = "2.34.0", default-features = false }
|
||||
regex = "1.5.4"
|
||||
serde = { version = "1.0.132", features = ["derive"] }
|
||||
cookie = "0.15.1"
|
||||
askama = { version = "0.11.1", default-features = false }
|
||||
async-recursion = "1.0.0"
|
||||
cached = "0.40.0"
|
||||
clap = { version = "4.0.18", default-features = false, features = ["std"] }
|
||||
regex = "1.6.0"
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
cookie = "0.16.1"
|
||||
futures-lite = "1.12.0"
|
||||
hyper = { version = "0.14.16", features = ["full"] }
|
||||
hyper = { version = "0.14.22", features = ["full"] }
|
||||
hyper-rustls = "0.23.0"
|
||||
percent-encoding = "2.2.0"
|
||||
route-recognizer = "0.3.1"
|
||||
serde_json = "1.0.73"
|
||||
tokio = { version = "1.15.0", features = ["full"] }
|
||||
time = "0.2.7"
|
||||
url = "2.2.2"
|
||||
serde_json = "1.0.87"
|
||||
tokio = { version = "1.21.2", features = ["full"] }
|
||||
time = "0.3.16"
|
||||
url = "2.3.1"
|
||||
rust-embed = { version = "6.4.2", features = ["include-exclude"] }
|
||||
|
12
FUNDING.yml
12
FUNDING.yml
@ -1,12 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
||||
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
|
||||
liberapay: spike
|
||||
issuehunt: # Replace with a single IssueHunt username
|
||||
otechie: # Replace with a single Otechie username
|
||||
custom: ['https://www.buymeacoffee.com/spikecodes']
|
84
README.md
84
README.md
@ -8,7 +8,7 @@
|
||||
|
||||
**10 second pitch:** Libreddit is a portmanteau of "libre" (meaning freedom) and "Reddit". It is a private front-end like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libreddit.spike.codes/r/unpopularopinion) without being [tracked](#reddit).
|
||||
|
||||
- 🚀 Fast: written in Rust for blazing fast speeds and memory safety
|
||||
- 🚀 Fast: written in Rust for blazing-fast speeds and memory safety
|
||||
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
|
||||
- 🕵 Private: all requests are proxied through the server, including media
|
||||
- 🔒 Secure: strong [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) prevents browser requests to Reddit
|
||||
@ -17,11 +17,13 @@
|
||||
|
||||
I appreciate any donations! Your support allows me to continue developing Libreddit.
|
||||
|
||||
<a href="https://www.buymeacoffee.com/spikecodes" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 50px" ></a>
|
||||
<a href="https://www.buymeacoffee.com/spikecodes" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 40px" ></a>
|
||||
<a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg" style="height: 40px"></a>
|
||||
|
||||
**Bitcoin:** [bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y](bitcoin:bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y)
|
||||
|
||||
**Monero:** [45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR](monero:45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR)
|
||||
**Bitcoin:** `bc1qwyxjnafpu3gypcpgs025cw9wa7ryudtecmwa6y`
|
||||
|
||||
**Monero:** `45FJrEuFPtG2o7QZz2Nps77TbHD4sPqxViwbdyV9A6ktfHiWs47UngG5zXPcLoDXAc8taeuBgeNjfeprwgeXYXhN3C9tVSR`
|
||||
|
||||
---
|
||||
|
||||
@ -29,43 +31,62 @@ I appreciate any donations! Your support allows me to continue developing Libred
|
||||
|
||||
Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new) to have your [selfhosted instance](#deployment) listed here!
|
||||
|
||||
🔗 **Want to automatically redirect Reddit links to Libreddit? Use [LibRedirect](https://github.com/libredirect/libredirect) or [Privacy Redirect](https://github.com/SimonBrazell/privacy-redirect)!**
|
||||
|
||||
| Website | Country | Cloudflare |
|
||||
|-|-|-|
|
||||
| [libredd.it](https://libredd.it) (official) | 🇺🇸 US | |
|
||||
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
|
||||
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇩🇪 DE | ✅ |
|
||||
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | |
|
||||
| [libreddit.40two.app](https://libreddit.40two.app) | 🇳🇱 NL | |
|
||||
| [reddit.invak.id](https://reddit.invak.id) | 🇧🇬 BG | |
|
||||
| [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
|
||||
| [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
|
||||
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | ✅ |
|
||||
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
|
||||
| [libreddit.strongthany.cc](https://libreddit.strongthany.cc) | 🇺🇸 US | |
|
||||
| [libreddit.privacy.com.de](https://libreddit.privacy.com.de) | 🇩🇪 DE | |
|
||||
| [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.sugoma.tk](https://libreddit.sugoma.tk) | 🇺🇸 US | |
|
||||
| [libreddit.jamiethalacker.dev](https://libreddit.jamiethalacker.dev) | 🇺🇸 US | ✅ |
|
||||
| [reddit.artemislena.eu](https://reddit.artemislena.eu) | 🇩🇪 DE | |
|
||||
| [r.nf](https://r.nf) | 🇩🇪 DE | ✅ |
|
||||
| [libreddit.awesomehub.io](https://libreddit.awesomehub.io) | 🇫🇮 FI | |
|
||||
| [libreddit.some-things.org](https://libreddit.some-things.org) | 🇨🇭 CH | |
|
||||
| [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | |
|
||||
| [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | |
|
||||
| [libreddit.alefvanoon.xyz](https://libreddit.alefvanoon.xyz) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.igna.rocks](https://libreddit.igna.rocks) | 🇺🇸 US | |
|
||||
| [libreddit.autarkic.org](https://libreddit.autarkic.org) | 🇺🇸 US | |
|
||||
| [libreddit.flux.industries](https://libreddit.flux.industries) | 🇩🇪 DE | ✅ |
|
||||
| [libreddit.drivet.xyz](https://libreddit.drivet.xyz) | 🇫🇮 FI | ✅ |
|
||||
| [lr.oversold.host](https://lr.oversold.host) | 🇱🇺 LU | |
|
||||
| [libreddit.intent.cool](https://libreddit.intent.cool) | 🇺🇸 US | |
|
||||
| [libreddit.drivet.xyz](https://libreddit.drivet.xyz) | 🇵🇱 PL | |
|
||||
| [libreddit.de](https://libreddit.de) | 🇩🇪 DE | |
|
||||
| [libreddit.pussthecat.org](https://libreddit.pussthecat.org) | 🇩🇪 DE | |
|
||||
| [libreddit.mutahar.rocks](https://libreddit.mutahar.rocks) | 🇫🇷 FR | |
|
||||
| [libreddit.northboot.xyz](https://libreddit.northboot.xyz) | 🇩🇪 DE | |
|
||||
| [leddit.xyz](https://www.leddit.xyz) | 🇩🇪 DE | |
|
||||
| [leddit.xyz](https://leddit.xyz) | 🇺🇸 US | |
|
||||
| [de.leddit.xyz](https://de.leddit.xyz) | 🇩🇪 DE | |
|
||||
| [lr.cowfee.moe](https://lr.cowfee.moe) | 🇺🇸 US | |
|
||||
| [libreddit.hu](https://libreddit.hu) | 🇫🇮 FI | ✅ |
|
||||
| [libreddit.totaldarkness.net](https://libreddit.totaldarkness.net) | 🇨🇦 CA | |
|
||||
| [libreddit.esmailelbob.xyz](https://libreddit.esmailelbob.xyz) | 🇪🇬 EG | |
|
||||
| [libreddit.esmailelbob.xyz](https://libreddit.esmailelbob.xyz) | 🇨🇦 CA | |
|
||||
| [lr.vern.cc](https://lr.vern.cc) | 🇨🇦 CA | |
|
||||
| [libreddit.nl](https://libreddit.nl) | 🇳🇱 NL | |
|
||||
| [lr.stilic.ml](https://lr.stilic.ml) | 🇫🇷 FR | ✅ |
|
||||
| [reddi.tk](https://reddi.tk) | 🇺🇸 US | ✅ |
|
||||
| [libreddit.bus-hit.me](https://libreddit.bus-hit.me) | 🇨🇦 CA | |
|
||||
| [r.walkx.org](https://r.walkx.org) | 🇳🇱 NL | ✅ |
|
||||
| [libreddit.kylrth.com](https://libreddit.kylrth.com) | 🇨🇦 CA | |
|
||||
| [libreddit.yonalee.eu](https://libreddit.yonalee.eu) | 🇱🇺 LU | ✅ |
|
||||
| [libreddit.winscloud.net](https://libreddit.winscloud.net) | 🇹🇭 TH | ✅ |
|
||||
| [libreddit.tiekoetter.com](https://libreddit.tiekoetter.com) | 🇩🇪 DE | |
|
||||
| [reddit.rtrace.io](https://reddit.rtrace.io) | 🇩🇪 DE | |
|
||||
| [libreddit.lunar.icu](https://libreddit.lunar.icu) | 🇩🇪 DE | ✅ |
|
||||
| [libreddit.privacydev.net](https://libreddit.privacydev.net) | 🇺🇸 US | |
|
||||
| [libreddit.notyourcomputer.net](https://libreddit.notyourcomputer.net) | 🇺🇸 US | |
|
||||
| [r.ahwx.org](https://r.ahwx.org) | 🇳🇱 NL | ✅ |
|
||||
| [bob.fr.to](https://bob.fr.to) | 🇺🇸 US | |
|
||||
| [reddit.beparanoid.de](https://reddit.beparanoid.de) | 🇨🇭 CH | |
|
||||
| [libreddit.dcs0.hu](https://libreddit.dcs0.hu) | 🇭🇺 HU | |
|
||||
| [reddit.dr460nf1r3.org](https://reddit.dr460nf1r3.org) | 🇩🇪 DE | ✅ |
|
||||
| [rd.jae.su](https://rd.jae.su) | 🇫🇮 FI | |
|
||||
| [libreddit.mha.fi](https://libreddit.mha.fi) | 🇫🇮 FI | |
|
||||
| [libreddit.foss.wtf](https://libreddit.foss.wtf) | 🇩🇪 DE | |
|
||||
| [libreddit.encrypted-data.xyz](https://libreddit.encrypted-data.xyz)| 🇫🇷 FR | ✅ |
|
||||
| [libreddit.eu.org](https://libreddit.eu.org)| 🇮🇪 IE | ✅ |
|
||||
| [l.opnxng.com](https://l.opnxng.com)| 🇸🇬 SG | |
|
||||
| [libreddit.cachyos.org](https://libreddit.cachyos.org) | 🇩🇪 DE | ✅ |
|
||||
| [libreddit.oxymagnesium.com](https://libreddit.oxymagnesium.com) | 🇺🇸 US | |
|
||||
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
|
||||
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
|
||||
@ -76,9 +97,14 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
|
||||
| [ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion](http://ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion) | 🇺🇸 US | |
|
||||
| [libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion](http://libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion) | 🇺🇸 US | |
|
||||
| [libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion](http://libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion) | 🇪🇬 EG | |
|
||||
|
||||
|
||||
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare). The checkmark will not be listed for a site which uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||
| [ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion](http://ol5begilptoou34emq2sshf3may3hlblvipdjtybbovpb7c7zodxmtqd.onion) | 🇩🇪 DE | |
|
||||
| [lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion](http://lbrdtjaj7567ptdd4rv74lv27qhxfkraabnyphgcvptl64ijx2tijwid.onion) | 🇨🇦 CA | |
|
||||
| [libreddit.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion](http://libreddit.esmail5pdn24shtvieloeedh7ehz3nrwcdivnfhfcedl7gf4kwddhkqd.onion) | 🇨🇦 CA | |
|
||||
| [reddit.prnoid54e44a4bduq5due64jkk7wcnkxcp5kv3juncm7veptjcqudgyd.onion](http://reddit.prnoid54e44a4bduq5due64jkk7wcnkxcp5kv3juncm7veptjcqudgyd.onion) | 🇨🇭 CH | |
|
||||
| [inz6tbezfwzexva6dize4cqraj2tjdhygxabmcgysccesvw2pybzhbyd.onion](http://inz6tbezfwzexva6dize4cqraj2tjdhygxabmcgysccesvw2pybzhbyd.onion) | 🇫🇮 FI | |
|
||||
| [libreddit.micohauwkjbyw5meacrb4ipicwvwg4xtzl7y7viv53kig2mdcsvwkyyd.onion](http://libreddit.micohauwkjbyw5meacrb4ipicwvwg4xtzl7y7viv53kig2mdcsvwkyyd.onion/)| 🇫🇮 FI | |
|
||||
| [lr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion](http://lr.vernccvbvyi5qhfzyqengccj7lkove6bjot2xhh5kajhwvidqafczrad.onion/) | 🇨🇦 CA | |
|
||||
A checkmark in the "Cloudflare" category here refers to the use of the reverse proxy, [Cloudflare](https://cloudflare.com). The checkmark will not be listed for a site that uses Cloudflare DNS but rather the proxying service which grants Cloudflare the ability to monitor traffic to the website.
|
||||
|
||||
---
|
||||
|
||||
@ -141,7 +167,7 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot
|
||||
- The requested URL
|
||||
- Search terms
|
||||
|
||||
**Location:** The same privacy policy goes on to describe location data may be collected through the use of:
|
||||
**Location:** The same privacy policy goes on to describe that location data may be collected through the use of:
|
||||
- GPS (consensual)
|
||||
- Bluetooth (consensual)
|
||||
- Content associated with a location (consensual)
|
||||
@ -165,7 +191,7 @@ For transparency, I hope to describe all the ways Libreddit handles user privacy
|
||||
|
||||
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libreddit.spike.codes/settings). These are not cross-site cookies and the cookies hold no personal data.
|
||||
|
||||
**Hosting:** The official instances are hosted on [Replit](https://replit.com/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting, using unofficial instances and browsing through Tor are welcomed.
|
||||
**Hosting:** The official instances are hosted on [Replit](https://replit.com/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, self-hosting, using unofficial instances, and browsing through Tor are welcomed.
|
||||
|
||||
---
|
||||
|
||||
@ -237,8 +263,8 @@ Assign a default value for each setting by passing environment variables to Libr
|
||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||
| `WIDE` | `["on", "off"]` | `off` |
|
||||
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||
@ -256,7 +282,7 @@ LIBREDDIT_DEFAULT_WIDE=on LIBREDDIT_DEFAULT_THEME=dark libreddit -r
|
||||
|
||||
## Proxying using NGINX
|
||||
|
||||
**NOTE** If you're [proxying Libreddit through a NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
|
||||
**NOTE** If you're [proxying Libreddit through an NGINX Reverse Proxy](https://github.com/spikecodes/libreddit/issues/122#issuecomment-782226853), add
|
||||
```nginx
|
||||
proxy_http_version 1.1;
|
||||
```
|
||||
|
@ -1,2 +1,2 @@
|
||||
ADDRESS=localhost
|
||||
ADDRESS=0.0.0.0
|
||||
PORT=12345
|
||||
|
@ -11,5 +11,27 @@ Environment=PORT=8080
|
||||
EnvironmentFile=-/etc/libreddit.conf
|
||||
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
|
||||
|
||||
# Hardening
|
||||
DeviceAllow=
|
||||
LockPersonality=yes
|
||||
MemoryDenyWriteExecute=yes
|
||||
PrivateDevices=yes
|
||||
ProcSubset=pid
|
||||
ProtectClock=yes
|
||||
ProtectControlGroups=yes
|
||||
ProtectHome=yes
|
||||
ProtectHostname=yes
|
||||
ProtectKernelLogs=yes
|
||||
ProtectKernelModules=yes
|
||||
ProtectKernelTunables=yes
|
||||
ProtectProc=invisible
|
||||
RestrictAddressFamilies=AF_INET AF_INET6
|
||||
RestrictNamespaces=yes
|
||||
RestrictRealtime=yes
|
||||
RestrictSUIDSGID=yes
|
||||
SystemCallArchitectures=native
|
||||
SystemCallFilter=@system-service ~@privileged ~@resources
|
||||
UMask=0077
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
|
@ -1,6 +1,7 @@
|
||||
use cached::proc_macro::cached;
|
||||
use futures_lite::{future::Boxed, FutureExt};
|
||||
use hyper::{body::Buf, client, Body, Request, Response, Uri};
|
||||
use percent_encoding::{percent_encode, CONTROLS};
|
||||
use serde_json::Value;
|
||||
use std::result::Result;
|
||||
|
||||
@ -90,7 +91,7 @@ fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String
|
||||
.headers()
|
||||
.get("Location")
|
||||
.map(|val| {
|
||||
let new_url = val.to_str().unwrap_or_default();
|
||||
let new_url = percent_encode(val.as_bytes(), CONTROLS).to_string();
|
||||
format!("{}{}raw_json=1", new_url, if new_url.contains('?') { "&" } else { "?" })
|
||||
})
|
||||
.unwrap_or_default()
|
||||
|
56
src/main.rs
56
src/main.rs
@ -11,7 +11,7 @@ mod user;
|
||||
mod utils;
|
||||
|
||||
// Import Crates
|
||||
use clap::{App as cli, Arg};
|
||||
use clap::{Arg, Command};
|
||||
|
||||
use futures_lite::FutureExt;
|
||||
use hyper::{header::HeaderValue, Body, Request, Response};
|
||||
@ -19,7 +19,7 @@ use hyper::{header::HeaderValue, Body, Request, Response};
|
||||
mod client;
|
||||
use client::proxy;
|
||||
use server::RequestExt;
|
||||
use utils::{error, redirect};
|
||||
use utils::{error, redirect, ThemeAssets};
|
||||
|
||||
mod server;
|
||||
|
||||
@ -85,50 +85,67 @@ async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Respons
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn style() -> Result<Response<Body>, String> {
|
||||
let mut res = include_str!("../static/style.css").to_string();
|
||||
for file in ThemeAssets::iter() {
|
||||
res.push('\n');
|
||||
let theme = ThemeAssets::get(file.as_ref()).unwrap();
|
||||
res.push_str(std::str::from_utf8(theme.data.as_ref()).unwrap());
|
||||
}
|
||||
Ok(
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("content-type", "text/css")
|
||||
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||
.body(res.to_string().into())
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let matches = cli::new("Libreddit")
|
||||
let matches = Command::new("Libreddit")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Private front-end for Reddit written in Rust ")
|
||||
.arg(
|
||||
Arg::with_name("redirect-https")
|
||||
.short("r")
|
||||
Arg::new("redirect-https")
|
||||
.short('r')
|
||||
.long("redirect-https")
|
||||
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
|
||||
.takes_value(false),
|
||||
.num_args(0),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("address")
|
||||
.short("a")
|
||||
Arg::new("address")
|
||||
.short('a')
|
||||
.long("address")
|
||||
.value_name("ADDRESS")
|
||||
.help("Sets address to listen on")
|
||||
.default_value("0.0.0.0")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("port")
|
||||
.short("p")
|
||||
Arg::new("port")
|
||||
.short('p')
|
||||
.long("port")
|
||||
.value_name("PORT")
|
||||
.help("Port to listen on")
|
||||
.default_value("8080")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("hsts")
|
||||
.short("H")
|
||||
Arg::new("hsts")
|
||||
.short('H')
|
||||
.long("hsts")
|
||||
.value_name("EXPIRE_TIME")
|
||||
.help("HSTS header to tell browsers that this site should only be accessed over HTTPS")
|
||||
.default_value("604800")
|
||||
.takes_value(true),
|
||||
.num_args(1),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let address = matches.value_of("address").unwrap_or("0.0.0.0");
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| matches.value_of("port").unwrap_or("8080").to_string());
|
||||
let hsts = matches.value_of("hsts");
|
||||
let address = matches.get_one("address").map(|m: &String| m.as_str()).unwrap_or("0.0.0.0");
|
||||
let port = std::env::var("PORT").unwrap_or_else(|_| matches.get_one("port").map(|m: &String| m.as_str()).unwrap_or("8080").to_string());
|
||||
let hsts = matches.get_one("hsts").map(|m: &String| m.as_str());
|
||||
|
||||
let listener = [address, ":", &port].concat();
|
||||
|
||||
@ -152,7 +169,7 @@ async fn main() {
|
||||
}
|
||||
|
||||
// Read static files
|
||||
app.at("/style.css").get(|_| resource(include_str!("../static/style.css"), "text/css", false).boxed());
|
||||
app.at("/style.css").get(|_| style().boxed());
|
||||
app
|
||||
.at("/manifest.json")
|
||||
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
|
||||
@ -193,6 +210,7 @@ async fn main() {
|
||||
|
||||
app.at("/user/[deleted]").get(|req| error(req, "User has deleted their account".to_string()).boxed());
|
||||
app.at("/user/:name").get(|r| user::profile(r).boxed());
|
||||
app.at("/user/:name/:listing").get(|r| user::profile(r).boxed());
|
||||
app.at("/user/:name/comments/:id").get(|r| post::item(r).boxed());
|
||||
app.at("/user/:name/comments/:id/:title").get(|r| post::item(r).boxed());
|
||||
app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
||||
|
27
src/post.rs
27
src/post.rs
@ -1,6 +1,5 @@
|
||||
// CRATES
|
||||
use crate::client::json;
|
||||
use crate::esc;
|
||||
use crate::server::RequestExt;
|
||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||
use crate::utils::{
|
||||
@ -13,7 +12,7 @@ use std::collections::HashSet;
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "post.html", escape = "none")]
|
||||
#[template(path = "post.html")]
|
||||
struct PostTemplate {
|
||||
comments: Vec<Comment>,
|
||||
post: Post,
|
||||
@ -100,17 +99,18 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
||||
let permalink = val(post, "permalink");
|
||||
|
||||
let body = if val(post, "removed_by_category") == "moderator" {
|
||||
format!("<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}\">view removed post</a></p></div>", permalink)
|
||||
format!(
|
||||
"<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}\">view removed post</a></p></div>",
|
||||
permalink
|
||||
)
|
||||
} else {
|
||||
rewrite_urls(&val(post, "selftext_html")).replace("\\", "")
|
||||
rewrite_urls(&val(post, "selftext_html"))
|
||||
};
|
||||
|
||||
dbg!(val(post, "permalink"));
|
||||
|
||||
// Build a post using data parsed from Reddit post API
|
||||
Post {
|
||||
id: val(post, "id"),
|
||||
title: esc!(post, "title"),
|
||||
title: val(post, "title"),
|
||||
community: val(post, "subreddit"),
|
||||
body,
|
||||
author: Author {
|
||||
@ -121,7 +121,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
||||
post["data"]["author_flair_richtext"].as_array(),
|
||||
post["data"]["author_flair_text"].as_str(),
|
||||
),
|
||||
text: esc!(post, "link_flair_text"),
|
||||
text: val(post, "link_flair_text"),
|
||||
background_color: val(post, "author_flair_background_color"),
|
||||
foreground_color: val(post, "author_flair_text_color"),
|
||||
},
|
||||
@ -145,7 +145,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
||||
post["data"]["link_flair_richtext"].as_array(),
|
||||
post["data"]["link_flair_text"].as_str(),
|
||||
),
|
||||
text: esc!(post, "link_flair_text"),
|
||||
text: val(post, "link_flair_text"),
|
||||
background_color: val(post, "link_flair_background_color"),
|
||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
@ -201,9 +201,12 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
||||
let highlighted = id == highlighted_comment;
|
||||
|
||||
let body = if val(&comment, "author") == "[deleted]" && val(&comment, "body") == "[removed]" {
|
||||
format!("<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}{}\">view removed comment</a></p></div>", post_link, id)
|
||||
format!(
|
||||
"<div class=\"md\"><p>[removed] — <a href=\"https://www.reveddit.com{}{}\">view removed comment</a></p></div>",
|
||||
post_link, id
|
||||
)
|
||||
} else {
|
||||
rewrite_urls(&val(&comment, "body_html")).to_string()
|
||||
rewrite_urls(&val(&comment, "body_html"))
|
||||
};
|
||||
|
||||
let author = Author {
|
||||
@ -214,7 +217,7 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
||||
data["author_flair_richtext"].as_array(),
|
||||
data["author_flair_text"].as_str(),
|
||||
),
|
||||
text: esc!(&comment, "link_flair_text"),
|
||||
text: val(&comment, "link_flair_text"),
|
||||
background_color: val(&comment, "author_flair_background_color"),
|
||||
foreground_color: val(&comment, "author_flair_text_color"),
|
||||
},
|
||||
|
@ -29,7 +29,7 @@ struct Subreddit {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "search.html", escape = "none")]
|
||||
#[template(path = "search.html")]
|
||||
struct SearchTemplate {
|
||||
posts: Vec<Post>,
|
||||
subreddits: Vec<Subreddit>,
|
||||
@ -54,6 +54,10 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
return Ok(redirect("/".to_string()));
|
||||
}
|
||||
|
||||
if query.starts_with("r/") {
|
||||
return Ok(redirect(format!("/{}", query)));
|
||||
}
|
||||
|
||||
let sub = req.param("sub").unwrap_or_default();
|
||||
let quarantined = can_access_quarantine(&req, &sub);
|
||||
// Handle random subreddits
|
||||
|
@ -105,7 +105,7 @@ impl ResponseExt for Response<Body> {
|
||||
fn remove_cookie(&mut self, name: String) {
|
||||
let mut cookie = Cookie::named(name);
|
||||
cookie.set_path("/");
|
||||
cookie.set_max_age(Duration::second());
|
||||
cookie.set_max_age(Duration::seconds(1));
|
||||
if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) {
|
||||
self.headers_mut().append("Set-Cookie", val);
|
||||
}
|
||||
@ -158,8 +158,8 @@ impl Server {
|
||||
Ok::<_, String>(service_fn(move |req: Request<Body>| {
|
||||
let headers = default_headers.clone();
|
||||
|
||||
// Remove double slashes
|
||||
let mut path = req.uri().path().replace("//", "/");
|
||||
// Remove double slashes and decode encoded slashes
|
||||
let mut path = req.uri().path().replace("//", "/").replace("%2F","/");
|
||||
|
||||
// Remove trailing slashes
|
||||
if path != "/" && path.ends_with('/') {
|
||||
|
@ -1,5 +1,4 @@
|
||||
// CRATES
|
||||
use crate::esc;
|
||||
use crate::utils::{
|
||||
catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
||||
};
|
||||
@ -11,7 +10,7 @@ use time::{Duration, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "subreddit.html", escape = "none")]
|
||||
#[template(path = "subreddit.html")]
|
||||
struct SubredditTemplate {
|
||||
sub: Subreddit,
|
||||
posts: Vec<Post>,
|
||||
@ -19,6 +18,7 @@ struct SubredditTemplate {
|
||||
ends: (String, String),
|
||||
prefs: Preferences,
|
||||
url: String,
|
||||
redirect_url: String,
|
||||
/// Whether the subreddit itself is filtered.
|
||||
is_filtered: bool,
|
||||
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||
@ -27,7 +27,7 @@ struct SubredditTemplate {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "wiki.html", escape = "none")]
|
||||
#[template(path = "wiki.html")]
|
||||
struct WikiTemplate {
|
||||
sub: String,
|
||||
wiki: String,
|
||||
@ -37,7 +37,7 @@ struct WikiTemplate {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "wall.html", escape = "none")]
|
||||
#[template(path = "wall.html")]
|
||||
struct WallTemplate {
|
||||
title: String,
|
||||
sub: String,
|
||||
@ -86,18 +86,17 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
} else {
|
||||
Subreddit::default()
|
||||
}
|
||||
} else if sub_name.contains('+') {
|
||||
// Multireddit
|
||||
} else {
|
||||
// Multireddit, all, popular
|
||||
Subreddit {
|
||||
name: sub_name.clone(),
|
||||
..Subreddit::default()
|
||||
}
|
||||
} else {
|
||||
Subreddit::default()
|
||||
};
|
||||
|
||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_default());
|
||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26").replace('+', "%2B");
|
||||
let filters = get_filters(&req);
|
||||
|
||||
// If all requested subs are filtered, we don't need to fetch posts.
|
||||
@ -109,6 +108,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||
prefs: Preferences::new(req),
|
||||
url,
|
||||
redirect_url,
|
||||
is_filtered: true,
|
||||
all_posts_filtered: false,
|
||||
})
|
||||
@ -124,6 +124,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||
prefs: Preferences::new(req),
|
||||
url,
|
||||
redirect_url,
|
||||
is_filtered: false,
|
||||
all_posts_filtered,
|
||||
})
|
||||
@ -211,7 +212,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
||||
.unwrap_or_default();
|
||||
|
||||
// Find each subreddit name (separated by '+') in sub parameter
|
||||
for part in sub.split('+') {
|
||||
for part in sub.split('+').filter(|x| x != &"") {
|
||||
// Retrieve display name for the subreddit
|
||||
let display;
|
||||
let part = if part.starts_with("u_") {
|
||||
@ -253,7 +254,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
||||
// Redirect back to subreddit
|
||||
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
||||
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
||||
format!("/{}/", redirect_path)
|
||||
format!("/{}", redirect_path)
|
||||
} else {
|
||||
format!("/r/{}", sub)
|
||||
};
|
||||
@ -334,10 +335,10 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
match json(path, quarantined).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => template(WikiTemplate {
|
||||
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
||||
wiki: rewrite_urls(&val(&response, "description_html")),
|
||||
// wiki: format!(
|
||||
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
||||
// rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
||||
// rewrite_urls(&val(&response, "description_html"),
|
||||
// moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""),
|
||||
// ),
|
||||
sub,
|
||||
@ -406,10 +407,10 @@ async fn subreddit(sub: &str, quarantined: bool) -> Result<Subreddit, String> {
|
||||
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||
|
||||
Ok(Subreddit {
|
||||
name: esc!(&res, "display_name"),
|
||||
title: esc!(&res, "title"),
|
||||
description: esc!(&res, "public_description"),
|
||||
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
|
||||
name: val(&res, "display_name"),
|
||||
title: val(&res, "title"),
|
||||
description: val(&res, "public_description"),
|
||||
info: rewrite_urls(&val(&res, "description_html")),
|
||||
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
|
||||
icon: format_url(&icon),
|
||||
members: format_num(members),
|
||||
|
29
src/user.rs
29
src/user.rs
@ -1,22 +1,24 @@
|
||||
// CRATES
|
||||
use crate::client::json;
|
||||
use crate::esc;
|
||||
use crate::server::RequestExt;
|
||||
use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User};
|
||||
use askama::Template;
|
||||
use hyper::{Body, Request, Response};
|
||||
use time::OffsetDateTime;
|
||||
use time::{macros::format_description, OffsetDateTime};
|
||||
|
||||
// STRUCTS
|
||||
#[derive(Template)]
|
||||
#[template(path = "user.html", escape = "none")]
|
||||
#[template(path = "user.html")]
|
||||
struct UserTemplate {
|
||||
user: User,
|
||||
posts: Vec<Post>,
|
||||
sort: (String, String),
|
||||
ends: (String, String),
|
||||
/// "overview", "comments", or "submitted"
|
||||
listing: String,
|
||||
prefs: Preferences,
|
||||
url: String,
|
||||
redirect_url: String,
|
||||
/// Whether the user themself is filtered.
|
||||
is_filtered: bool,
|
||||
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||
@ -26,13 +28,17 @@ struct UserTemplate {
|
||||
|
||||
// FUNCTIONS
|
||||
pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
let listing = req.param("listing").unwrap_or_else(|| "overview".to_string());
|
||||
|
||||
// Build the Reddit JSON API path
|
||||
let path = format!(
|
||||
"/user/{}.json?{}&raw_json=1",
|
||||
"/user/{}/{}.json?{}&raw_json=1",
|
||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
||||
req.uri().query().unwrap_or_default()
|
||||
listing,
|
||||
req.uri().query().unwrap_or_default(),
|
||||
);
|
||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||
let redirect_url = url[1..].replace('?', "%3F").replace('&', "%26");
|
||||
|
||||
// Retrieve other variables from Libreddit request
|
||||
let sort = param(&path, "sort").unwrap_or_default();
|
||||
@ -46,8 +52,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
posts: Vec::new(),
|
||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||
listing,
|
||||
prefs: Preferences::new(req),
|
||||
url,
|
||||
redirect_url,
|
||||
is_filtered: true,
|
||||
all_posts_filtered: false,
|
||||
})
|
||||
@ -62,8 +70,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||
posts,
|
||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||
listing,
|
||||
prefs: Preferences::new(req),
|
||||
url,
|
||||
redirect_url,
|
||||
is_filtered: false,
|
||||
all_posts_filtered,
|
||||
})
|
||||
@ -82,7 +92,8 @@ async fn user(name: &str) -> Result<User, String> {
|
||||
// Send a request to the url
|
||||
json(path, false).await.map(|res| {
|
||||
// Grab creation date as unix timestamp
|
||||
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
let created_unix = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||
let created = OffsetDateTime::from_unix_timestamp(created_unix).unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
|
||||
// Closure used to parse JSON from Reddit APIs
|
||||
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
||||
@ -90,11 +101,11 @@ async fn user(name: &str) -> Result<User, String> {
|
||||
// Parse the JSON output into a User struct
|
||||
User {
|
||||
name: res["data"]["name"].as_str().unwrap_or(name).to_owned(),
|
||||
title: esc!(about("title")),
|
||||
title: about("title"),
|
||||
icon: format_url(&about("icon_img")),
|
||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
||||
banner: esc!(about("banner_img")),
|
||||
created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(),
|
||||
banner: about("banner_img"),
|
||||
description: about("public_description"),
|
||||
}
|
||||
})
|
||||
|
95
src/utils.rs
95
src/utils.rs
@ -1,15 +1,16 @@
|
||||
//
|
||||
// CRATES
|
||||
//
|
||||
use crate::{client::json, esc, server::RequestExt};
|
||||
use crate::{client::json, server::RequestExt};
|
||||
use askama::Template;
|
||||
use cookie::Cookie;
|
||||
use hyper::{Body, Request, Response};
|
||||
use regex::Regex;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use time::{macros::format_description, Duration, OffsetDateTime};
|
||||
use url::Url;
|
||||
|
||||
// Post flair with content, background color and foreground color
|
||||
@ -41,7 +42,7 @@ impl FlairPart {
|
||||
Self {
|
||||
flair_part_type: value("e").to_string(),
|
||||
value: match value("e") {
|
||||
"text" => esc!(value("t")),
|
||||
"text" => value("t").to_string(),
|
||||
"emoji" => format_url(value("u")),
|
||||
_ => String::new(),
|
||||
},
|
||||
@ -54,7 +55,7 @@ impl FlairPart {
|
||||
"text" => match text_flair {
|
||||
Some(text) => vec![Self {
|
||||
flair_part_type: "text".to_string(),
|
||||
value: esc!(text),
|
||||
value: text.to_string(),
|
||||
}],
|
||||
None => Vec::new(),
|
||||
},
|
||||
@ -217,24 +218,19 @@ pub struct Post {
|
||||
impl Post {
|
||||
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
||||
pub async fn fetch(path: &str, quarantine: bool) -> Result<(Vec<Self>, String), String> {
|
||||
let res;
|
||||
let post_list;
|
||||
|
||||
// Send a request to the url
|
||||
match json(path.to_string(), quarantine).await {
|
||||
let res = match json(path.to_string(), quarantine).await {
|
||||
// If success, receive JSON in response
|
||||
Ok(response) => {
|
||||
res = response;
|
||||
}
|
||||
Ok(response) => response,
|
||||
// If the Reddit API returns an error, exit this function
|
||||
Err(msg) => return Err(msg),
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch the list of posts from the JSON response
|
||||
match res["data"]["children"].as_array() {
|
||||
Some(list) => post_list = list,
|
||||
let post_list = match res["data"]["children"].as_array() {
|
||||
Some(list) => list,
|
||||
None => return Err("No posts found".to_string()),
|
||||
}
|
||||
};
|
||||
|
||||
let mut posts: Vec<Self> = Vec::new();
|
||||
|
||||
@ -245,7 +241,7 @@ impl Post {
|
||||
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
||||
let score = data["score"].as_i64().unwrap_or_default();
|
||||
let ratio: f64 = data["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||
let title = esc!(post, "title");
|
||||
let title = val(post, "title");
|
||||
|
||||
// Determine the type of media along with the media URL
|
||||
let (post_type, media, gallery) = Media::parse(data).await;
|
||||
@ -270,7 +266,7 @@ impl Post {
|
||||
data["author_flair_richtext"].as_array(),
|
||||
data["author_flair_text"].as_str(),
|
||||
),
|
||||
text: esc!(post, "link_flair_text"),
|
||||
text: val(post, "link_flair_text"),
|
||||
background_color: val(post, "author_flair_background_color"),
|
||||
foreground_color: val(post, "author_flair_text_color"),
|
||||
},
|
||||
@ -298,7 +294,7 @@ impl Post {
|
||||
data["link_flair_richtext"].as_array(),
|
||||
data["link_flair_text"].as_str(),
|
||||
),
|
||||
text: esc!(post, "link_flair_text"),
|
||||
text: val(post, "link_flair_text"),
|
||||
background_color: val(post, "link_flair_background_color"),
|
||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||
"black".to_string()
|
||||
@ -324,7 +320,7 @@ impl Post {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "comment.html", escape = "none")]
|
||||
#[template(path = "comment.html")]
|
||||
// Comment with content, post, score and data/time that it was posted
|
||||
pub struct Comment {
|
||||
pub id: String,
|
||||
@ -400,7 +396,7 @@ impl Awards {
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html", escape = "none")]
|
||||
#[template(path = "error.html")]
|
||||
pub struct ErrorTemplate {
|
||||
pub msg: String,
|
||||
pub prefs: Preferences,
|
||||
@ -445,6 +441,7 @@ pub struct Params {
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Preferences {
|
||||
pub available_themes: Vec<String>,
|
||||
pub theme: String,
|
||||
pub front_page: String,
|
||||
pub layout: String,
|
||||
@ -459,10 +456,23 @@ pub struct Preferences {
|
||||
pub filters: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "static/themes/"]
|
||||
#[include = "*.css"]
|
||||
pub struct ThemeAssets;
|
||||
|
||||
impl Preferences {
|
||||
// Build preferences from cookies
|
||||
pub fn new(req: Request<Body>) -> Self {
|
||||
// Read available theme names from embedded css files.
|
||||
// Always make the default "system" theme available.
|
||||
let mut themes = vec!["system".to_string()];
|
||||
for file in ThemeAssets::iter() {
|
||||
let chunks: Vec<&str> = file.as_ref().split(".css").collect();
|
||||
themes.push(chunks[0].to_owned())
|
||||
}
|
||||
Self {
|
||||
available_themes: themes,
|
||||
theme: setting(&req, "theme"),
|
||||
front_page: setting(&req, "front_page"),
|
||||
layout: setting(&req, "layout"),
|
||||
@ -548,7 +558,7 @@ pub fn format_url(url: &str) -> String {
|
||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||
String::new()
|
||||
} else {
|
||||
Url::parse(url).map_or(String::new(), |parsed| {
|
||||
Url::parse(url).map_or(url.to_string(), |parsed| {
|
||||
let domain = parsed.domain().unwrap_or_default();
|
||||
|
||||
let capture = |regex: &str, format: &str, segments: i16| {
|
||||
@ -584,6 +594,9 @@ pub fn format_url(url: &str) -> String {
|
||||
|
||||
match domain {
|
||||
"www.reddit.com" => capture(r"https://www\.reddit\.com/(.*)", "/", 1),
|
||||
"old.reddit.com" => capture(r"https://old\.reddit\.com/(.*)", "/", 1),
|
||||
"np.reddit.com" => capture(r"https://np\.reddit\.com/(.*)", "/", 1),
|
||||
"reddit.com" => capture(r"https://reddit\.com/(.*)", "/", 1),
|
||||
"v.redd.it" => chain!(
|
||||
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$|\?source=fallback))", "/vid/", 2),
|
||||
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
|
||||
@ -604,8 +617,11 @@ pub fn format_url(url: &str) -> String {
|
||||
|
||||
// Rewrite Reddit links to Libreddit in body of text
|
||||
pub fn rewrite_urls(input_text: &str) -> String {
|
||||
let text1 =
|
||||
Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#).map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string());
|
||||
let text1 = Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#)
|
||||
.map_or(String::new(), |re| re.replace_all(input_text, r#"href="/"#).to_string())
|
||||
// Remove (html-encoded) "\" from URLs.
|
||||
.replace("%5C", "")
|
||||
.replace('\\', "");
|
||||
|
||||
// Rewrite external media previews to Libreddit
|
||||
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
|
||||
@ -634,12 +650,12 @@ pub fn format_num(num: i64) -> (String, String) {
|
||||
|
||||
// Parse a relative and absolute time from a UNIX timestamp
|
||||
pub fn time(created: f64) -> (String, String) {
|
||||
let time = OffsetDateTime::from_unix_timestamp(created.round() as i64);
|
||||
let time = OffsetDateTime::from_unix_timestamp(created.round() as i64).unwrap_or(OffsetDateTime::UNIX_EPOCH);
|
||||
let time_delta = OffsetDateTime::now_utc() - time;
|
||||
|
||||
// If the time difference is more than a month, show full date
|
||||
let rel_time = if time_delta > Duration::days(30) {
|
||||
time.format("%b %d '%y")
|
||||
time.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default()
|
||||
// Otherwise, show relative date/time
|
||||
} else if time_delta.whole_days() > 0 {
|
||||
format!("{}d ago", time_delta.whole_days())
|
||||
@ -649,7 +665,12 @@ pub fn time(created: f64) -> (String, String) {
|
||||
format!("{}m ago", time_delta.whole_minutes())
|
||||
};
|
||||
|
||||
(rel_time, time.format("%b %d %Y, %H:%M:%S UTC"))
|
||||
(
|
||||
rel_time,
|
||||
time
|
||||
.format(format_description!("[month repr:short] [day] [year], [hour]:[minute]:[second] UTC"))
|
||||
.unwrap_or_default(),
|
||||
)
|
||||
}
|
||||
|
||||
// val() function used to parse JSON from Reddit APIs
|
||||
@ -657,17 +678,6 @@ pub fn val(j: &Value, k: &str) -> String {
|
||||
j["data"][k].as_str().unwrap_or_default().to_string()
|
||||
}
|
||||
|
||||
// Escape < and > to accurately render HTML
|
||||
#[macro_export]
|
||||
macro_rules! esc {
|
||||
($f:expr) => {
|
||||
$f.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||
};
|
||||
($j:expr, $k:expr) => {
|
||||
$j["data"][$k].as_str().unwrap_or_default().to_string().replace('<', "<").replace('>', ">")
|
||||
};
|
||||
}
|
||||
|
||||
//
|
||||
// NETWORKING
|
||||
//
|
||||
@ -707,6 +717,7 @@ pub async fn error(req: Request<Body>, msg: String) -> Result<Response<Body>, St
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::format_num;
|
||||
use super::rewrite_urls;
|
||||
|
||||
#[test]
|
||||
fn format_num_works() {
|
||||
@ -716,4 +727,14 @@ mod tests {
|
||||
assert_eq!(format_num(1001), ("1.0k".to_string(), "1001".to_string()));
|
||||
assert_eq!(format_num(1_999_999), ("2.0m".to_string(), "1999999".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewrite_urls_removes_backslashes() {
|
||||
let comment_body_html =
|
||||
r#"<a href=\"https://www.reddit.com/r/linux%5C_gaming/comments/x/just%5C_a%5C_test%5C/\">https://www.reddit.com/r/linux\\_gaming/comments/x/just\\_a\\_test/</a>"#;
|
||||
assert_eq!(
|
||||
rewrite_urls(comment_body_html),
|
||||
r#"<a href="https://www.reddit.com/r/linux_gaming/comments/x/just_a_test/">https://www.reddit.com/r/linux_gaming/comments/x/just_a_test/</a>"#
|
||||
)
|
||||
}
|
||||
}
|
||||
|
146
static/style.css
146
static/style.css
@ -45,124 +45,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Light theme setting */
|
||||
.light {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
--background: #ddd;
|
||||
--outside: #ececec;
|
||||
--post: #eee;
|
||||
--panel-border: 1px solid #ccc;
|
||||
--highlighted: white;
|
||||
--visited: #555;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Black theme setting */
|
||||
.black {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
--background: black;
|
||||
--outside: black;
|
||||
--post: black;
|
||||
--panel-border: 2px solid #0f0f0f;
|
||||
--highlighted: #0f0f0f;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Dracula theme setting */
|
||||
.dracula {
|
||||
--accent: #bd93f9;
|
||||
--green: #50fa7b;
|
||||
--text: #f8f8f2;
|
||||
--foreground: #3d4051;
|
||||
--background: #282a36;
|
||||
--outside: #393c4d;
|
||||
--post: #333544;
|
||||
--panel-border: 2px solid #44475a;
|
||||
--highlighted: #4e5267;
|
||||
--visited: #969692;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Nord theme setting */
|
||||
.nord {
|
||||
--accent: #8fbcbb;
|
||||
--green: #a3be8c;
|
||||
--text: #eceff4;
|
||||
--foreground: #3b4252;
|
||||
--background: #2e3440;
|
||||
--outside: #434c5e;
|
||||
--post: #434c5e;
|
||||
--panel-border: 2px solid #4c566a;
|
||||
--highlighted: #3b4252;
|
||||
--visited: #a3a5aa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Laserwave theme setting */
|
||||
.laserwave {
|
||||
--accent: #eb64b9;
|
||||
--green: #74dfc4;
|
||||
--text: #e0dfe1;
|
||||
--foreground: #302a36;
|
||||
--background: #27212e;
|
||||
--outside: #3e3647;
|
||||
--post: #3e3647;
|
||||
--panel-border: 2px solid #2f2738;
|
||||
--highlighted: #302a36;
|
||||
--visited: #91889b;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Violet theme setting */
|
||||
.violet {
|
||||
--accent: #7c71dd;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #1F2347;
|
||||
--background: #12152b;
|
||||
--outside: #181c3a;
|
||||
--post: #181c3a;
|
||||
--panel-border: 1px solid #1F2347;
|
||||
--highlighted: #1F2347;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Gold theme setting */
|
||||
.gold {
|
||||
--accent: #f2aa4c;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #234;
|
||||
--background: #101820;
|
||||
--outside: #1b2936;
|
||||
--post: #1b2936;
|
||||
--panel-border: 0px solid black;
|
||||
--highlighted: #234;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
/* Rosebox theme setting */
|
||||
.rosebox {
|
||||
--accent: #a57562;
|
||||
--green: #a3be8c;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #262626;
|
||||
--outside: #222;
|
||||
--post: #222;
|
||||
--panel-border: 1px solid #222;
|
||||
--highlighted: #262626;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
/* Other themes are located in the "themes" folder */
|
||||
|
||||
/* General */
|
||||
|
||||
@ -177,6 +60,7 @@
|
||||
|
||||
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
|
||||
pre, form, fieldset, table, th, td, select, input {
|
||||
accent-color: var(--accent);
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
font-family: "Inter", sans-serif;
|
||||
@ -487,7 +371,7 @@ aside {
|
||||
|
||||
/* Sorting and Search */
|
||||
|
||||
select, #search, #sort_options, #inside, #searchbox > *, #sort_submit {
|
||||
select, #search, #sort_options, #listing_options, #inside, #searchbox > *, #sort_submit {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
@ -563,6 +447,11 @@ button.submit:hover > svg { stroke: var(--accent); }
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#listing_options + #sort_select {
|
||||
margin-left: 10px;
|
||||
border-radius: 5px 0px 0px 5px;
|
||||
}
|
||||
|
||||
#search_sort {
|
||||
background: var(--highlighted);
|
||||
border-radius: 5px;
|
||||
@ -591,16 +480,16 @@ button.submit:hover > svg { stroke: var(--accent); }
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
#sort_options, footer > a {
|
||||
#sort_options, #listing_options, footer > a {
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
box-shadow: var(--shadow);
|
||||
background: var(--outside);
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#sort_options > a, footer > a {
|
||||
#sort_options > a, #listing_options > a, footer > a {
|
||||
color: var(--text);
|
||||
padding: 10px 20px;
|
||||
text-align: center;
|
||||
@ -608,12 +497,12 @@ button.submit:hover > svg { stroke: var(--accent); }
|
||||
transition: 0.2s background;
|
||||
}
|
||||
|
||||
#sort_options > a.selected {
|
||||
#sort_options > a.selected, #listing_options > a.selected {
|
||||
background: var(--accent);
|
||||
color: var(--foreground);
|
||||
}
|
||||
|
||||
#sort_options > a:not(.selected):hover {
|
||||
#sort_options > a:not(.selected):hover, #listing_options > a:not(.selected):hover {
|
||||
background: var(--foreground);
|
||||
}
|
||||
|
||||
@ -749,6 +638,7 @@ a.search_subreddit:hover {
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
margin: 5px 15px 5px 12px;
|
||||
grid-area: post_title;
|
||||
}
|
||||
@ -876,8 +766,9 @@ a.search_subreddit:hover {
|
||||
|
||||
#post_url {
|
||||
color: var(--accent);
|
||||
margin: 5px 15px;
|
||||
margin: 5px 12px;
|
||||
grid-area: post_media;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.post_body {
|
||||
@ -886,6 +777,7 @@ a.search_subreddit:hover {
|
||||
padding: 5px 15px 5px 12px;
|
||||
grid-area: post_body;
|
||||
width: calc(100% - 30px);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
/* Used only for text post preview */
|
||||
@ -903,7 +795,7 @@ a.search_subreddit:hover {
|
||||
opacity: 0.5;
|
||||
font-size: 14px;
|
||||
grid-area: post_footer;
|
||||
margin: 5px 20px 15px 15px;
|
||||
margin: 5px 20px 15px 12px;
|
||||
}
|
||||
|
||||
.post_comments {
|
||||
@ -1311,6 +1203,8 @@ input[type="submit"] {
|
||||
.md table {
|
||||
margin: 5px;
|
||||
overflow-x: auto;
|
||||
display: block;
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
.md code {
|
||||
|
14
static/themes/black.css
Normal file
14
static/themes/black.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Black theme setting */
|
||||
.black {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: white;
|
||||
--foreground: #0f0f0f;
|
||||
--background: black;
|
||||
--outside: black;
|
||||
--post: black;
|
||||
--panel-border: 2px solid #0f0f0f;
|
||||
--highlighted: #0f0f0f;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/dark.css
Normal file
14
static/themes/dark.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Dark theme setting */
|
||||
.dark{
|
||||
--accent: aqua;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #0f0f0f;
|
||||
--outside: #1f1f1f;
|
||||
--post: #161616;
|
||||
--panel-border: 1px solid #333;
|
||||
--highlighted: #333;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
14
static/themes/dracula.css
Normal file
14
static/themes/dracula.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Dracula theme setting */
|
||||
.dracula {
|
||||
--accent: #bd93f9;
|
||||
--green: #50fa7b;
|
||||
--text: #f8f8f2;
|
||||
--foreground: #3d4051;
|
||||
--background: #282a36;
|
||||
--outside: #393c4d;
|
||||
--post: #333544;
|
||||
--panel-border: 2px solid #44475a;
|
||||
--highlighted: #4e5267;
|
||||
--visited: #969692;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/gold.css
Normal file
14
static/themes/gold.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Gold theme setting */
|
||||
.gold {
|
||||
--accent: #f2aa4c;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #234;
|
||||
--background: #101820;
|
||||
--outside: #1b2936;
|
||||
--post: #1b2936;
|
||||
--panel-border: 0px solid black;
|
||||
--highlighted: #234;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
14
static/themes/laserwave.css
Normal file
14
static/themes/laserwave.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Laserwave theme setting */
|
||||
.laserwave {
|
||||
--accent: #eb64b9;
|
||||
--green: #74dfc4;
|
||||
--text: #e0dfe1;
|
||||
--foreground: #302a36;
|
||||
--background: #27212e;
|
||||
--outside: #3e3647;
|
||||
--post: #3e3647;
|
||||
--panel-border: 2px solid #2f2738;
|
||||
--highlighted: #302a36;
|
||||
--visited: #91889b;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/light.css
Normal file
14
static/themes/light.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Light theme setting */
|
||||
.light {
|
||||
--accent: #009a9a;
|
||||
--green: #00a229;
|
||||
--text: black;
|
||||
--foreground: #f5f5f5;
|
||||
--background: #ddd;
|
||||
--outside: #ececec;
|
||||
--post: #eee;
|
||||
--panel-border: 1px solid #ccc;
|
||||
--highlighted: white;
|
||||
--visited: #555;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
14
static/themes/nord.css
Normal file
14
static/themes/nord.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Nord theme setting */
|
||||
.nord {
|
||||
--accent: #8fbcbb;
|
||||
--green: #a3be8c;
|
||||
--text: #eceff4;
|
||||
--foreground: #3b4252;
|
||||
--background: #2e3440;
|
||||
--outside: #434c5e;
|
||||
--post: #434c5e;
|
||||
--panel-border: 2px solid #4c566a;
|
||||
--highlighted: #3b4252;
|
||||
--visited: #a3a5aa;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
13
static/themes/rosebox.css
Normal file
13
static/themes/rosebox.css
Normal file
@ -0,0 +1,13 @@
|
||||
/* Rosebox theme setting */
|
||||
.rosebox {
|
||||
--accent: #a57562;
|
||||
--green: #a3be8c;
|
||||
--text: white;
|
||||
--foreground: #222;
|
||||
--background: #262626;
|
||||
--outside: #222;
|
||||
--post: #222;
|
||||
--panel-border: 1px solid #222;
|
||||
--highlighted: #262626;
|
||||
--shadow: 0 1px 3px rgba(0, 0, 0, 0.5);
|
||||
}
|
14
static/themes/violet.css
Normal file
14
static/themes/violet.css
Normal file
@ -0,0 +1,14 @@
|
||||
/* Violet theme setting */
|
||||
.violet {
|
||||
--accent: #7c71dd;
|
||||
--green: #5cff85;
|
||||
--text: white;
|
||||
--foreground: #1F2347;
|
||||
--background: #12152b;
|
||||
--outside: #181c3a;
|
||||
--post: #181c3a;
|
||||
--panel-border: 1px solid #1F2347;
|
||||
--highlighted: #1F2347;
|
||||
--visited: #aaa;
|
||||
--shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
|
||||
}
|
@ -13,7 +13,7 @@
|
||||
{% if author.name != "[deleted]" %}
|
||||
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a>
|
||||
{% else %}
|
||||
<span class="comment_author">u/[deleted]</span>
|
||||
<span class="comment_author {{ author.distinguished }}">u/[deleted]</span>
|
||||
{% endif %}
|
||||
{% if author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
||||
@ -32,9 +32,9 @@
|
||||
{% if is_filtered %}
|
||||
<div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div>
|
||||
{% else %}
|
||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body }}</div>
|
||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
||||
{% endif %}
|
||||
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap() }}{%- endfor %}
|
||||
<blockquote class="replies">{% for c in replies -%}{{ c.render().unwrap()|safe }}{%- endfor %}
|
||||
</blockquote>
|
||||
</details>
|
||||
</div>
|
||||
|
@ -37,7 +37,7 @@
|
||||
<p class="post_header">
|
||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/user/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
<a class="post_author {{ post.author.distinguished }}" href="/user/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
{% if post.author.flair.flair_parts.len() > 0 %}
|
||||
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
||||
{% endif %}
|
||||
@ -55,15 +55,15 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="post_title">
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||
<h1 class="post_title">
|
||||
{{ post.title }}
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||
class="post_flair"
|
||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call utils::render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
</p>
|
||||
</h1>
|
||||
|
||||
<!-- POST MEDIA -->
|
||||
<!-- post_type: {{ post.post_type }} -->
|
||||
@ -110,7 +110,7 @@
|
||||
{% endif %}
|
||||
|
||||
<!-- POST BODY -->
|
||||
<div class="post_body">{{ post.body }}</div>
|
||||
<div class="post_body">{{ post.body|safe }}</div>
|
||||
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
|
||||
<div class="post_footer">
|
||||
<ul id="post_links">
|
||||
@ -144,7 +144,7 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{{ c.render().unwrap() }}
|
||||
{{ c.render().unwrap()|safe }}
|
||||
</div>
|
||||
{%- endfor %}
|
||||
|
||||
|
@ -39,7 +39,7 @@
|
||||
{% endif %}
|
||||
{% for subreddit in subreddits %}
|
||||
<a href="{{ subreddit.url }}" class="search_subreddit">
|
||||
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
|
||||
<div class="search_subreddit_left">{% if subreddit.icon != "" %}<img loading="lazy" src="{{ subreddit.icon|safe }}" alt="r/{{ subreddit.name }} icon">{% endif %}</div>
|
||||
<div class="search_subreddit_right">
|
||||
<p class="search_subreddit_header">
|
||||
<span class="search_subreddit_name">r/{{ subreddit.name }}</span>
|
||||
@ -92,13 +92,13 @@
|
||||
{% if params.before != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&before={{ params.before }}">PREV</a>
|
||||
&before={{ params.before }}" accesskey="P">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if params.after != "" %}
|
||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||
&sort={{ params.sort }}&t={{ params.t }}
|
||||
&after={{ params.after }}">NEXT</a>
|
||||
&after={{ params.after }}" accesskey="N">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
{% endif %}
|
||||
|
@ -15,7 +15,7 @@
|
||||
<div id="theme">
|
||||
<label for="theme">Theme:</label>
|
||||
<select name="theme">
|
||||
{% call utils::options(prefs.theme, ["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox"], "system") %}
|
||||
{% call utils::options(prefs.theme, prefs.available_themes, "system") %}
|
||||
</select>
|
||||
</div>
|
||||
<legend>Interface</legend>
|
||||
@ -110,7 +110,7 @@
|
||||
|
||||
<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 }}&front_page={{ prefs.front_page }}&layout={{ prefs.layout }}&wide={{ prefs.wide }}&comment_sort={{ prefs.comment_sort }}&show_nsfw={{ prefs.show_nsfw }}&use_hls={{ prefs.use_hls }}&hide_hls_notification={{ prefs.hide_hls_notification }}&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 }}&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 }}&hide_hls_notification={{ prefs.hide_hls_notification }}&subscriptions={{ prefs.subscriptions.join("%2B") }}&filters={{ prefs.filters.join("%2B") }}">this link</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -41,7 +41,7 @@
|
||||
</form>
|
||||
|
||||
{% if sub.name.contains("+") %}
|
||||
<form action="/r/{{ sub.name }}/subscribe" method="POST">
|
||||
<form action="/r/{{ sub.name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button id="multisub" class="subscribe" title="Subscribe to each sub in this multireddit">Subscribe to Multireddit</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@ -65,21 +65,21 @@
|
||||
|
||||
<footer>
|
||||
{% if !ends.0.is_empty() %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if !ends.1.is_empty() %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if is_filtered || (!sub.name.is_empty() && !sub.name.contains("+")) %}
|
||||
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %}
|
||||
<aside>
|
||||
{% if is_filtered %}
|
||||
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||
{% endif %}
|
||||
{% if !sub.name.is_empty() && !sub.name.contains("+") %}
|
||||
{% if !sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+") %}
|
||||
<div class="panel" id="subreddit">
|
||||
{% if sub.wiki %}
|
||||
<div id="top">
|
||||
@ -89,7 +89,7 @@
|
||||
{% endif %}
|
||||
<div id="sub_meta">
|
||||
<img loading="lazy" id="sub_icon" src="{{ sub.icon }}" alt="Icon for r/{{ sub.name }}">
|
||||
<p id="sub_title">{{ sub.title }}</p>
|
||||
<h1 id="sub_title">{{ sub.title }}</h1>
|
||||
<p id="sub_name">r/{{ sub.name }}</p>
|
||||
<p id="sub_description">{{ sub.description }}</p>
|
||||
<div id="sub_details">
|
||||
@ -101,22 +101,22 @@
|
||||
<div id="sub_actions">
|
||||
<div id="sub_subscription">
|
||||
{% if prefs.subscriptions.contains(sub.name) %}
|
||||
<form action="/r/{{ sub.name }}/unsubscribe" method="POST">
|
||||
<form action="/r/{{ sub.name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unsubscribe">Unsubscribe</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ sub.name }}/subscribe" method="POST">
|
||||
<form action="/r/{{ sub.name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="subscribe">Subscribe</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="sub_filter">
|
||||
{% if prefs.filters.contains(sub.name) %}
|
||||
<form action="/r/{{ sub.name }}/unfilter" method="POST">
|
||||
<form action="/r/{{ sub.name }}/unfilter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unfilter">Unfilter</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ sub.name }}/filter" method="POST">
|
||||
<form action="/r/{{ sub.name }}/filter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="filter">Filter</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
@ -127,7 +127,7 @@
|
||||
<details class="panel" id="sidebar">
|
||||
<summary id="sidebar_label">Sidebar</summary>
|
||||
<div id="sidebar_contents">
|
||||
{{ sub.info }}
|
||||
{{ sub.info|safe }}
|
||||
{# <hr>
|
||||
<h2>Moderators</h2>
|
||||
<br>
|
||||
|
@ -16,9 +16,12 @@
|
||||
{% if !is_filtered %}
|
||||
<div id="column_one">
|
||||
<form id="sort">
|
||||
<select name="sort">
|
||||
{% call utils::options(sort.0, ["hot", "new", "top"], "") %}
|
||||
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
|
||||
<div id="listing_options">
|
||||
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
|
||||
</div>
|
||||
<select id="sort_select" name="sort">
|
||||
{% call utils::options(sort.0, ["hot", "new", "top", "controversial"], "") %}
|
||||
</select>{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
||||
@ -49,7 +52,7 @@
|
||||
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
</summary>
|
||||
<p class="comment_body">{{ post.body }}</p>
|
||||
<p class="comment_body">{{ post.body|safe }}</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -63,11 +66,11 @@
|
||||
|
||||
<footer>
|
||||
{% if ends.0 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}">PREV</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&before={{ ends.0 }}" accesskey="P">PREV</a>
|
||||
{% endif %}
|
||||
|
||||
{% if ends.1 != "" %}
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}">NEXT</a>
|
||||
<a href="?sort={{ sort.0 }}&t={{ sort.1 }}&after={{ ends.1 }}" accesskey="N">NEXT</a>
|
||||
{% endif %}
|
||||
</footer>
|
||||
</div>
|
||||
@ -78,7 +81,7 @@
|
||||
{% endif %}
|
||||
<div class="panel" id="user">
|
||||
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
||||
<p id="user_title">{{ user.title }}</p>
|
||||
<h1 id="user_title">{{ user.title }}</h1>
|
||||
<p id="user_name">u/{{ user.name }}</p>
|
||||
<div id="user_description">{{ user.description }}</div>
|
||||
<div id="user_details">
|
||||
@ -91,22 +94,22 @@
|
||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||
<div id="user_subscription">
|
||||
{% if prefs.subscriptions.contains(name) %}
|
||||
<form action="/r/{{ name }}/unsubscribe" method="POST">
|
||||
<form action="/r/{{ name }}/unsubscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unsubscribe">Unfollow</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ name }}/subscribe" method="POST">
|
||||
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="subscribe">Follow</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="user_filter">
|
||||
{% if prefs.filters.contains(name) %}
|
||||
<form action="/r/{{ name }}/unfilter" method="POST">
|
||||
<form action="/r/{{ name }}/unfilter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="unfilter">Unfilter</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form action="/r/{{ name }}/filter" method="POST">
|
||||
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
|
||||
<button class="filter">Filter</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<input id="search" type="text" name="q" placeholder="Search" title="Search libreddit" value="{{ search }}">
|
||||
{% if root != "/r/" && !root.is_empty() %}
|
||||
<div id="inside">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr">
|
||||
<input type="checkbox" name="restrict_sr" id="restrict_sr" checked>
|
||||
<label for="restrict_sr" class="search_label" title="Restrict search to this subreddit">in {{ root }}</label>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -38,7 +38,6 @@
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro sub_list(current) -%}
|
||||
{% if prefs.subscriptions.len() > 0 %}
|
||||
<details id="feeds">
|
||||
<summary>Feeds</summary>
|
||||
<div id="feed_list">
|
||||
@ -46,13 +45,14 @@
|
||||
<a href="/">Home</a>
|
||||
<a href="/r/popular">Popular</a>
|
||||
<a href="/r/all">All</a>
|
||||
<p>REDDIT FEEDS</p>
|
||||
{% for sub in prefs.subscriptions %}
|
||||
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a>
|
||||
{% endfor %}
|
||||
{% 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>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
{% endif %}
|
||||
{%- endmacro %}
|
||||
|
||||
{% macro render_hls_notification(redirect_url) -%}
|
||||
@ -72,7 +72,7 @@
|
||||
{% endif -%}
|
||||
<a class="post_subreddit" href="/{{ community }}">{{ community }}</a>
|
||||
<span class="dot">•</span>
|
||||
<a class="post_author" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
<a class="post_author {{ post.author.distinguished }}" href="/u/{{ post.author.name }}">u/{{ post.author.name }}</a>
|
||||
<span class="dot">•</span>
|
||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||
{% if !post.awards.is_empty() %}
|
||||
@ -83,7 +83,7 @@
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</p>
|
||||
<p class="post_title">
|
||||
<h2 class="post_title">
|
||||
{% if post.flair.flair_parts.len() > 0 %}
|
||||
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||
class="post_flair"
|
||||
@ -91,7 +91,7 @@
|
||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||
{% endif %}
|
||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||
</p>
|
||||
</h2>
|
||||
<!-- POST MEDIA/THUMBNAIL -->
|
||||
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
|
||||
<a href="{{ post.media.url }}" class="post_media_image {% if post.media.height / post.media.width < 2 %}short{% endif %}" >
|
||||
@ -138,10 +138,10 @@
|
||||
|
||||
<div class="post_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
|
||||
<div class="post_body post_preview">
|
||||
{{ post.body }}
|
||||
{{ post.body|safe }}
|
||||
</div>
|
||||
<div class="post_footer">
|
||||
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} comments">{{ post.comments.0 }} comments</a>
|
||||
<a href="{{ post.permalink }}" class="post_comments" title="{{ post.comments.1 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}">{{ post.comments.0 }} {% if post.comments.1 == "1" %}comment{% else %}comments{% endif %}</a>
|
||||
</div>
|
||||
</div>
|
||||
{%- endmacro %}
|
||||
|
@ -22,8 +22,8 @@
|
||||
<div>Wiki</div>
|
||||
</div>
|
||||
<div id="wiki">
|
||||
{{ wiki }}
|
||||
{{ wiki|safe }}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
Reference in New Issue
Block a user