Compare commits
102 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 | |||
db3196df5a | |||
b3d4f6f91c | |||
45b875b85d | |||
992d7889c4 | |||
3188f9d8e7 | |||
90fa0b5496 | |||
7aeabfc4bc | |||
150ebe38f3 | |||
2905d114fa | |||
40e97cc75d | |||
7c73e352ce | |||
341c623be8 | |||
4c8b724a9d | |||
227d74b187 | |||
f05a818edd | |||
ceee13cfb7 | |||
a39495b3cb | |||
38cfe4ad71 | |||
0b89539c2b | |||
046b8b3edc | |||
0656756d21 | |||
43551f70fd | |||
364c29c4d5 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@ -1 +1,2 @@
|
|||||||
liberapay: spike
|
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
|
name: 🐛 Bug report
|
||||||
about: Create a report to help us improve
|
about: Create a report to help us improve
|
||||||
title: ''
|
title: '🐛 Bug Report: '
|
||||||
labels: bug
|
labels: bug
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ assignees: ''
|
|||||||
A clear and concise description of what the bug is.
|
A clear and concise description of what the bug is.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## To reproduce
|
## Steps to reproduce the bug
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
@ -22,12 +22,12 @@ Steps to reproduce the behavior:
|
|||||||
4. See error
|
4. See error
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Expected behavior
|
## What's the expected behavior?
|
||||||
<!--
|
<!--
|
||||||
A clear and concise description of what you expected to happen.
|
A clear and concise description of what you expected to happen.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
## Additional context
|
## Additional context / screenshot
|
||||||
<!--
|
<!--
|
||||||
Add any other context about the problem here.
|
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
|
name: ✨ Feature parity
|
||||||
about: Suggest implementing a feature into Libreddit that is found in Reddit.com
|
about: Suggest implementing a feature into Libreddit that is found in Reddit.com
|
||||||
title: ''
|
title: '✨ Feature parity: '
|
||||||
labels: feature parity
|
labels: feature parity
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ assignees: ''
|
|||||||
A clear and concise description of what the feature is.
|
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.
|
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.
|
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.
|
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
|
name: 💡 Feature request
|
||||||
about: Suggest a feature for Libreddit that is not found in Reddit
|
about: Suggest a feature for Libreddit that is not found in Reddit
|
||||||
title: ''
|
title: '💡 Feature request: '
|
||||||
labels: enhancement
|
labels: enhancement
|
||||||
assignees: ''
|
assignees: ''
|
||||||
|
|
||||||
@ -12,7 +12,7 @@ assignees: ''
|
|||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
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.
|
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.
|
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.
|
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
|
platforms: linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: spikecodes/libreddit:arm
|
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
|
platforms: linux/arm/v7
|
||||||
push: true
|
push: true
|
||||||
tags: spikecodes/libreddit:armv7
|
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
|
platforms: linux/amd64
|
||||||
push: true
|
push: true
|
||||||
tags: spikecodes/libreddit:latest
|
tags: spikecodes/libreddit:latest
|
||||||
|
cache-from: type=gha
|
||||||
|
cache-to: type=gha,mode=max
|
||||||
|
9
.github/workflows/rust.yml
vendored
9
.github/workflows/rust.yml
vendored
@ -22,6 +22,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: cargo build --release
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Publish to crates.io
|
||||||
|
continue-on-error: true
|
||||||
|
run: cargo publish --no-verify --token ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v2.2.1
|
- uses: actions/upload-artifact@v2.2.1
|
||||||
name: Upload a Build Artifact
|
name: Upload a Build Artifact
|
||||||
@ -43,12 +47,13 @@ jobs:
|
|||||||
if: github.base_ref != 'master'
|
if: github.base_ref != 'master'
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.version.outputs.version }}
|
tag_name: ${{ steps.version.outputs.version }}
|
||||||
name: ${{ steps.version.outputs.version }} - NAME
|
name: ${{ steps.version.outputs.version }} - ${{ github.event.head_commit.message }}
|
||||||
draft: true
|
draft: true
|
||||||
files: |
|
files: |
|
||||||
target/release/libreddit
|
target/release/libreddit
|
||||||
libreddit.sha512
|
libreddit.sha512
|
||||||
body: |
|
body: |
|
||||||
- ${{ github.event.head_commit.message }} ${{ github.sha }}
|
- ${{ github.event.head_commit.message }} ${{ github.sha }}
|
||||||
|
generate_release_notes: true
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
|
||||||
|
1013
Cargo.lock
generated
1013
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@ -3,23 +3,25 @@ name = "libreddit"
|
|||||||
description = " Alternative private front-end to Reddit"
|
description = " Alternative private front-end to Reddit"
|
||||||
license = "AGPL-3.0"
|
license = "AGPL-3.0"
|
||||||
repository = "https://github.com/spikecodes/libreddit"
|
repository = "https://github.com/spikecodes/libreddit"
|
||||||
version = "0.20.0"
|
version = "0.23.0"
|
||||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
askama = { version = "0.10.5", default-features = false }
|
askama = { version = "0.11.1", default-features = false }
|
||||||
async-recursion = "0.3.2"
|
async-recursion = "1.0.0"
|
||||||
cached = "0.26.2"
|
cached = "0.40.0"
|
||||||
clap = { version = "2.33.3", default-features = false }
|
clap = { version = "4.0.18", default-features = false, features = ["std"] }
|
||||||
regex = "1.5.4"
|
regex = "1.6.0"
|
||||||
serde = { version = "1.0.130", features = ["derive"] }
|
serde = { version = "1.0.147", features = ["derive"] }
|
||||||
cookie = "0.15.1"
|
cookie = "0.16.1"
|
||||||
futures-lite = "1.12.0"
|
futures-lite = "1.12.0"
|
||||||
hyper = { version = "0.14.15", features = ["full"] }
|
hyper = { version = "0.14.22", features = ["full"] }
|
||||||
hyper-rustls = "0.23.0"
|
hyper-rustls = "0.23.0"
|
||||||
|
percent-encoding = "2.2.0"
|
||||||
route-recognizer = "0.3.1"
|
route-recognizer = "0.3.1"
|
||||||
serde_json = "1.0.72"
|
serde_json = "1.0.87"
|
||||||
tokio = { version = "1.14.0", features = ["full"] }
|
tokio = { version = "1.21.2", features = ["full"] }
|
||||||
time = "0.2.7"
|
time = "0.3.16"
|
||||||
url = "2.2.2"
|
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: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
|
108
README.md
108
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).
|
**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
|
- ☁️ Light: no JavaScript, no ads, no tracking, no bloat
|
||||||
- 🕵 Private: all requests are proxied through the server, including media
|
- 🕵 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
|
- 🔒 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.
|
I appreciate any donations! Your support allows me to continue developing Libreddit.
|
||||||
|
|
||||||
**Liberapay:** <a href="https://liberapay.com/spike/donate"><img alt="Donate using Liberapay" src="https://liberapay.com/assets/widgets/donate.svg"></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,39 +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!
|
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 |
|
| Website | Country | Cloudflare |
|
||||||
|-|-|-|
|
|-|-|-|
|
||||||
| [libredd.it](https://libredd.it) (official) | 🇺🇸 US | |
|
| [libredd.it](https://libredd.it) (official) | 🇺🇸 US | |
|
||||||
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
|
| [libreddit.spike.codes](https://libreddit.spike.codes) (official) | 🇺🇸 US | |
|
||||||
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇩🇪 DE | ✅ |
|
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇩🇪 DE | ✅ |
|
||||||
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | |
|
| [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.invak.id](https://reddit.invak.id) | 🇧🇬 BG | |
|
||||||
| [reddit.phii.me](https://reddit.phii.me) | 🇺🇸 US | |
|
|
||||||
| [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
|
| [lr.riverside.rocks](https://lr.riverside.rocks) | 🇺🇸 US | |
|
||||||
| [libreddit.silkky.cloud](https://libreddit.silkky.cloud) | 🇫🇮 FI | ✅ |
|
| [libreddit.strongthany.cc](https://libreddit.strongthany.cc) | 🇺🇸 US | |
|
||||||
| [libreddit.database.red](https://libreddit.database.red) | 🇺🇸 US | ✅ |
|
| [libreddit.privacy.com.de](https://libreddit.privacy.com.de) | 🇩🇪 DE | |
|
||||||
| [libreddit.exonip.de](https://libreddit.exonip.de) | 🇩🇪 DE | |
|
|
||||||
| [libreddit.domain.glass](https://libreddit.domain.glass) | 🇺🇸 US | ✅ |
|
| [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 | ✅ |
|
| [r.nf](https://r.nf) | 🇩🇪 DE | ✅ |
|
||||||
| [libreddit.awesomehub.io](https://libreddit.awesomehub.io) | 🇫🇮 FI | |
|
|
||||||
| [libreddit.some-things.org](https://libreddit.some-things.org) | 🇨🇭 CH | |
|
| [libreddit.some-things.org](https://libreddit.some-things.org) | 🇨🇭 CH | |
|
||||||
| [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | |
|
| [reddit.stuehieyr.com](https://reddit.stuehieyr.com) | 🇩🇪 DE | |
|
||||||
| [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | |
|
| [lr.mint.lgbt](https://lr.mint.lgbt) | 🇨🇦 CA | |
|
||||||
| [libreddit.alefvanoon.xyz](https://libreddit.alefvanoon.xyz) | 🇺🇸 US | ✅ |
|
| [libreddit.intent.cool](https://libreddit.intent.cool) | 🇺🇸 US | |
|
||||||
| [libreddit.igna.rocks](https://libreddit.igna.rocks) | 🇺🇸 US | |
|
| [libreddit.drivet.xyz](https://libreddit.drivet.xyz) | 🇵🇱 PL | |
|
||||||
| [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.de](https://libreddit.de) | 🇩🇪 DE | |
|
| [libreddit.de](https://libreddit.de) | 🇩🇪 DE | |
|
||||||
| [libreddit.pussthecat.org](https://libreddit.pussthecat.org) | 🇩🇪 DE | |
|
| [libreddit.pussthecat.org](https://libreddit.pussthecat.org) | 🇩🇪 DE | |
|
||||||
| [libreddit.mutahar.rocks](https://libreddit.mutahar.rocks) | 🇫🇷 FR | |
|
| [libreddit.mutahar.rocks](https://libreddit.mutahar.rocks) | 🇫🇷 FR | |
|
||||||
| [libreddit.northboot.xyz](https://libreddit.northboot.xyz) | 🇩🇪 DE | |
|
| [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) | 🇨🇦 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 | |
|
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||||
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
|
| [fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion](http://fwhhsbrbltmrct5hshrnqlqygqvcgmnek3cnka55zj4y7nuus5muwyyd.onion) | 🇩🇪 DE | |
|
||||||
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
|
| [kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion](http://kphht2jcflojtqte4b4kyx7p2ahagv4debjj32nre67dxz7y57seqwyd.onion) | 🇳🇱 NL | |
|
||||||
@ -69,9 +94,17 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
|
|||||||
| [liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion](http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion) | 🇩🇪 DE | |
|
| [liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion](http://liredejj74h5xjqr2dylnl5howb2bpikfowqoveub55ru27x43357iid.onion) | 🇩🇪 DE | |
|
||||||
| [kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion](http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion) | 🇺🇸 US | |
|
| [kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion](http://kzhfp3nvb4qp575vy23ccbrgfocezjtl5dx66uthgrhu7nscu6rcwjyd.onion) | 🇺🇸 US | |
|
||||||
| [ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion](http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion) | 🇩🇪 DE | |
|
| [ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion](http://ecue64ybzvn6vjzl37kcsnwt4ycmbsyf74nbttyg7rkc3t3qwnj7mcyd.onion) | 🇩🇪 DE | |
|
||||||
|
| [ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion](http://ledditqo2mxfvlgobxnlhrkq4dh34jss6evfkdkb2thlvy6dn4f4gpyd.onion) | 🇺🇸 US | |
|
||||||
|
| [libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion](http://libredoxhxwnmsb6dvzzd35hmgzmawsq5i764es7witwhddvpc2razid.onion) | 🇺🇸 US | |
|
||||||
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.
|
| [libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion](http://libreddit.2syis2nnyytz6jnusnjurva4swlaizlnleiks5mjp46phuwjbdjqwgqd.onion) | 🇪🇬 EG | |
|
||||||
|
| [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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -134,7 +167,7 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot
|
|||||||
- The requested URL
|
- The requested URL
|
||||||
- Search terms
|
- 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)
|
- GPS (consensual)
|
||||||
- Bluetooth (consensual)
|
- Bluetooth (consensual)
|
||||||
- Content associated with a location (consensual)
|
- Content associated with a location (consensual)
|
||||||
@ -158,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.
|
**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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -224,17 +257,18 @@ libreddit
|
|||||||
|
|
||||||
Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
Assign a default value for each setting by passing environment variables to Libreddit in the format `LIBREDDIT_DEFAULT_{X}`. Replace `{X}` with the setting name (see list below) in capital letters.
|
||||||
|
|
||||||
| Name | Possible values | Default value |
|
| Name | Possible values | Default value |
|
||||||
|-------------------------|------------------------------------------------------------------------------------------|---------------|
|
|-------------------------|-----------------------------------------------------------------------------------------------------|---------------|
|
||||||
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold"]` | `system` |
|
| `THEME` | `["system", "light", "dark", "black", "dracula", "nord", "laserwave", "violet", "gold", "rosebox"]` | `system` |
|
||||||
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
| `FRONT_PAGE` | `["default", "popular", "all"]` | `default` |
|
||||||
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
| `LAYOUT` | `["card", "clean", "compact"]` | `card` |
|
||||||
| `WIDE` | `["on", "off"]` | `off` |
|
| `WIDE` | `["on", "off"]` | `off` |
|
||||||
| `COMMENT_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
| `POST_SORT` | `["hot", "new", "top", "rising", "controversial"]` | `hot` |
|
||||||
| `POST_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
| `COMMENT_SORT` | `["confidence", "top", "new", "controversial", "old"]` | `confidence` |
|
||||||
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
| `SHOW_NSFW` | `["on", "off"]` | `off` |
|
||||||
| `USE_HLS` | `["on", "off"]` | `off` |
|
| `USE_HLS` | `["on", "off"]` | `off` |
|
||||||
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
| `HIDE_HLS_NOTIFICATION` | `["on", "off"]` | `off` |
|
||||||
|
| `AUTOPLAY_VIDEOS` | `["on", "off"]` | `off` |
|
||||||
|
|
||||||
### Examples
|
### Examples
|
||||||
|
|
||||||
@ -248,7 +282,7 @@ LIBREDDIT_DEFAULT_WIDE=on LIBREDDIT_DEFAULT_THEME=dark libreddit -r
|
|||||||
|
|
||||||
## Proxying using NGINX
|
## 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
|
```nginx
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
```
|
```
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
ADDRESS=localhost
|
ADDRESS=0.0.0.0
|
||||||
PORT=12345
|
PORT=12345
|
||||||
|
@ -11,5 +11,27 @@ Environment=PORT=8080
|
|||||||
EnvironmentFile=-/etc/libreddit.conf
|
EnvironmentFile=-/etc/libreddit.conf
|
||||||
ExecStart=/usr/bin/libreddit -a ${ADDRESS} -p ${PORT}
|
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]
|
[Install]
|
||||||
WantedBy=default.target
|
WantedBy=default.target
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
use cached::proc_macro::cached;
|
use cached::proc_macro::cached;
|
||||||
use futures_lite::{future::Boxed, FutureExt};
|
use futures_lite::{future::Boxed, FutureExt};
|
||||||
use hyper::{body::Buf, client, Body, Request, Response, Uri};
|
use hyper::{body::Buf, client, Body, Request, Response, Uri};
|
||||||
|
use percent_encoding::{percent_encode, CONTROLS};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::{result::Result, str::FromStr};
|
use std::result::Result;
|
||||||
|
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
|
|
||||||
@ -20,7 +21,7 @@ pub async fn proxy(req: Request<Body>, format: &str) -> Result<Response<Body>, S
|
|||||||
|
|
||||||
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
|
async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String> {
|
||||||
// First parameter is target URL (mandatory).
|
// First parameter is target URL (mandatory).
|
||||||
let url = Uri::from_str(url).map_err(|_| "Couldn't parse URL".to_string())?;
|
let uri = url.parse::<Uri>().map_err(|_| "Couldn't parse URL".to_string())?;
|
||||||
|
|
||||||
// Prepare the HTTPS connector.
|
// Prepare the HTTPS connector.
|
||||||
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
let https = hyper_rustls::HttpsConnectorBuilder::new().with_native_roots().https_only().enable_http1().build();
|
||||||
@ -28,7 +29,7 @@ async fn stream(url: &str, req: &Request<Body>) -> Result<Response<Body>, String
|
|||||||
// Build the hyper client from the HTTPS connector.
|
// Build the hyper client from the HTTPS connector.
|
||||||
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
let client: client::Client<_, hyper::Body> = client::Client::builder().build(https);
|
||||||
|
|
||||||
let mut builder = Request::get(url);
|
let mut builder = Request::get(uri);
|
||||||
|
|
||||||
// Copy useful headers from original request
|
// Copy useful headers from original request
|
||||||
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
for &key in &["Range", "If-Modified-Since", "Cache-Control"] {
|
||||||
@ -89,7 +90,10 @@ fn request(url: String, quarantine: bool) -> Boxed<Result<Response<Body>, String
|
|||||||
response
|
response
|
||||||
.headers()
|
.headers()
|
||||||
.get("Location")
|
.get("Location")
|
||||||
.map(|val| val.to_str().unwrap_or_default())
|
.map(|val| {
|
||||||
|
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()
|
.unwrap_or_default()
|
||||||
.to_string(),
|
.to_string(),
|
||||||
quarantine,
|
quarantine,
|
||||||
|
69
src/main.rs
69
src/main.rs
@ -1,13 +1,6 @@
|
|||||||
// Global specifiers
|
// Global specifiers
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(clippy::pedantic, clippy::all)]
|
#![allow(clippy::cmp_owned)]
|
||||||
#![allow(
|
|
||||||
clippy::needless_pass_by_value,
|
|
||||||
clippy::cast_possible_truncation,
|
|
||||||
clippy::cast_possible_wrap,
|
|
||||||
clippy::manual_find_map,
|
|
||||||
clippy::unused_async
|
|
||||||
)]
|
|
||||||
|
|
||||||
// Reference local files
|
// Reference local files
|
||||||
mod post;
|
mod post;
|
||||||
@ -18,7 +11,7 @@ mod user;
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
// Import Crates
|
// Import Crates
|
||||||
use clap::{App as cli, Arg};
|
use clap::{Arg, Command};
|
||||||
|
|
||||||
use futures_lite::FutureExt;
|
use futures_lite::FutureExt;
|
||||||
use hyper::{header::HeaderValue, Body, Request, Response};
|
use hyper::{header::HeaderValue, Body, Request, Response};
|
||||||
@ -26,7 +19,7 @@ use hyper::{header::HeaderValue, Body, Request, Response};
|
|||||||
mod client;
|
mod client;
|
||||||
use client::proxy;
|
use client::proxy;
|
||||||
use server::RequestExt;
|
use server::RequestExt;
|
||||||
use utils::{error, redirect};
|
use utils::{error, redirect, ThemeAssets};
|
||||||
|
|
||||||
mod server;
|
mod server;
|
||||||
|
|
||||||
@ -70,6 +63,7 @@ async fn font() -> Result<Response<Body>, String> {
|
|||||||
Response::builder()
|
Response::builder()
|
||||||
.status(200)
|
.status(200)
|
||||||
.header("content-type", "font/woff2")
|
.header("content-type", "font/woff2")
|
||||||
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
.body(include_bytes!("../static/Inter.var.woff2").as_ref().into())
|
.body(include_bytes!("../static/Inter.var.woff2").as_ref().into())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
)
|
)
|
||||||
@ -91,50 +85,67 @@ async fn resource(body: &str, content_type: &str, cache: bool) -> Result<Respons
|
|||||||
Ok(res)
|
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]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
let matches = cli::new("Libreddit")
|
let matches = Command::new("Libreddit")
|
||||||
.version(env!("CARGO_PKG_VERSION"))
|
.version(env!("CARGO_PKG_VERSION"))
|
||||||
.about("Private front-end for Reddit written in Rust ")
|
.about("Private front-end for Reddit written in Rust ")
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("redirect-https")
|
Arg::new("redirect-https")
|
||||||
.short("r")
|
.short('r')
|
||||||
.long("redirect-https")
|
.long("redirect-https")
|
||||||
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
|
.help("Redirect all HTTP requests to HTTPS (no longer functional)")
|
||||||
.takes_value(false),
|
.num_args(0),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("address")
|
Arg::new("address")
|
||||||
.short("a")
|
.short('a')
|
||||||
.long("address")
|
.long("address")
|
||||||
.value_name("ADDRESS")
|
.value_name("ADDRESS")
|
||||||
.help("Sets address to listen on")
|
.help("Sets address to listen on")
|
||||||
.default_value("0.0.0.0")
|
.default_value("0.0.0.0")
|
||||||
.takes_value(true),
|
.num_args(1),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("port")
|
Arg::new("port")
|
||||||
.short("p")
|
.short('p')
|
||||||
.long("port")
|
.long("port")
|
||||||
.value_name("PORT")
|
.value_name("PORT")
|
||||||
.help("Port to listen on")
|
.help("Port to listen on")
|
||||||
.default_value("8080")
|
.default_value("8080")
|
||||||
.takes_value(true),
|
.num_args(1),
|
||||||
)
|
)
|
||||||
.arg(
|
.arg(
|
||||||
Arg::with_name("hsts")
|
Arg::new("hsts")
|
||||||
.short("H")
|
.short('H')
|
||||||
.long("hsts")
|
.long("hsts")
|
||||||
.value_name("EXPIRE_TIME")
|
.value_name("EXPIRE_TIME")
|
||||||
.help("HSTS header to tell browsers that this site should only be accessed over HTTPS")
|
.help("HSTS header to tell browsers that this site should only be accessed over HTTPS")
|
||||||
.default_value("604800")
|
.default_value("604800")
|
||||||
.takes_value(true),
|
.num_args(1),
|
||||||
)
|
)
|
||||||
.get_matches();
|
.get_matches();
|
||||||
|
|
||||||
let address = matches.value_of("address").unwrap_or("0.0.0.0");
|
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.value_of("port").unwrap_or("8080").to_string());
|
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.value_of("hsts");
|
let hsts = matches.get_one("hsts").map(|m: &String| m.as_str());
|
||||||
|
|
||||||
let listener = [address, ":", &port].concat();
|
let listener = [address, ":", &port].concat();
|
||||||
|
|
||||||
@ -158,7 +169,7 @@ async fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read static files
|
// 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
|
app
|
||||||
.at("/manifest.json")
|
.at("/manifest.json")
|
||||||
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
|
.get(|_| resource(include_str!("../static/manifest.json"), "application/json", false).boxed());
|
||||||
@ -183,6 +194,9 @@ async fn main() {
|
|||||||
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
app.at("/img/*path").get(|r| proxy(r, "https://i.redd.it/{path}").boxed());
|
||||||
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
app.at("/thumb/:point/:id").get(|r| proxy(r, "https://{point}.thumbs.redditmedia.com/{id}").boxed());
|
||||||
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
app.at("/emoji/:id/:name").get(|r| proxy(r, "https://emoji.redditmedia.com/{id}/{name}").boxed());
|
||||||
|
app
|
||||||
|
.at("/preview/:loc/award_images/:fullname/:id")
|
||||||
|
.get(|r| proxy(r, "https://{loc}view.redd.it/award_images/{fullname}/{id}").boxed());
|
||||||
app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed());
|
app.at("/preview/:loc/:id").get(|r| proxy(r, "https://{loc}view.redd.it/{id}").boxed());
|
||||||
app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed());
|
app.at("/style/*path").get(|r| proxy(r, "https://styles.redditmedia.com/{path}").boxed());
|
||||||
app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed());
|
app.at("/static/*path").get(|r| proxy(r, "https://www.redditstatic.com/{path}").boxed());
|
||||||
@ -196,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/[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").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").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").get(|r| post::item(r).boxed());
|
||||||
app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
app.at("/user/:name/comments/:id/:title/:comment_id").get(|r| post::item(r).boxed());
|
||||||
|
36
src/post.rs
36
src/post.rs
@ -1,6 +1,5 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::esc;
|
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::subreddit::{can_access_quarantine, quarantine};
|
use crate::subreddit::{can_access_quarantine, quarantine};
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
@ -13,7 +12,7 @@ use std::collections::HashSet;
|
|||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "post.html", escape = "none")]
|
#[template(path = "post.html")]
|
||||||
struct PostTemplate {
|
struct PostTemplate {
|
||||||
comments: Vec<Comment>,
|
comments: Vec<Comment>,
|
||||||
post: Post,
|
post: Post,
|
||||||
@ -97,12 +96,23 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
|
|
||||||
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
|
let awards: Awards = Awards::parse(&post["data"]["all_awardings"]);
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rewrite_urls(&val(post, "selftext_html"))
|
||||||
|
};
|
||||||
|
|
||||||
// Build a post using data parsed from Reddit post API
|
// Build a post using data parsed from Reddit post API
|
||||||
Post {
|
Post {
|
||||||
id: val(post, "id"),
|
id: val(post, "id"),
|
||||||
title: esc!(post, "title"),
|
title: val(post, "title"),
|
||||||
community: val(post, "subreddit"),
|
community: val(post, "subreddit"),
|
||||||
body: rewrite_urls(&val(post, "selftext_html")).replace("\\", ""),
|
body,
|
||||||
author: Author {
|
author: Author {
|
||||||
name: val(post, "author"),
|
name: val(post, "author"),
|
||||||
flair: Flair {
|
flair: Flair {
|
||||||
@ -111,13 +121,13 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
post["data"]["author_flair_richtext"].as_array(),
|
post["data"]["author_flair_richtext"].as_array(),
|
||||||
post["data"]["author_flair_text"].as_str(),
|
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"),
|
background_color: val(post, "author_flair_background_color"),
|
||||||
foreground_color: val(post, "author_flair_text_color"),
|
foreground_color: val(post, "author_flair_text_color"),
|
||||||
},
|
},
|
||||||
distinguished: val(post, "distinguished"),
|
distinguished: val(post, "distinguished"),
|
||||||
},
|
},
|
||||||
permalink: val(post, "permalink"),
|
permalink,
|
||||||
score: format_num(score),
|
score: format_num(score),
|
||||||
upvote_ratio: ratio as i64,
|
upvote_ratio: ratio as i64,
|
||||||
post_type,
|
post_type,
|
||||||
@ -135,7 +145,7 @@ async fn parse_post(json: &serde_json::Value) -> Post {
|
|||||||
post["data"]["link_flair_richtext"].as_array(),
|
post["data"]["link_flair_richtext"].as_array(),
|
||||||
post["data"]["link_flair_text"].as_str(),
|
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"),
|
background_color: val(post, "link_flair_background_color"),
|
||||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||||
"black".to_string()
|
"black".to_string()
|
||||||
@ -174,7 +184,6 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
|
let edited = data["edited"].as_f64().map_or((String::new(), String::new()), time);
|
||||||
|
|
||||||
let score = data["score"].as_i64().unwrap_or(0);
|
let score = data["score"].as_i64().unwrap_or(0);
|
||||||
let body = rewrite_urls(&val(&comment, "body_html"));
|
|
||||||
|
|
||||||
// If this comment contains replies, handle those too
|
// If this comment contains replies, handle those too
|
||||||
let replies: Vec<Comment> = if data["replies"].is_object() {
|
let replies: Vec<Comment> = if data["replies"].is_object() {
|
||||||
@ -191,6 +200,15 @@ fn parse_comments(json: &serde_json::Value, post_link: &str, post_author: &str,
|
|||||||
let id = val(&comment, "id");
|
let id = val(&comment, "id");
|
||||||
let highlighted = id == highlighted_comment;
|
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
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
rewrite_urls(&val(&comment, "body_html"))
|
||||||
|
};
|
||||||
|
|
||||||
let author = Author {
|
let author = Author {
|
||||||
name: val(&comment, "author"),
|
name: val(&comment, "author"),
|
||||||
flair: Flair {
|
flair: Flair {
|
||||||
@ -199,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_richtext"].as_array(),
|
||||||
data["author_flair_text"].as_str(),
|
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"),
|
background_color: val(&comment, "author_flair_background_color"),
|
||||||
foreground_color: val(&comment, "author_flair_text_color"),
|
foreground_color: val(&comment, "author_flair_text_color"),
|
||||||
},
|
},
|
||||||
|
@ -29,7 +29,7 @@ struct Subreddit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "search.html", escape = "none")]
|
#[template(path = "search.html")]
|
||||||
struct SearchTemplate {
|
struct SearchTemplate {
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
subreddits: Vec<Subreddit>,
|
subreddits: Vec<Subreddit>,
|
||||||
@ -47,13 +47,17 @@ struct SearchTemplate {
|
|||||||
// SERVICES
|
// SERVICES
|
||||||
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
||||||
let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
let nsfw_results = if setting(&req, "show_nsfw") == "on" { "&include_over_18=on" } else { "" };
|
||||||
let path = format!("{}.json?{}{}", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
let path = format!("{}.json?{}{}&raw_json=1", req.uri().path(), req.uri().query().unwrap_or_default(), nsfw_results);
|
||||||
let query = param(&path, "q").unwrap_or_default();
|
let query = param(&path, "q").unwrap_or_default();
|
||||||
|
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
return Ok(redirect("/".to_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 sub = req.param("sub").unwrap_or_default();
|
||||||
let quarantined = can_access_quarantine(&req, &sub);
|
let quarantined = can_access_quarantine(&req, &sub);
|
||||||
// Handle random subreddits
|
// Handle random subreddits
|
||||||
@ -78,7 +82,7 @@ pub async fn find(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
let url = String::from(req.uri().path_and_query().map_or("", |val| val.as_str()));
|
||||||
|
|
||||||
// If all requested subs are filtered, we don't need to fetch posts.
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
if sub.split("+").all(|s| filters.contains(s)) {
|
if sub.split('+').all(|s| filters.contains(s)) {
|
||||||
template(SearchTemplate {
|
template(SearchTemplate {
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
subreddits,
|
subreddits,
|
||||||
|
@ -105,7 +105,7 @@ impl ResponseExt for Response<Body> {
|
|||||||
fn remove_cookie(&mut self, name: String) {
|
fn remove_cookie(&mut self, name: String) {
|
||||||
let mut cookie = Cookie::named(name);
|
let mut cookie = Cookie::named(name);
|
||||||
cookie.set_path("/");
|
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()) {
|
if let Ok(val) = HeaderValue::from_str(&cookie.to_string()) {
|
||||||
self.headers_mut().append("Set-Cookie", val);
|
self.headers_mut().append("Set-Cookie", val);
|
||||||
}
|
}
|
||||||
@ -158,8 +158,8 @@ impl Server {
|
|||||||
Ok::<_, String>(service_fn(move |req: Request<Body>| {
|
Ok::<_, String>(service_fn(move |req: Request<Body>| {
|
||||||
let headers = default_headers.clone();
|
let headers = default_headers.clone();
|
||||||
|
|
||||||
// Remove double slashes
|
// Remove double slashes and decode encoded slashes
|
||||||
let mut path = req.uri().path().replace("//", "/");
|
let mut path = req.uri().path().replace("//", "/").replace("%2F","/");
|
||||||
|
|
||||||
// Remove trailing slashes
|
// Remove trailing slashes
|
||||||
if path != "/" && path.ends_with('/') {
|
if path != "/" && path.ends_with('/') {
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::esc;
|
|
||||||
use crate::utils::{
|
use crate::utils::{
|
||||||
catch_random, error, filter_posts, format_num, format_url, get_filters, param, redirect, rewrite_urls, setting, template, val, Post, Preferences, Subreddit,
|
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
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "subreddit.html", escape = "none")]
|
#[template(path = "subreddit.html")]
|
||||||
struct SubredditTemplate {
|
struct SubredditTemplate {
|
||||||
sub: Subreddit,
|
sub: Subreddit,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
@ -19,6 +18,7 @@ struct SubredditTemplate {
|
|||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
url: String,
|
url: String,
|
||||||
|
redirect_url: String,
|
||||||
/// Whether the subreddit itself is filtered.
|
/// Whether the subreddit itself is filtered.
|
||||||
is_filtered: bool,
|
is_filtered: bool,
|
||||||
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||||
@ -27,7 +27,7 @@ struct SubredditTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "wiki.html", escape = "none")]
|
#[template(path = "wiki.html")]
|
||||||
struct WikiTemplate {
|
struct WikiTemplate {
|
||||||
sub: String,
|
sub: String,
|
||||||
wiki: String,
|
wiki: String,
|
||||||
@ -37,7 +37,7 @@ struct WikiTemplate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "wall.html", escape = "none")]
|
#[template(path = "wall.html")]
|
||||||
struct WallTemplate {
|
struct WallTemplate {
|
||||||
title: String,
|
title: String,
|
||||||
sub: String,
|
sub: String,
|
||||||
@ -86,22 +86,21 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
} else {
|
} else {
|
||||||
Subreddit::default()
|
Subreddit::default()
|
||||||
}
|
}
|
||||||
} else if sub_name.contains('+') {
|
} else {
|
||||||
// Multireddit
|
// Multireddit, all, popular
|
||||||
Subreddit {
|
Subreddit {
|
||||||
name: sub_name.clone(),
|
name: sub_name.clone(),
|
||||||
..Subreddit::default()
|
..Subreddit::default()
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
Subreddit::default()
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let path = format!("/r/{}/{}.json?{}&raw_json=1", sub_name.clone(), sort, req.uri().query().unwrap_or_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 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);
|
let filters = get_filters(&req);
|
||||||
|
|
||||||
// If all requested subs are filtered, we don't need to fetch posts.
|
// If all requested subs are filtered, we don't need to fetch posts.
|
||||||
if sub_name.split("+").all(|s| filters.contains(s)) {
|
if sub_name.split('+').all(|s| filters.contains(s)) {
|
||||||
template(SubredditTemplate {
|
template(SubredditTemplate {
|
||||||
sub,
|
sub,
|
||||||
posts: Vec::new(),
|
posts: Vec::new(),
|
||||||
@ -109,6 +108,7 @@ pub async fn community(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
redirect_url,
|
||||||
is_filtered: true,
|
is_filtered: true,
|
||||||
all_posts_filtered: false,
|
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),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
redirect_url,
|
||||||
is_filtered: false,
|
is_filtered: false,
|
||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
})
|
})
|
||||||
@ -211,7 +212,7 @@ pub async fn subscriptions_filters(req: Request<Body>) -> Result<Response<Body>,
|
|||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
// Find each subreddit name (separated by '+') in sub parameter
|
// 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
|
// Retrieve display name for the subreddit
|
||||||
let display;
|
let display;
|
||||||
let part = if part.starts_with("u_") {
|
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
|
// Redirect back to subreddit
|
||||||
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
// check for redirect parameter if unsubscribing/unfiltering from outside sidebar
|
||||||
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
let path = if let Some(redirect_path) = param(&format!("?{}", query), "redirect") {
|
||||||
format!("/{}/", redirect_path)
|
format!("/{}", redirect_path)
|
||||||
} else {
|
} else {
|
||||||
format!("/r/{}", sub)
|
format!("/r/{}", sub)
|
||||||
};
|
};
|
||||||
@ -334,10 +335,10 @@ pub async fn sidebar(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
match json(path, quarantined).await {
|
match json(path, quarantined).await {
|
||||||
// If success, receive JSON in response
|
// If success, receive JSON in response
|
||||||
Ok(response) => template(WikiTemplate {
|
Ok(response) => template(WikiTemplate {
|
||||||
wiki: rewrite_urls(&val(&response, "description_html").replace("\\", "")),
|
wiki: rewrite_urls(&val(&response, "description_html")),
|
||||||
// wiki: format!(
|
// wiki: format!(
|
||||||
// "{}<hr><h1>Moderators</h1><br><ul>{}</ul>",
|
// "{}<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(""),
|
// moderators(&sub, quarantined).await.unwrap_or(vec!["Could not fetch moderators".to_string()]).join(""),
|
||||||
// ),
|
// ),
|
||||||
sub,
|
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() };
|
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||||
|
|
||||||
Ok(Subreddit {
|
Ok(Subreddit {
|
||||||
name: esc!(&res, "display_name"),
|
name: val(&res, "display_name"),
|
||||||
title: esc!(&res, "title"),
|
title: val(&res, "title"),
|
||||||
description: esc!(&res, "public_description"),
|
description: val(&res, "public_description"),
|
||||||
info: rewrite_urls(&val(&res, "description_html").replace("\\", "")),
|
info: rewrite_urls(&val(&res, "description_html")),
|
||||||
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
|
// moderators: moderators_list(sub, quarantined).await.unwrap_or_default(),
|
||||||
icon: format_url(&icon),
|
icon: format_url(&icon),
|
||||||
members: format_num(members),
|
members: format_num(members),
|
||||||
|
29
src/user.rs
29
src/user.rs
@ -1,22 +1,24 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::client::json;
|
use crate::client::json;
|
||||||
use crate::esc;
|
|
||||||
use crate::server::RequestExt;
|
use crate::server::RequestExt;
|
||||||
use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User};
|
use crate::utils::{error, filter_posts, format_url, get_filters, param, template, Post, Preferences, User};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use time::OffsetDateTime;
|
use time::{macros::format_description, OffsetDateTime};
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "user.html", escape = "none")]
|
#[template(path = "user.html")]
|
||||||
struct UserTemplate {
|
struct UserTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
sort: (String, String),
|
sort: (String, String),
|
||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
|
/// "overview", "comments", or "submitted"
|
||||||
|
listing: String,
|
||||||
prefs: Preferences,
|
prefs: Preferences,
|
||||||
url: String,
|
url: String,
|
||||||
|
redirect_url: String,
|
||||||
/// Whether the user themself is filtered.
|
/// Whether the user themself is filtered.
|
||||||
is_filtered: bool,
|
is_filtered: bool,
|
||||||
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
/// Whether all fetched posts are filtered (to differentiate between no posts fetched in the first place,
|
||||||
@ -26,13 +28,17 @@ struct UserTemplate {
|
|||||||
|
|
||||||
// FUNCTIONS
|
// FUNCTIONS
|
||||||
pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
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
|
// Build the Reddit JSON API path
|
||||||
let path = format!(
|
let path = format!(
|
||||||
"/user/{}.json?{}&raw_json=1",
|
"/user/{}/{}.json?{}&raw_json=1",
|
||||||
req.param("name").unwrap_or_else(|| "reddit".to_string()),
|
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 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
|
// Retrieve other variables from Libreddit request
|
||||||
let sort = param(&path, "sort").unwrap_or_default();
|
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(),
|
posts: Vec::new(),
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
ends: (param(&path, "after").unwrap_or_default(), "".to_string()),
|
||||||
|
listing,
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
redirect_url,
|
||||||
is_filtered: true,
|
is_filtered: true,
|
||||||
all_posts_filtered: false,
|
all_posts_filtered: false,
|
||||||
})
|
})
|
||||||
@ -62,8 +70,10 @@ pub async fn profile(req: Request<Body>) -> Result<Response<Body>, String> {
|
|||||||
posts,
|
posts,
|
||||||
sort: (sort, param(&path, "t").unwrap_or_default()),
|
sort: (sort, param(&path, "t").unwrap_or_default()),
|
||||||
ends: (param(&path, "after").unwrap_or_default(), after),
|
ends: (param(&path, "after").unwrap_or_default(), after),
|
||||||
|
listing,
|
||||||
prefs: Preferences::new(req),
|
prefs: Preferences::new(req),
|
||||||
url,
|
url,
|
||||||
|
redirect_url,
|
||||||
is_filtered: false,
|
is_filtered: false,
|
||||||
all_posts_filtered,
|
all_posts_filtered,
|
||||||
})
|
})
|
||||||
@ -82,7 +92,8 @@ async fn user(name: &str) -> Result<User, String> {
|
|||||||
// Send a request to the url
|
// Send a request to the url
|
||||||
json(path, false).await.map(|res| {
|
json(path, false).await.map(|res| {
|
||||||
// Grab creation date as unix timestamp
|
// 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
|
// Closure used to parse JSON from Reddit APIs
|
||||||
let about = |item| res["data"]["subreddit"][item].as_str().unwrap_or_default().to_string();
|
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
|
// Parse the JSON output into a User struct
|
||||||
User {
|
User {
|
||||||
name: res["data"]["name"].as_str().unwrap_or(name).to_owned(),
|
name: res["data"]["name"].as_str().unwrap_or(name).to_owned(),
|
||||||
title: esc!(about("title")),
|
title: about("title"),
|
||||||
icon: format_url(&about("icon_img")),
|
icon: format_url(&about("icon_img")),
|
||||||
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||||
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
created: created.format(format_description!("[month repr:short] [day] '[year repr:last_two]")).unwrap_or_default(),
|
||||||
banner: esc!(about("banner_img")),
|
banner: about("banner_img"),
|
||||||
description: about("public_description"),
|
description: about("public_description"),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
157
src/utils.rs
157
src/utils.rs
@ -1,15 +1,16 @@
|
|||||||
//
|
//
|
||||||
// CRATES
|
// CRATES
|
||||||
//
|
//
|
||||||
use crate::{client::json, esc, server::RequestExt};
|
use crate::{client::json, server::RequestExt};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use cookie::Cookie;
|
use cookie::Cookie;
|
||||||
use hyper::{Body, Request, Response};
|
use hyper::{Body, Request, Response};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use rust_embed::RustEmbed;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use time::{Duration, OffsetDateTime};
|
use time::{macros::format_description, Duration, OffsetDateTime};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
// Post flair with content, background color and foreground color
|
// Post flair with content, background color and foreground color
|
||||||
@ -21,6 +22,7 @@ pub struct Flair {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Part of flair, either emoji or text
|
// Part of flair, either emoji or text
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct FlairPart {
|
pub struct FlairPart {
|
||||||
pub flair_part_type: String,
|
pub flair_part_type: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
@ -40,7 +42,7 @@ impl FlairPart {
|
|||||||
Self {
|
Self {
|
||||||
flair_part_type: value("e").to_string(),
|
flair_part_type: value("e").to_string(),
|
||||||
value: match value("e") {
|
value: match value("e") {
|
||||||
"text" => esc!(value("t")),
|
"text" => value("t").to_string(),
|
||||||
"emoji" => format_url(value("u")),
|
"emoji" => format_url(value("u")),
|
||||||
_ => String::new(),
|
_ => String::new(),
|
||||||
},
|
},
|
||||||
@ -53,7 +55,7 @@ impl FlairPart {
|
|||||||
"text" => match text_flair {
|
"text" => match text_flair {
|
||||||
Some(text) => vec![Self {
|
Some(text) => vec![Self {
|
||||||
flair_part_type: "text".to_string(),
|
flair_part_type: "text".to_string(),
|
||||||
value: esc!(text),
|
value: text.to_string(),
|
||||||
}],
|
}],
|
||||||
None => Vec::new(),
|
None => Vec::new(),
|
||||||
},
|
},
|
||||||
@ -74,6 +76,7 @@ pub struct Flags {
|
|||||||
pub stickied: bool,
|
pub stickied: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
pub struct Media {
|
pub struct Media {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
pub alt_url: String,
|
pub alt_url: String,
|
||||||
@ -86,28 +89,29 @@ impl Media {
|
|||||||
pub async fn parse(data: &Value) -> (String, Self, Vec<GalleryMedia>) {
|
pub async fn parse(data: &Value) -> (String, Self, Vec<GalleryMedia>) {
|
||||||
let mut gallery = Vec::new();
|
let mut gallery = Vec::new();
|
||||||
|
|
||||||
|
// Define the various known places that Reddit might put video URLs.
|
||||||
|
let data_preview = &data["preview"]["reddit_video_preview"];
|
||||||
|
let secure_media = &data["secure_media"]["reddit_video"];
|
||||||
|
let crosspost_parent_media = &data["crosspost_parent_list"][0]["secure_media"]["reddit_video"];
|
||||||
|
|
||||||
// If post is a video, return the video
|
// If post is a video, return the video
|
||||||
let (post_type, url_val, alt_url_val) = if data["preview"]["reddit_video_preview"]["fallback_url"].is_string() {
|
let (post_type, url_val, alt_url_val) = if data_preview["fallback_url"].is_string() {
|
||||||
// Return reddit video
|
|
||||||
(
|
(
|
||||||
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
|
if data_preview["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
|
||||||
"gif"
|
&data_preview["fallback_url"],
|
||||||
} else {
|
Some(&data_preview["hls_url"]),
|
||||||
"video"
|
|
||||||
},
|
|
||||||
&data["preview"]["reddit_video_preview"]["fallback_url"],
|
|
||||||
Some(&data["preview"]["reddit_video_preview"]["hls_url"]),
|
|
||||||
)
|
)
|
||||||
} else if data["secure_media"]["reddit_video"]["fallback_url"].is_string() {
|
} else if secure_media["fallback_url"].is_string() {
|
||||||
// Return reddit video
|
|
||||||
(
|
(
|
||||||
if data["preview"]["reddit_video_preview"]["is_gif"].as_bool().unwrap_or(false) {
|
if secure_media["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
|
||||||
"gif"
|
&secure_media["fallback_url"],
|
||||||
} else {
|
Some(&secure_media["hls_url"]),
|
||||||
"video"
|
)
|
||||||
},
|
} else if crosspost_parent_media["fallback_url"].is_string() {
|
||||||
&data["secure_media"]["reddit_video"]["fallback_url"],
|
(
|
||||||
Some(&data["secure_media"]["reddit_video"]["hls_url"]),
|
if crosspost_parent_media["is_gif"].as_bool().unwrap_or(false) { "gif" } else { "video" },
|
||||||
|
&crosspost_parent_media["fallback_url"],
|
||||||
|
Some(&crosspost_parent_media["hls_url"]),
|
||||||
)
|
)
|
||||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||||
// Handle images, whether GIFs or pics
|
// Handle images, whether GIFs or pics
|
||||||
@ -140,18 +144,12 @@ impl Media {
|
|||||||
|
|
||||||
let source = &data["preview"]["images"][0]["source"];
|
let source = &data["preview"]["images"][0]["source"];
|
||||||
|
|
||||||
let url = if post_type == "self" || post_type == "link" {
|
|
||||||
url_val.as_str().unwrap_or_default().to_string()
|
|
||||||
} else {
|
|
||||||
format_url(url_val.as_str().unwrap_or_default())
|
|
||||||
};
|
|
||||||
|
|
||||||
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
let alt_url = alt_url_val.map_or(String::new(), |val| format_url(val.as_str().unwrap_or_default()));
|
||||||
|
|
||||||
(
|
(
|
||||||
post_type.to_string(),
|
post_type.to_string(),
|
||||||
Self {
|
Self {
|
||||||
url,
|
url: format_url(url_val.as_str().unwrap_or_default()),
|
||||||
alt_url,
|
alt_url,
|
||||||
width: source["width"].as_i64().unwrap_or_default(),
|
width: source["width"].as_i64().unwrap_or_default(),
|
||||||
height: source["height"].as_i64().unwrap_or_default(),
|
height: source["height"].as_i64().unwrap_or_default(),
|
||||||
@ -220,24 +218,19 @@ pub struct Post {
|
|||||||
impl Post {
|
impl Post {
|
||||||
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
// 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> {
|
pub async fn fetch(path: &str, quarantine: bool) -> Result<(Vec<Self>, String), String> {
|
||||||
let res;
|
|
||||||
let post_list;
|
|
||||||
|
|
||||||
// Send a request to the url
|
// 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
|
// If success, receive JSON in response
|
||||||
Ok(response) => {
|
Ok(response) => response,
|
||||||
res = response;
|
|
||||||
}
|
|
||||||
// If the Reddit API returns an error, exit this function
|
// If the Reddit API returns an error, exit this function
|
||||||
Err(msg) => return Err(msg),
|
Err(msg) => return Err(msg),
|
||||||
}
|
};
|
||||||
|
|
||||||
// Fetch the list of posts from the JSON response
|
// Fetch the list of posts from the JSON response
|
||||||
match res["data"]["children"].as_array() {
|
let post_list = match res["data"]["children"].as_array() {
|
||||||
Some(list) => post_list = list,
|
Some(list) => list,
|
||||||
None => return Err("No posts found".to_string()),
|
None => return Err("No posts found".to_string()),
|
||||||
}
|
};
|
||||||
|
|
||||||
let mut posts: Vec<Self> = Vec::new();
|
let mut posts: Vec<Self> = Vec::new();
|
||||||
|
|
||||||
@ -248,16 +241,16 @@ impl Post {
|
|||||||
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
let (rel_time, created) = time(data["created_utc"].as_f64().unwrap_or_default());
|
||||||
let score = data["score"].as_i64().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 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
|
// Determine the type of media along with the media URL
|
||||||
let (post_type, media, gallery) = Media::parse(&data).await;
|
let (post_type, media, gallery) = Media::parse(data).await;
|
||||||
let awards = Awards::parse(&data["all_awardings"]);
|
let awards = Awards::parse(&data["all_awardings"]);
|
||||||
|
|
||||||
// selftext_html is set for text posts when browsing.
|
// selftext_html is set for text posts when browsing.
|
||||||
let mut body = rewrite_urls(&val(post, "selftext_html"));
|
let mut body = rewrite_urls(&val(post, "selftext_html"));
|
||||||
if body == "" {
|
if body.is_empty() {
|
||||||
body = rewrite_urls(&val(post, "body_html"))
|
body = rewrite_urls(&val(post, "body_html"));
|
||||||
}
|
}
|
||||||
|
|
||||||
posts.push(Self {
|
posts.push(Self {
|
||||||
@ -273,7 +266,7 @@ impl Post {
|
|||||||
data["author_flair_richtext"].as_array(),
|
data["author_flair_richtext"].as_array(),
|
||||||
data["author_flair_text"].as_str(),
|
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"),
|
background_color: val(post, "author_flair_background_color"),
|
||||||
foreground_color: val(post, "author_flair_text_color"),
|
foreground_color: val(post, "author_flair_text_color"),
|
||||||
},
|
},
|
||||||
@ -301,7 +294,7 @@ impl Post {
|
|||||||
data["link_flair_richtext"].as_array(),
|
data["link_flair_richtext"].as_array(),
|
||||||
data["link_flair_text"].as_str(),
|
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"),
|
background_color: val(post, "link_flair_background_color"),
|
||||||
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
foreground_color: if val(post, "link_flair_text_color") == "dark" {
|
||||||
"black".to_string()
|
"black".to_string()
|
||||||
@ -327,7 +320,7 @@ impl Post {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "comment.html", escape = "none")]
|
#[template(path = "comment.html")]
|
||||||
// Comment with content, post, score and data/time that it was posted
|
// Comment with content, post, score and data/time that it was posted
|
||||||
pub struct Comment {
|
pub struct Comment {
|
||||||
pub id: String,
|
pub id: String,
|
||||||
@ -384,7 +377,7 @@ impl Awards {
|
|||||||
pub fn parse(items: &Value) -> Self {
|
pub fn parse(items: &Value) -> Self {
|
||||||
let parsed = items.as_array().unwrap_or(&Vec::new()).iter().fold(Vec::new(), |mut awards, item| {
|
let parsed = items.as_array().unwrap_or(&Vec::new()).iter().fold(Vec::new(), |mut awards, item| {
|
||||||
let name = item["name"].as_str().unwrap_or_default().to_string();
|
let name = item["name"].as_str().unwrap_or_default().to_string();
|
||||||
let icon_url = format_url(&item["icon_url"].as_str().unwrap_or_default().to_string());
|
let icon_url = format_url(item["resized_icons"][0]["url"].as_str().unwrap_or_default());
|
||||||
let description = item["description"].as_str().unwrap_or_default().to_string();
|
let description = item["description"].as_str().unwrap_or_default().to_string();
|
||||||
let count: i64 = i64::from_str(&item["count"].to_string()).unwrap_or(1);
|
let count: i64 = i64::from_str(&item["count"].to_string()).unwrap_or(1);
|
||||||
|
|
||||||
@ -403,7 +396,7 @@ impl Awards {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "error.html", escape = "none")]
|
#[template(path = "error.html")]
|
||||||
pub struct ErrorTemplate {
|
pub struct ErrorTemplate {
|
||||||
pub msg: String,
|
pub msg: String,
|
||||||
pub prefs: Preferences,
|
pub prefs: Preferences,
|
||||||
@ -448,6 +441,7 @@ pub struct Params {
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Preferences {
|
pub struct Preferences {
|
||||||
|
pub available_themes: Vec<String>,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
pub front_page: String,
|
pub front_page: String,
|
||||||
pub layout: String,
|
pub layout: String,
|
||||||
@ -462,10 +456,23 @@ pub struct Preferences {
|
|||||||
pub filters: Vec<String>,
|
pub filters: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(RustEmbed)]
|
||||||
|
#[folder = "static/themes/"]
|
||||||
|
#[include = "*.css"]
|
||||||
|
pub struct ThemeAssets;
|
||||||
|
|
||||||
impl Preferences {
|
impl Preferences {
|
||||||
// Build preferences from cookies
|
// Build preferences from cookies
|
||||||
pub fn new(req: Request<Body>) -> Self {
|
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 {
|
Self {
|
||||||
|
available_themes: themes,
|
||||||
theme: setting(&req, "theme"),
|
theme: setting(&req, "theme"),
|
||||||
front_page: setting(&req, "front_page"),
|
front_page: setting(&req, "front_page"),
|
||||||
layout: setting(&req, "layout"),
|
layout: setting(&req, "layout"),
|
||||||
@ -484,7 +491,7 @@ impl Preferences {
|
|||||||
|
|
||||||
/// Gets a `HashSet` of filters from the cookie in the given `Request`.
|
/// Gets a `HashSet` of filters from the cookie in the given `Request`.
|
||||||
pub fn get_filters(req: &Request<Body>) -> HashSet<String> {
|
pub fn get_filters(req: &Request<Body>) -> HashSet<String> {
|
||||||
setting(&req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect::<HashSet<String>>()
|
setting(req, "filters").split('+').map(String::from).filter(|s| !s.is_empty()).collect::<HashSet<String>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filters a `Vec<Post>` by the given `HashSet` of filters (each filter being a subreddit name or a user name). If a
|
/// Filters a `Vec<Post>` by the given `HashSet` of filters (each filter being a subreddit name or a user name). If a
|
||||||
@ -551,7 +558,7 @@ pub fn format_url(url: &str) -> String {
|
|||||||
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||||
String::new()
|
String::new()
|
||||||
} else {
|
} 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 domain = parsed.domain().unwrap_or_default();
|
||||||
|
|
||||||
let capture = |regex: &str, format: &str, segments: i16| {
|
let capture = |regex: &str, format: &str, segments: i16| {
|
||||||
@ -586,8 +593,12 @@ pub fn format_url(url: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
match domain {
|
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!(
|
"v.redd.it" => chain!(
|
||||||
capture(r"https://v\.redd\.it/(.*)/DASH_([0-9]{2,4}(\.mp4|$))", "/vid/", 2),
|
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)
|
capture(r"https://v\.redd\.it/(.+)/(HLSPlaylist\.m3u8.*)$", "/hls/", 2)
|
||||||
),
|
),
|
||||||
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
|
"i.redd.it" => capture(r"https://i\.redd\.it/(.*)", "/img/", 1),
|
||||||
@ -598,7 +609,7 @@ pub fn format_url(url: &str) -> String {
|
|||||||
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
|
"external-preview.redd.it" => capture(r"https://external\-preview\.redd\.it/(.*)", "/preview/external-pre/", 1),
|
||||||
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
|
"styles.redditmedia.com" => capture(r"https://styles\.redditmedia\.com/(.*)", "/style/", 1),
|
||||||
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
|
"www.redditstatic.com" => capture(r"https://www\.redditstatic\.com/(.*)", "/static/", 1),
|
||||||
_ => String::new(),
|
_ => url.to_string(),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -606,8 +617,11 @@ pub fn format_url(url: &str) -> String {
|
|||||||
|
|
||||||
// Rewrite Reddit links to Libreddit in body of text
|
// Rewrite Reddit links to Libreddit in body of text
|
||||||
pub fn rewrite_urls(input_text: &str) -> String {
|
pub fn rewrite_urls(input_text: &str) -> String {
|
||||||
let text1 =
|
let text1 = Regex::new(r#"href="(https|http|)://(www\.|old\.|np\.|amp\.|)(reddit\.com|redd\.it)/"#)
|
||||||
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());
|
.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
|
// Rewrite external media previews to Libreddit
|
||||||
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
|
Regex::new(r"https://external-preview\.redd\.it(.*)[^?]").map_or(String::new(), |re| {
|
||||||
@ -636,12 +650,12 @@ pub fn format_num(num: i64) -> (String, String) {
|
|||||||
|
|
||||||
// Parse a relative and absolute time from a UNIX timestamp
|
// Parse a relative and absolute time from a UNIX timestamp
|
||||||
pub fn time(created: f64) -> (String, String) {
|
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;
|
let time_delta = OffsetDateTime::now_utc() - time;
|
||||||
|
|
||||||
// If the time difference is more than a month, show full date
|
// If the time difference is more than a month, show full date
|
||||||
let rel_time = if time_delta > Duration::days(30) {
|
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
|
// Otherwise, show relative date/time
|
||||||
} else if time_delta.whole_days() > 0 {
|
} else if time_delta.whole_days() > 0 {
|
||||||
format!("{}d ago", time_delta.whole_days())
|
format!("{}d ago", time_delta.whole_days())
|
||||||
@ -651,7 +665,12 @@ pub fn time(created: f64) -> (String, String) {
|
|||||||
format!("{}m ago", time_delta.whole_minutes())
|
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
|
// val() function used to parse JSON from Reddit APIs
|
||||||
@ -659,17 +678,6 @@ pub fn val(j: &Value, k: &str) -> String {
|
|||||||
j["data"][k].as_str().unwrap_or_default().to_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
|
// NETWORKING
|
||||||
//
|
//
|
||||||
@ -709,6 +717,7 @@ pub async fn error(req: Request<Body>, msg: String) -> Result<Response<Body>, St
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::format_num;
|
use super::format_num;
|
||||||
|
use super::rewrite_urls;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn format_num_works() {
|
fn format_num_works() {
|
||||||
@ -718,4 +727,14 @@ mod tests {
|
|||||||
assert_eq!(format_num(1001), ("1.0k".to_string(), "1001".to_string()));
|
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()));
|
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>"#
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
168
static/style.css
168
static/style.css
@ -45,124 +45,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light theme setting */
|
/* Other themes are located in the "themes" folder */
|
||||||
.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* General */
|
/* General */
|
||||||
|
|
||||||
@ -177,6 +60,7 @@
|
|||||||
|
|
||||||
html, body, div, h1, h2, h3, h4, h5, h6, ul, ol, dl, li, dt, dd, p, blockquote,
|
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 {
|
pre, form, fieldset, table, th, td, select, input {
|
||||||
|
accent-color: var(--accent);
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: "Inter", sans-serif;
|
font-family: "Inter", sans-serif;
|
||||||
@ -465,6 +349,7 @@ aside {
|
|||||||
#wiki {
|
#wiki {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
padding: 35px;
|
padding: 35px;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
#top {
|
#top {
|
||||||
@ -486,8 +371,8 @@ aside {
|
|||||||
|
|
||||||
/* Sorting and Search */
|
/* Sorting and Search */
|
||||||
|
|
||||||
select, #search, #sort_options, #inside, #searchbox > *, #sort_submit {
|
select, #search, #sort_options, #listing_options, #inside, #searchbox > *, #sort_submit {
|
||||||
height: 40px;
|
height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search_label {
|
.search_label {
|
||||||
@ -504,7 +389,7 @@ select {
|
|||||||
|
|
||||||
select, #search {
|
select, #search {
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0 15px;
|
padding: 0 10px;
|
||||||
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
@ -562,6 +447,11 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||||||
border-radius: 5px 0px 0px 5px;
|
border-radius: 5px 0px 0px 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#listing_options + #sort_select {
|
||||||
|
margin-left: 10px;
|
||||||
|
border-radius: 5px 0px 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
#search_sort {
|
#search_sort {
|
||||||
background: var(--highlighted);
|
background: var(--highlighted);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -590,15 +480,16 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort_options, footer > a {
|
#sort_options, #listing_options, footer > a {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
align-items: center;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort_options > a, footer > a {
|
#sort_options > a, #listing_options > a, footer > a {
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@ -606,12 +497,12 @@ button.submit:hover > svg { stroke: var(--accent); }
|
|||||||
transition: 0.2s background;
|
transition: 0.2s background;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort_options > a.selected {
|
#sort_options > a.selected, #listing_options > a.selected {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort_options > a:not(.selected):hover {
|
#sort_options > a:not(.selected):hover, #listing_options > a:not(.selected):hover {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -718,7 +609,7 @@ a.search_subreddit:hover {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.post_score {
|
.post_score {
|
||||||
padding-top: 16px;
|
padding-top: 19px;
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
@ -747,6 +638,7 @@ a.search_subreddit:hover {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
margin: 5px 15px 5px 12px;
|
margin: 5px 15px 5px 12px;
|
||||||
grid-area: post_title;
|
grid-area: post_title;
|
||||||
}
|
}
|
||||||
@ -874,8 +766,9 @@ a.search_subreddit:hover {
|
|||||||
|
|
||||||
#post_url {
|
#post_url {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
margin: 5px 15px;
|
margin: 5px 12px;
|
||||||
grid-area: post_media;
|
grid-area: post_media;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_body {
|
.post_body {
|
||||||
@ -884,6 +777,7 @@ a.search_subreddit:hover {
|
|||||||
padding: 5px 15px 5px 12px;
|
padding: 5px 15px 5px 12px;
|
||||||
grid-area: post_body;
|
grid-area: post_body;
|
||||||
width: calc(100% - 30px);
|
width: calc(100% - 30px);
|
||||||
|
overflow-wrap: anywhere;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Used only for text post preview */
|
/* Used only for text post preview */
|
||||||
@ -901,7 +795,7 @@ a.search_subreddit:hover {
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
grid-area: post_footer;
|
grid-area: post_footer;
|
||||||
margin: 5px 20px 15px 15px;
|
margin: 5px 20px 15px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_comments {
|
.post_comments {
|
||||||
@ -1179,12 +1073,10 @@ summary.comment_data {
|
|||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.prefs {
|
.prefs {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
background: var(--post);
|
background: var(--post);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -1197,11 +1089,19 @@ summary.comment_data {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 35px;
|
height: 35px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 10px;
|
margin-top: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefs > p {
|
.prefs legend {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
border-bottom: 1px solid var(--highlighted);
|
||||||
|
font-size: 18px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.prefs legend:not(:first-child) {
|
||||||
|
padding-top: 10px;
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prefs select {
|
.prefs select {
|
||||||
@ -1303,6 +1203,8 @@ input[type="submit"] {
|
|||||||
.md table {
|
.md table {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
|
display: block;
|
||||||
|
max-width: fit-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md code {
|
.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);
|
||||||
|
}
|
@ -35,7 +35,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% block search %}{% endblock %}
|
{% block search %}{% endblock %}
|
||||||
<div id="links">
|
<div id="links">
|
||||||
<a id="reddit_link" href="https://www.reddit.com{{ url }}">
|
<a id="reddit_link" href="https://www.reddit.com{{ url }}" rel="nofollow">
|
||||||
<span>reddit</span>
|
<span>reddit</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
<path d="M23 12.0737C23 10.7308 21.9222 9.64226 20.5926 9.64226C19.9435 9.64226 19.3557 9.90274 18.923 10.3244C17.2772 9.12492 15.0099 8.35046 12.4849 8.26135L13.5814 3.05002L17.1643 3.8195C17.2081 4.73947 17.9539 5.47368 18.8757 5.47368C19.8254 5.47368 20.5951 4.69626 20.5951 3.73684C20.5951 2.77769 19.8254 2 18.8758 2C18.2001 2 17.6214 2.39712 17.3404 2.96952L13.3393 2.11066C13.2279 2.08679 13.1116 2.10858 13.016 2.17125C12.9204 2.23393 12.8533 2.33235 12.8295 2.44491L11.6051 8.25987C9.04278 8.33175 6.73904 9.10729 5.07224 10.3201C4.63988 9.90099 4.05398 9.64226 3.40757 9.64226C2.0781 9.64226 1 10.7308 1 12.0737C1 13.0618 1.58457 13.9105 2.4225 14.2909C2.38466 14.5342 2.36545 14.78 2.36505 15.0263C2.36505 18.7673 6.67626 21.8 11.9945 21.8C17.3131 21.8 21.6243 18.7673 21.6243 15.0263C21.6243 14.7794 21.6043 14.5359 21.5678 14.2957C22.4109 13.9175 23 13.0657 23 12.0737Z"/>
|
<path d="M23 12.0737C23 10.7308 21.9222 9.64226 20.5926 9.64226C19.9435 9.64226 19.3557 9.90274 18.923 10.3244C17.2772 9.12492 15.0099 8.35046 12.4849 8.26135L13.5814 3.05002L17.1643 3.8195C17.2081 4.73947 17.9539 5.47368 18.8757 5.47368C19.8254 5.47368 20.5951 4.69626 20.5951 3.73684C20.5951 2.77769 19.8254 2 18.8758 2C18.2001 2 17.6214 2.39712 17.3404 2.96952L13.3393 2.11066C13.2279 2.08679 13.1116 2.10858 13.016 2.17125C12.9204 2.23393 12.8533 2.33235 12.8295 2.44491L11.6051 8.25987C9.04278 8.33175 6.73904 9.10729 5.07224 10.3201C4.63988 9.90099 4.05398 9.64226 3.40757 9.64226C2.0781 9.64226 1 10.7308 1 12.0737C1 13.0618 1.58457 13.9105 2.4225 14.2909C2.38466 14.5342 2.36545 14.78 2.36505 15.0263C2.36505 18.7673 6.67626 21.8 11.9945 21.8C17.3131 21.8 21.6243 18.7673 21.6243 15.0263C21.6243 14.7794 21.6043 14.5359 21.5678 14.2957C22.4109 13.9175 23 13.0657 23 12.0737Z"/>
|
||||||
|
@ -10,7 +10,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<details class="comment_right" {% if !collapsed || highlighted %}open{% endif %}>
|
<details class="comment_right" {% if !collapsed || highlighted %}open{% endif %}>
|
||||||
<summary class="comment_data">
|
<summary class="comment_data">
|
||||||
<a class="comment_author {{ author.distinguished }} {% if author.name == post_author %}op{% endif %}" href="/user/{{ author.name }}">u/{{ author.name }}</a>
|
{% 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 {{ author.distinguished }}">u/[deleted]</span>
|
||||||
|
{% endif %}
|
||||||
{% if author.flair.flair_parts.len() > 0 %}
|
{% if author.flair.flair_parts.len() > 0 %}
|
||||||
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
<small class="author_flair">{% call utils::render_flair(author.flair.flair_parts) %}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -28,9 +32,9 @@
|
|||||||
{% if is_filtered %}
|
{% if is_filtered %}
|
||||||
<div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div>
|
<div class="comment_body_filtered {% if highlighted %}highlighted{% endif %}">(Filtered content)</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body }}</div>
|
<div class="comment_body {% if highlighted %}highlighted{% endif %}">{{ body|safe }}</div>
|
||||||
{% endif %}
|
{% 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>
|
</blockquote>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
@ -37,7 +37,7 @@
|
|||||||
<p class="post_header">
|
<p class="post_header">
|
||||||
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
<span class="dot">•</span>
|
<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 %}
|
{% if post.author.flair.flair_parts.len() > 0 %}
|
||||||
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
<small class="author_flair">{% call utils::render_flair(post.author.flair.flair_parts) %}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -55,15 +55,15 @@
|
|||||||
</span>
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p class="post_title">
|
<h1 class="post_title">
|
||||||
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
{{ post.title }}
|
||||||
{% if post.flair.flair_parts.len() > 0 %}
|
{% 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"
|
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||||
class="post_flair"
|
class="post_flair"
|
||||||
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call utils::render_flair(post.flair.flair_parts) %}</a>
|
style="color:{{ post.flair.foreground_color }}; background:{{ post.flair.background_color }};">{% call utils::render_flair(post.flair.flair_parts) %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||||
</p>
|
</h1>
|
||||||
|
|
||||||
<!-- POST MEDIA -->
|
<!-- POST MEDIA -->
|
||||||
<!-- post_type: {{ post.post_type }} -->
|
<!-- post_type: {{ post.post_type }} -->
|
||||||
@ -110,7 +110,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- POST BODY -->
|
<!-- 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_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
|
||||||
<div class="post_footer">
|
<div class="post_footer">
|
||||||
<ul id="post_links">
|
<ul id="post_links">
|
||||||
@ -144,7 +144,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{{ c.render().unwrap() }}
|
{{ c.render().unwrap()|safe }}
|
||||||
</div>
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% for subreddit in subreddits %}
|
{% for subreddit in subreddits %}
|
||||||
<a href="{{ subreddit.url }}" class="search_subreddit">
|
<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">
|
<div class="search_subreddit_right">
|
||||||
<p class="search_subreddit_header">
|
<p class="search_subreddit_header">
|
||||||
<span class="search_subreddit_name">r/{{ subreddit.name }}</span>
|
<span class="search_subreddit_name">r/{{ subreddit.name }}</span>
|
||||||
@ -92,13 +92,13 @@
|
|||||||
{% if params.before != "" %}
|
{% if params.before != "" %}
|
||||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||||
&sort={{ params.sort }}&t={{ params.t }}
|
&sort={{ params.sort }}&t={{ params.t }}
|
||||||
&before={{ params.before }}">PREV</a>
|
&before={{ params.before }}" accesskey="P">PREV</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if params.after != "" %}
|
{% if params.after != "" %}
|
||||||
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||||
&sort={{ params.sort }}&t={{ params.t }}
|
&sort={{ params.sort }}&t={{ params.t }}
|
||||||
&after={{ params.after }}">NEXT</a>
|
&after={{ params.after }}" accesskey="N">NEXT</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -11,14 +11,14 @@
|
|||||||
<div id="settings">
|
<div id="settings">
|
||||||
<form action="/settings" method="POST">
|
<form action="/settings" method="POST">
|
||||||
<div class="prefs">
|
<div class="prefs">
|
||||||
<p>Appearance</p>
|
<legend>Appearance</legend>
|
||||||
<div id="theme">
|
<div id="theme">
|
||||||
<label for="theme">Theme:</label>
|
<label for="theme">Theme:</label>
|
||||||
<select name="theme">
|
<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>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<p>Interface</p>
|
<legend>Interface</legend>
|
||||||
<div id="front_page">
|
<div id="front_page">
|
||||||
<label for="front_page">Front page:</label>
|
<label for="front_page">Front page:</label>
|
||||||
<select name="front_page">
|
<select name="front_page">
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<input type="hidden" value="off" name="wide">
|
<input type="hidden" value="off" name="wide">
|
||||||
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
<input type="checkbox" name="wide" {% if prefs.wide == "on" %}checked{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<p>Content</p>
|
<legend>Content</legend>
|
||||||
<div id="post_sort">
|
<div id="post_sort">
|
||||||
<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">
|
||||||
@ -79,7 +79,7 @@
|
|||||||
</form>
|
</form>
|
||||||
{% if prefs.subscriptions.len() > 0 %}
|
{% if prefs.subscriptions.len() > 0 %}
|
||||||
<div class="prefs" id="settings_subs">
|
<div class="prefs" id="settings_subs">
|
||||||
<p>Subscribed Feeds</p>
|
<legend>Subscribed Feeds</legend>
|
||||||
{% for sub in prefs.subscriptions %}
|
{% for sub in prefs.subscriptions %}
|
||||||
<div>
|
<div>
|
||||||
{% let feed -%}
|
{% let feed -%}
|
||||||
@ -94,7 +94,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if !prefs.filters.is_empty() %}
|
{% if !prefs.filters.is_empty() %}
|
||||||
<div class="prefs" id="settings_filters">
|
<div class="prefs" id="settings_filters">
|
||||||
<p>Filtered Feeds</p>
|
<legend>Filtered Feeds</legend>
|
||||||
{% for sub in prefs.filters %}
|
{% for sub in prefs.filters %}
|
||||||
<div>
|
<div>
|
||||||
{% let feed -%}
|
{% let feed -%}
|
||||||
@ -110,7 +110,7 @@
|
|||||||
|
|
||||||
<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 }}&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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if sub.name.contains("+") %}
|
{% 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>
|
<button id="multisub" class="subscribe" title="Subscribe to each sub in this multireddit">Subscribe to Multireddit</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -64,22 +64,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ends.1 != "" %}
|
{% 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 %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_filtered || (sub.name != "" && !sub.name.contains("+")) %}
|
{% if is_filtered || (!sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+")) %}
|
||||||
<aside>
|
<aside>
|
||||||
{% if is_filtered %}
|
{% if is_filtered %}
|
||||||
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
<center>(Content from r/{{ sub.name }} has been filtered)</center>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if sub.name != "" && !sub.name.contains("+") %}
|
{% if !sub.name.is_empty() && sub.name != "all" && sub.name != "popular" && !sub.name.contains("+") %}
|
||||||
<div class="panel" id="subreddit">
|
<div class="panel" id="subreddit">
|
||||||
{% if sub.wiki %}
|
{% if sub.wiki %}
|
||||||
<div id="top">
|
<div id="top">
|
||||||
@ -89,7 +89,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="sub_meta">
|
<div id="sub_meta">
|
||||||
<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 }}">
|
||||||
<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_name">r/{{ sub.name }}</p>
|
||||||
<p id="sub_description">{{ sub.description }}</p>
|
<p id="sub_description">{{ sub.description }}</p>
|
||||||
<div id="sub_details">
|
<div id="sub_details">
|
||||||
@ -101,22 +101,22 @@
|
|||||||
<div id="sub_actions">
|
<div id="sub_actions">
|
||||||
<div id="sub_subscription">
|
<div id="sub_subscription">
|
||||||
{% if prefs.subscriptions.contains(sub.name) %}
|
{% 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>
|
<button class="unsubscribe">Unsubscribe</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% 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>
|
<button class="subscribe">Subscribe</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="sub_filter">
|
<div id="sub_filter">
|
||||||
{% if prefs.filters.contains(sub.name) %}
|
{% 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>
|
<button class="unfilter">Unfilter</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% 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>
|
<button class="filter">Filter</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -127,7 +127,7 @@
|
|||||||
<details class="panel" id="sidebar">
|
<details class="panel" id="sidebar">
|
||||||
<summary id="sidebar_label">Sidebar</summary>
|
<summary id="sidebar_label">Sidebar</summary>
|
||||||
<div id="sidebar_contents">
|
<div id="sidebar_contents">
|
||||||
{{ sub.info }}
|
{{ sub.info|safe }}
|
||||||
{# <hr>
|
{# <hr>
|
||||||
<h2>Moderators</h2>
|
<h2>Moderators</h2>
|
||||||
<br>
|
<br>
|
||||||
|
@ -16,9 +16,12 @@
|
|||||||
{% if !is_filtered %}
|
{% if !is_filtered %}
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form id="sort">
|
<form id="sort">
|
||||||
<select name="sort">
|
<div id="listing_options">
|
||||||
{% call utils::options(sort.0, ["hot", "new", "top"], "") %}
|
{% call utils::sort(["/user/", user.name.as_str()].concat(), ["overview", "comments", "submitted"], listing) %}
|
||||||
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
|
</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") %}
|
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||||
</select>{% endif %}<button id="sort_submit" class="submit">
|
</select>{% endif %}<button id="sort_submit" class="submit">
|
||||||
<svg width="15" viewBox="0 0 110 100" fill="none" stroke-width="10" stroke-linecap="round">
|
<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>
|
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<p class="comment_body">{{ post.body }}</p>
|
<p class="comment_body">{{ post.body|safe }}</p>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -63,11 +66,11 @@
|
|||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% 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 %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ends.1 != "" %}
|
{% 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 %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
@ -78,7 +81,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="panel" id="user">
|
<div class="panel" id="user">
|
||||||
<img loading="lazy" id="user_icon" src="{{ user.icon }}" alt="User icon">
|
<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>
|
<p id="user_name">u/{{ user.name }}</p>
|
||||||
<div id="user_description">{{ user.description }}</div>
|
<div id="user_description">{{ user.description }}</div>
|
||||||
<div id="user_details">
|
<div id="user_details">
|
||||||
@ -91,22 +94,22 @@
|
|||||||
{% let name = ["u_", user.name.as_str()].join("") %}
|
{% let name = ["u_", user.name.as_str()].join("") %}
|
||||||
<div id="user_subscription">
|
<div id="user_subscription">
|
||||||
{% if prefs.subscriptions.contains(name) %}
|
{% 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>
|
<button class="unsubscribe">Unfollow</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="/r/{{ name }}/subscribe" method="POST">
|
<form action="/r/{{ name }}/subscribe?redirect={{ redirect_url }}" method="POST">
|
||||||
<button class="subscribe">Follow</button>
|
<button class="subscribe">Follow</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div id="user_filter">
|
<div id="user_filter">
|
||||||
{% if prefs.filters.contains(name) %}
|
{% 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>
|
<button class="unfilter">Unfilter</button>
|
||||||
</form>
|
</form>
|
||||||
{% else %}
|
{% else %}
|
||||||
<form action="/r/{{ name }}/filter" method="POST">
|
<form action="/r/{{ name }}/filter?redirect={{ redirect_url }}" method="POST">
|
||||||
<button class="filter">Filter</button>
|
<button class="filter">Filter</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% macro options(current, values, default) -%}
|
{% macro options(current, values, default) -%}
|
||||||
{% for value in values %}
|
{% for value in values %}
|
||||||
<option value="{{ value }}" {% if current == value || (current == "" && value == default) %}selected{% endif %}>
|
<option value="{{ value }}" {% if current == value.to_string() || (current == "" && value.to_string() == default.to_string()) %}selected{% endif %}>
|
||||||
{{ format!("{}{}", value.get(0..1).unwrap_or_default().to_uppercase(), value.get(1..).unwrap_or_default()) }}
|
{{ format!("{}{}", value.get(0..1).unwrap_or_default().to_uppercase(), value.get(1..).unwrap_or_default()) }}
|
||||||
</option>
|
</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
{% macro sort(root, methods, selected) -%}
|
{% macro sort(root, methods, selected) -%}
|
||||||
{% for method in methods %}
|
{% for method in methods %}
|
||||||
<a {% if method == selected %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
|
<a {% if method.to_string() == selected.to_string() %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
|
||||||
{{ format!("{}{}", method.get(0..1).unwrap_or_default().to_uppercase(), method.get(1..).unwrap_or_default()) }}
|
{{ format!("{}{}", method.get(0..1).unwrap_or_default().to_uppercase(), method.get(1..).unwrap_or_default()) }}
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -19,7 +19,7 @@
|
|||||||
<input id="search" type="text" name="q" placeholder="Search" title="Search libreddit" value="{{ search }}">
|
<input id="search" type="text" name="q" placeholder="Search" title="Search libreddit" value="{{ search }}">
|
||||||
{% if root != "/r/" && !root.is_empty() %}
|
{% if root != "/r/" && !root.is_empty() %}
|
||||||
<div id="inside">
|
<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>
|
<label for="restrict_sr" class="search_label" title="Restrict search to this subreddit">in {{ root }}</label>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -34,11 +34,10 @@
|
|||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro render_flair(flair_parts) -%}
|
{% macro render_flair(flair_parts) -%}
|
||||||
{% for flair_part in flair_parts %}{% if flair_part.flair_part_type == "emoji" %}<span class="emoji" style="background-image:url('{{ flair_part.value }}');"></span>{% else if flair_part.flair_part_type == "text" && !flair_part.value.is_empty() %}<span>{{ flair_part.value }}</span>{% endif %}{% endfor %}
|
{% for flair_part in flair_parts.clone() %}{% if flair_part.flair_part_type == "emoji" %}<span class="emoji" style="background-image:url('{{ flair_part.value }}');"></span>{% else if flair_part.flair_part_type == "text" && !flair_part.value.is_empty() %}<span>{{ flair_part.value }}</span>{% endif %}{% endfor %}
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro sub_list(current) -%}
|
{% macro sub_list(current) -%}
|
||||||
{% if prefs.subscriptions.len() > 0 %}
|
|
||||||
<details id="feeds">
|
<details id="feeds">
|
||||||
<summary>Feeds</summary>
|
<summary>Feeds</summary>
|
||||||
<div id="feed_list">
|
<div id="feed_list">
|
||||||
@ -46,13 +45,14 @@
|
|||||||
<a href="/">Home</a>
|
<a href="/">Home</a>
|
||||||
<a href="/r/popular">Popular</a>
|
<a href="/r/popular">Popular</a>
|
||||||
<a href="/r/all">All</a>
|
<a href="/r/all">All</a>
|
||||||
<p>REDDIT FEEDS</p>
|
{% if prefs.subscriptions.len() > 0 %}
|
||||||
{% for sub in prefs.subscriptions %}
|
<p>REDDIT FEEDS</p>
|
||||||
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a>
|
{% for sub in prefs.subscriptions %}
|
||||||
{% endfor %}
|
<a href="/r/{{ sub }}" {% if sub == current %}class="selected"{% endif %}>{{ sub }}</a>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
{% endif %}
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
{% macro render_hls_notification(redirect_url) -%}
|
{% macro render_hls_notification(redirect_url) -%}
|
||||||
@ -72,7 +72,7 @@
|
|||||||
{% endif -%}
|
{% endif -%}
|
||||||
<a class="post_subreddit" href="/{{ community }}">{{ community }}</a>
|
<a class="post_subreddit" href="/{{ community }}">{{ community }}</a>
|
||||||
<span class="dot">•</span>
|
<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="dot">•</span>
|
||||||
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
<span class="created" title="{{ post.created }}">{{ post.rel_time }}</span>
|
||||||
{% if !post.awards.is_empty() %}
|
{% if !post.awards.is_empty() %}
|
||||||
@ -83,7 +83,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
<p class="post_title">
|
<h2 class="post_title">
|
||||||
{% if post.flair.flair_parts.len() > 0 %}
|
{% 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"
|
<a href="/r/{{ post.community }}/search?q=flair_name%3A%22{{ post.flair.text }}%22&restrict_sr=on"
|
||||||
class="post_flair"
|
class="post_flair"
|
||||||
@ -91,7 +91,7 @@
|
|||||||
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
dir="ltr">{% call render_flair(post.flair.flair_parts) %}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
<a href="{{ post.permalink }}">{{ post.title }}</a>{% if post.flags.nsfw %} <small class="nsfw">NSFW</small>{% endif %}
|
||||||
</p>
|
</h2>
|
||||||
<!-- POST MEDIA/THUMBNAIL -->
|
<!-- POST MEDIA/THUMBNAIL -->
|
||||||
{% if (prefs.layout.is_empty() || prefs.layout == "card") && post.post_type == "image" %}
|
{% 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 %}" >
|
<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_score" title="{{ post.score.1 }}">{{ post.score.0 }}<span class="label"> Upvotes</span></div>
|
||||||
<div class="post_body post_preview">
|
<div class="post_body post_preview">
|
||||||
{{ post.body }}
|
{{ post.body|safe }}
|
||||||
</div>
|
</div>
|
||||||
<div class="post_footer">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
@ -22,8 +22,8 @@
|
|||||||
<div>Wiki</div>
|
<div>Wiki</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="wiki">
|
<div id="wiki">
|
||||||
{{ wiki }}
|
{{ wiki|safe }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
Reference in New Issue
Block a user