Compare commits
70 Commits
Author | SHA1 | Date | |
---|---|---|---|
fac56d7f87 | |||
ef1ad17234 | |||
b8cdc605a2 | |||
ef2f9ad12b | |||
b13874d0db | |||
3d142afd03 | |||
7fcb7fcfed | |||
747d5a7c67 | |||
770c4d3630 | |||
e7b448a282 | |||
c7c787dff1 | |||
59a34a0e85 | |||
6e8cf69227 | |||
3444989f9a | |||
7e96bb3d80 | |||
0adbb1556e | |||
710eecdb9d | |||
8a57fa8a1d | |||
b33d79ed9b | |||
0f506fc41b | |||
c9cd825d55 | |||
e63384e6a6 | |||
3260a4d596 | |||
da5c4603d9 | |||
b50fa6f3ae | |||
aa7b4b2af7 | |||
2b0193f5ea | |||
2185d895c0 | |||
9c1a932214 | |||
8c0269af1c | |||
df89c5076e | |||
f819ad2bc6 | |||
f5884a5270 | |||
c046d00060 | |||
5934e34ea0 | |||
463b44ac52 | |||
b40d21e559 | |||
a422a74747 | |||
4124fa87d3 | |||
1dd0c4ee20 | |||
0dd114c166 | |||
67090e9b08 | |||
d97fb49fde | |||
9263b0657f | |||
a3384cbaa6 | |||
5d26b5c764 | |||
516403ee47 | |||
5ea504e6e8 | |||
f49bff9853 | |||
4ec529cdb8 | |||
779de6f8af | |||
0925a9b334 | |||
2f2ed6169d | |||
59ef30c76d | |||
d43b49e7e4 | |||
64a92195dd | |||
a7925ed62d | |||
39ba50dada | |||
bc1b29246d | |||
2d77a91150 | |||
93c1db502d | |||
a6dc7ee043 | |||
c7282520cd | |||
a866c1d068 | |||
aa9aad6743 | |||
f65ee2eb6a | |||
44c4341e67 | |||
1c886f8003 | |||
b481d26be2 | |||
f00ef59404 |
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
370
Cargo.lock
generated
370
Cargo.lock
generated
@ -75,7 +75,7 @@ dependencies = [
|
|||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project 1.0.2",
|
"pin-project 1.0.3",
|
||||||
"rand",
|
"rand",
|
||||||
"regex",
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
@ -83,7 +83,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sha-1",
|
"sha-1",
|
||||||
"slab",
|
"slab",
|
||||||
"time 0.2.23",
|
"time",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -92,8 +92,8 @@ version = "0.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
|
checksum = "b4ca8ce00b267af8ccebbd647de0d61e0674b6e61185cc7a592ff88772bed655"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -247,14 +247,14 @@ dependencies = [
|
|||||||
"fxhash",
|
"fxhash",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
"pin-project 1.0.2",
|
"pin-project 1.0.3",
|
||||||
"regex",
|
"regex",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"socket2",
|
"socket2",
|
||||||
"time 0.2.23",
|
"time",
|
||||||
"tinyvec",
|
"tinyvec",
|
||||||
"url",
|
"url",
|
||||||
]
|
]
|
||||||
@ -265,16 +265,16 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
|
checksum = "ad26f77093333e0e7c6ffe54ebe3582d908a104e448723eec6d43d08b07143fb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.14.0"
|
version = "0.14.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c0929d69e78dd9bf5408269919fcbcaeb2e35e5d43e5815517cdc6a8e11a423"
|
checksum = "a55f82cfe485775d02112886f4169bde0c5894d75e79ead7eafe7e40a25e45f7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gimli",
|
"gimli",
|
||||||
]
|
]
|
||||||
@ -295,10 +295,16 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama"
|
name = "arrayvec"
|
||||||
version = "0.8.0"
|
version = "0.5.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3dc2a4b6d7f812d2b13d251ae792caecebd635d6401761162d4b71d5ebe1a010"
|
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "askama"
|
||||||
|
version = "0.10.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d298738b6e47e1034e560e5afe63aa488fea34e25ec11b855a76f0d7b8e73134"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama_derive",
|
"askama_derive",
|
||||||
"askama_escape",
|
"askama_escape",
|
||||||
@ -307,34 +313,36 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_derive"
|
name = "askama_derive"
|
||||||
version = "0.8.0"
|
version = "0.10.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "23ee2fff0f22ad5d215cace1227cd036c28e81e26206763bb837b6d0e766c87d"
|
checksum = "ca2925c4c290382f9d2fa3d1c1b6a63fa1427099721ecca4749b154cc9c25522"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama_shared",
|
"askama_shared",
|
||||||
"nom",
|
"proc-macro2",
|
||||||
"proc-macro2 0.4.30",
|
"syn",
|
||||||
"quote 0.6.13",
|
|
||||||
"syn 0.15.44",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_escape"
|
name = "askama_escape"
|
||||||
version = "0.2.0"
|
version = "0.10.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b0de942230b5beedaa9e1d64df5b76fa1c97002e4c7982897be899cccf40621d"
|
checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "askama_shared"
|
name = "askama_shared"
|
||||||
version = "0.8.0"
|
version = "0.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a6dfa6b6d254fd066a8bbed9a8f913123e3f701db89216ad4f0aff04ad87718c"
|
checksum = "2582b77e0f3c506ec4838a25fa8a5f97b9bed72bb6d3d272ea1c031d8bd373bc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"askama_escape",
|
"askama_escape",
|
||||||
"humansize",
|
"humansize",
|
||||||
|
"nom",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
|
"percent-encoding",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"syn",
|
||||||
"toml",
|
"toml",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -344,9 +352,9 @@ version = "0.3.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee"
|
checksum = "e5444eec77a9ec2bfe4524139e09195862e981400c4358d3b760cae634e4c4ee"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -355,9 +363,9 @@ version = "0.1.42"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
|
checksum = "8d3a45e77e34375a7923b1e8febb049bb011f064714a8e17a1a616fef01da13d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -429,6 +437,18 @@ version = "1.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitvec"
|
||||||
|
version = "0.19.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7ba35e9565969edb811639dbebfe34edc0368e472c5018474c8eb2543397f81"
|
||||||
|
dependencies = [
|
||||||
|
"funty",
|
||||||
|
"radium",
|
||||||
|
"tap",
|
||||||
|
"wyz",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@ -503,24 +523,11 @@ version = "1.0.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "chrono"
|
|
||||||
version = "0.4.19"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"num-integer",
|
|
||||||
"num-traits",
|
|
||||||
"time 0.1.44",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "const_fn"
|
name = "const_fn"
|
||||||
version = "0.4.4"
|
version = "0.4.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd51eab21ab4fd6a3bf889e2d0958c0a6e3a61ad04260325e919e652a2a62826"
|
checksum = "28b9d6de7f49e22cf97ad17fc4036ece69300032f45f78f30b4a4482cdc3f4a6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cookie"
|
name = "cookie"
|
||||||
@ -529,8 +536,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f"
|
checksum = "784ad0fbab4f3e9cef09f20e0aea6000ae08d2cb98ac4c0abc53df18803d702f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"time 0.2.23",
|
"time",
|
||||||
"version_check 0.9.2",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -560,9 +567,9 @@ version = "0.99.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
|
checksum = "41cb0e6161ad61ed084a36ba71fbba9e3ac5aee3606fb607fe08da6acbcf3d8c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -602,9 +609,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
|
checksum = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -651,6 +658,12 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "funty"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures"
|
name = "futures"
|
||||||
version = "0.3.8"
|
version = "0.3.8"
|
||||||
@ -694,9 +707,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
|
checksum = "77408a692f1f97bcc61dc001d752e00643408fbc922e4d634c655df50d595556"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-hack",
|
"proc-macro-hack",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -727,7 +740,7 @@ dependencies = [
|
|||||||
"futures-sink",
|
"futures-sink",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"memchr",
|
"memchr",
|
||||||
"pin-project 1.0.2",
|
"pin-project 1.0.3",
|
||||||
"pin-utils",
|
"pin-utils",
|
||||||
"proc-macro-hack",
|
"proc-macro-hack",
|
||||||
"proc-macro-nested",
|
"proc-macro-nested",
|
||||||
@ -750,18 +763,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typenum",
|
"typenum",
|
||||||
"version_check 0.9.2",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.1.15"
|
version = "0.1.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fc587bc0ec293155d5bfa6b9891ec18a1e330c234f896ea47fbada4cadbe47e6"
|
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 0.1.10",
|
"cfg-if 1.0.0",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.9.0+wasi-snapshot-preview1",
|
"wasi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -880,7 +893,7 @@ dependencies = [
|
|||||||
"httparse",
|
"httparse",
|
||||||
"httpdate",
|
"httpdate",
|
||||||
"itoa",
|
"itoa",
|
||||||
"pin-project 1.0.2",
|
"pin-project 1.0.3",
|
||||||
"socket2",
|
"socket2",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
@ -999,23 +1012,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "lexical-core"
|
||||||
version = "0.2.81"
|
version = "0.7.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1482821306169ec4d07f6aca392a4681f66c75c9918aa49641a2595db64053cb"
|
checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616"
|
||||||
|
dependencies = [
|
||||||
|
"arrayvec",
|
||||||
|
"bitflags",
|
||||||
|
"cfg-if 0.1.10",
|
||||||
|
"ryu",
|
||||||
|
"static_assertions",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.82"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89203f3fba0a3795506acaad8ebce3c80c0af93f994d5a1d7a0b1eeb23271929"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libreddit"
|
name = "libreddit"
|
||||||
version = "0.2.3"
|
version = "0.2.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"askama",
|
"askama",
|
||||||
"async-recursion",
|
"async-recursion",
|
||||||
"base64 0.13.0",
|
"base64 0.13.0",
|
||||||
"chrono",
|
"regex",
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"time",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1150,22 +1178,14 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "nom"
|
name = "nom"
|
||||||
version = "4.2.3"
|
version = "6.0.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6"
|
checksum = "88034cfd6b4a0d54dd14f4a507eceee36c0b70e5a02236c4e4df571102be17f0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bitvec",
|
||||||
|
"lexical-core",
|
||||||
"memchr",
|
"memchr",
|
||||||
"version_check 0.1.5",
|
"version_check",
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "num-integer"
|
|
||||||
version = "0.1.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
|
|
||||||
dependencies = [
|
|
||||||
"autocfg",
|
|
||||||
"num-traits",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1247,11 +1267,11 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project"
|
name = "pin-project"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ccc2237c2c489783abd8c4c80e5450fc0e98644555b1364da68cc29aa151ca7"
|
checksum = "5a83804639aad6ba65345661744708855f9fbcb71176ea8d28d05aeb11d975e7"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"pin-project-internal 1.0.2",
|
"pin-project-internal 1.0.3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1260,20 +1280,20 @@ version = "0.4.27"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
|
checksum = "65ad2ae56b6abe3a1ee25f15ee605bacadb9a764edaba9c2bf4103800d4a1895"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-internal"
|
name = "pin-project-internal"
|
||||||
version = "1.0.2"
|
version = "1.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8e8d2bf0b23038a4424865103a4df472855692821aab4e4f5c3312d461d9e5f"
|
checksum = "b7bcc46b8f73443d15bc1c5fecbb315718491fa9187fa483f0e359323cde8b3a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1284,9 +1304,9 @@ checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-project-lite"
|
name = "pin-project-lite"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6b063f57ec186e6140e2b8b6921e5f1bd89c7356dda5b33acc5401203ca6131c"
|
checksum = "e36743d754ccdf9954c2e352ce2d4b106e024c814f6499c2dadff80da9a442d8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pin-utils"
|
name = "pin-utils"
|
||||||
@ -1312,22 +1332,13 @@ version = "0.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "proc-macro2"
|
|
||||||
version = "0.4.30"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
|
|
||||||
dependencies = [
|
|
||||||
"unicode-xid 0.1.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"unicode-xid 0.2.1",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1336,24 +1347,21 @@ version = "1.2.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "quote"
|
|
||||||
version = "0.6.13"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 0.4.30",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.8"
|
version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
checksum = "991431c3519a3f36861882da93630ce66b52918dcf1b8e2fd66b397fc96f28df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radium"
|
||||||
|
version = "0.5.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "941ba9d78d8e2f7ce474c015eea4d9c6d25b6a3327f9832ee29a4de27f91bbb8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rand"
|
name = "rand"
|
||||||
version = "0.7.3"
|
version = "0.7.3"
|
||||||
@ -1441,7 +1449,7 @@ dependencies = [
|
|||||||
"mime",
|
"mime",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite 0.2.0",
|
"pin-project-lite 0.2.1",
|
||||||
"rustls",
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
@ -1560,9 +1568,9 @@ version = "1.0.118"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
checksum = "c84d3526699cd55261af4b941e4e725444df67aa4f9e6a3564f18030d12672df"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1624,9 +1632,9 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "smallvec"
|
name = "smallvec"
|
||||||
version = "1.5.1"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ae524f056d7d770e174287294f562e95044c68e88dec909a00d2094805db9d75"
|
checksum = "1a55ca5f3b68e41c979bf8c46a6f1da892ca4db8f94023ce0bd32407573b1ac0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "socket2"
|
name = "socket2"
|
||||||
@ -1647,13 +1655,19 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "standback"
|
name = "standback"
|
||||||
version = "0.2.13"
|
version = "0.2.14"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf906c8b8fc3f6ecd1046e01da1d8ddec83e48c8b08b84dcc02b585a6bedf5a8"
|
checksum = "c66a8cff4fa24853fdf6b51f75c6d7f8206d7c75cab4e467bcd7f25c2b1febe0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"version_check 0.9.2",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "static_assertions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "stdweb"
|
name = "stdweb"
|
||||||
version = "0.4.20"
|
version = "0.4.20"
|
||||||
@ -1674,11 +1688,11 @@ version = "0.5.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
|
checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1688,13 +1702,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
|
checksum = "58fa5ff6ad0d98d1ffa8cb115892b6e69d67799f6763e162a1c9db421dc22e11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base-x",
|
"base-x",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha1",
|
"sha1",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1705,25 +1719,20 @@ checksum = "213701ba3370744dcd1a12960caa4843b3d68b4d1c0a5d575e0d65b2ee9d16c0"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "0.15.44"
|
version = "1.0.58"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
|
checksum = "cc60a3d73ea6594cd712d830cc1f0390fd71542d8c8cd24e70cc54cdfd5e05d5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 0.4.30",
|
"proc-macro2",
|
||||||
"quote 0.6.13",
|
"quote",
|
||||||
"unicode-xid 0.1.0",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "tap"
|
||||||
version = "1.0.56"
|
version = "1.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a9802ddde94170d186eeee5005b798d9c159fa970403f1be19976d0cfb939b72"
|
checksum = "36474e732d1affd3a6ed582781b3683df3d0563714c59c39591e8ff707cf078e"
|
||||||
dependencies = [
|
|
||||||
"proc-macro2 1.0.24",
|
|
||||||
"quote 1.0.8",
|
|
||||||
"unicode-xid 0.2.1",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
@ -1740,16 +1749,16 @@ version = "1.0.23"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
checksum = "9be73a2caec27583d0046ef3796c3794f868a5bc813db689eed00c7631275cd1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thread_local"
|
name = "thread_local"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14"
|
checksum = "bb9bc092d0d51e76b2b19d9d85534ffc9ec2db959a2523cdae0697e2972cd447"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
]
|
]
|
||||||
@ -1763,17 +1772,6 @@ dependencies = [
|
|||||||
"num_cpus",
|
"num_cpus",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "time"
|
|
||||||
version = "0.1.44"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
|
||||||
"winapi 0.3.9",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "time"
|
name = "time"
|
||||||
version = "0.2.23"
|
version = "0.2.23"
|
||||||
@ -1785,7 +1783,7 @@ dependencies = [
|
|||||||
"standback",
|
"standback",
|
||||||
"stdweb",
|
"stdweb",
|
||||||
"time-macros",
|
"time-macros",
|
||||||
"version_check 0.9.2",
|
"version_check",
|
||||||
"winapi 0.3.9",
|
"winapi 0.3.9",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1806,10 +1804,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
|
checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-hack",
|
"proc-macro-hack",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"standback",
|
"standback",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1876,9 +1874,9 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.4.10"
|
version = "0.5.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "758664fc71a3a69038656bee8b6be6477d2a6c315a6b81f7081f591bffa4111f"
|
checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@ -1897,7 +1895,7 @@ checksum = "9f47026cdc4080c07e49b37087de021820269d996f581aac150ef9e5583eefe3"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if 1.0.0",
|
"cfg-if 1.0.0",
|
||||||
"log",
|
"log",
|
||||||
"pin-project-lite 0.2.0",
|
"pin-project-lite 0.2.1",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -1978,7 +1976,7 @@ version = "2.6.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"version_check 0.9.2",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -2005,12 +2003,6 @@ version = "1.7.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-xid"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-xid"
|
name = "unicode-xid"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
@ -2035,12 +2027,6 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version_check"
|
|
||||||
version = "0.1.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.2"
|
version = "0.9.2"
|
||||||
@ -2063,12 +2049,6 @@ version = "0.9.0+wasi-snapshot-preview1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "wasi"
|
|
||||||
version = "0.10.0+wasi-snapshot-preview1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-bindgen"
|
name = "wasm-bindgen"
|
||||||
version = "0.2.69"
|
version = "0.2.69"
|
||||||
@ -2090,9 +2070,9 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
"log",
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2114,7 +2094,7 @@ version = "0.2.69"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
|
checksum = "7a6ac8995ead1f084a8dea1e65f194d0973800c7f571f6edd70adf06ecf77084"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"wasm-bindgen-macro-support",
|
"wasm-bindgen-macro-support",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -2124,9 +2104,9 @@ version = "0.2.69"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
|
checksum = "b5a48c72f299d80557c7c62e37e7225369ecc0c963964059509fbafe917c7549"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2 1.0.24",
|
"proc-macro2",
|
||||||
"quote 1.0.8",
|
"quote",
|
||||||
"syn 1.0.56",
|
"syn",
|
||||||
"wasm-bindgen-backend",
|
"wasm-bindgen-backend",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
@ -2233,3 +2213,9 @@ dependencies = [
|
|||||||
"winapi 0.2.8",
|
"winapi 0.2.8",
|
||||||
"winapi-build",
|
"winapi-build",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wyz"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214"
|
||||||
|
18
Cargo.toml
18
Cargo.toml
@ -3,20 +3,18 @@ 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.2.3"
|
version = "0.2.6"
|
||||||
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
authors = ["spikecodes <19519553+spikecodes@users.noreply.github.com>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["proxy"]
|
|
||||||
proxy = ["actix-web/rustls", "base64"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
base64 = { version = "0.13.0", optional = true }
|
base64 = "0.13.0"
|
||||||
actix-web = "3.2.0"
|
actix-web = { version = "3.3.2", features = ["rustls"] }
|
||||||
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
|
reqwest = { version = "0.10", default_features = false, features = ["rustls-tls"] }
|
||||||
askama = "0.8.0"
|
askama = "0.10.5"
|
||||||
serde = "1.0.117"
|
serde = { version = "1.0.118", default_features = false, features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
chrono = "0.4.19"
|
|
||||||
async-recursion = "0.3.1"
|
async-recursion = "0.3.1"
|
||||||
|
url = "2.2.0"
|
||||||
|
regex = "1.4.2"
|
||||||
|
time = "0.2.23"
|
21
README.md
21
README.md
@ -7,7 +7,6 @@ Libre + Reddit = [Libreddit](https://libredd.it)
|
|||||||
- 🚀 Fast: written in Rust for blazing fast speeds and safety
|
- 🚀 Fast: written in Rust for blazing fast speeds and safety
|
||||||
- ☁️ Light: no JavaScript, no ads, no tracking
|
- ☁️ Light: no JavaScript, no ads, no tracking
|
||||||
- 🕵 Private: all requests are proxied through the server, including media
|
- 🕵 Private: all requests are proxied through the server, including media
|
||||||
- 🦺 Safe: does not rely on Reddit OAuth or require a Reddit API Key
|
|
||||||
- 🔒 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
|
||||||
|
|
||||||
Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libredd.it/r/unpopularopinion) without being [tracked](#reddit).
|
Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the coldest takes of [r/unpopularopinion](https://libredd.it/r/unpopularopinion) without being [tracked](#reddit).
|
||||||
@ -18,7 +17,6 @@ Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the
|
|||||||
- [About](#about)
|
- [About](#about)
|
||||||
- [Elsewhere](#elsewhere)
|
- [Elsewhere](#elsewhere)
|
||||||
- [Info](#info)
|
- [Info](#info)
|
||||||
- [In Progress](#in-progress)
|
|
||||||
- [Teddit Comparison](#how-does-it-compare-to-teddit)
|
- [Teddit Comparison](#how-does-it-compare-to-teddit)
|
||||||
- [Comparison](#comparison)
|
- [Comparison](#comparison)
|
||||||
- [Speed](#speed)
|
- [Speed](#speed)
|
||||||
@ -35,7 +33,7 @@ Like [Invidious](https://github.com/iv-org/invidious) but for Reddit. Browse the
|
|||||||
|
|
||||||
## Screenshot
|
## Screenshot
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Instances
|
## Instances
|
||||||
|
|
||||||
@ -47,6 +45,8 @@ Feel free to [open an issue](https://github.com/spikecodes/libreddit/issues/new)
|
|||||||
| [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) | 🇺🇸 US | ✅ |
|
| [libreddit.dothq.co](https://libreddit.dothq.co) | 🇺🇸 US | ✅ |
|
||||||
| [libreddit.insanity.wtf](https://libreddit.insanity.wtf) | 🇺🇸 US | ✅ |
|
| [libreddit.insanity.wtf](https://libreddit.insanity.wtf) | 🇺🇸 US | ✅ |
|
||||||
|
| [libreddit.kavin.rocks](https://libreddit.kavin.rocks) | 🇮🇳 IN | ✅ |
|
||||||
|
| [spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion](http://spjmllawtheisznfs7uryhxumin26ssv2draj7oope3ok3wuhy43eoyd.onion) | 🇮🇳 IN | |
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@ -62,10 +62,7 @@ Find Libreddit on...
|
|||||||
### Info
|
### Info
|
||||||
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
Libreddit hopes to provide an easier way to browse Reddit, without the ads, trackers, and bloat. Libreddit was inspired by other alternative front-ends to popular services such as [Invidious](https://github.com/iv-org/invidious) for YouTube, [Nitter](https://github.com/zedeus/nitter) for Twitter, and [Bibliogram](https://sr.ht/~cadence/bibliogram/) for Instagram.
|
||||||
|
|
||||||
Libreddit currently implements most of Reddit's functionalities but still lacks a few features that are being worked on below.
|
Libreddit currently implements most of Reddit's (signed-out) functionalities but still lacks [a few features](https://github.com/spikecodes/libreddit/issues).
|
||||||
|
|
||||||
### In Progress
|
|
||||||
- Searching
|
|
||||||
|
|
||||||
### How does it compare to Teddit?
|
### How does it compare to Teddit?
|
||||||
|
|
||||||
@ -74,7 +71,6 @@ Teddit is another awesome open source project designed to provide an alternative
|
|||||||
If you are looking to compare, the biggest differences I have noticed are:
|
If you are looking to compare, the biggest differences I have noticed are:
|
||||||
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
- Libreddit is themed around Reddit's redesign whereas Teddit appears to stick much closer to Reddit's old design. This may suit some users better as design is always subjective.
|
||||||
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
- Libreddit is written in [Rust](https://www.rust-lang.org) for speed and memory safety. It uses [Actix Web](https://actix.rs), which was [benchmarked as the fastest web server for single queries](https://www.techempower.com/benchmarks/#hw=ph&test=db).
|
||||||
- Unlike Teddit (at the time of writing this), Libreddit does not require a Reddit API key to host.
|
|
||||||
|
|
||||||
## Comparison
|
## Comparison
|
||||||
|
|
||||||
@ -127,11 +123,11 @@ Results from Google Lighthouse ([Libreddit Report](https://lighthouse-dot-webdot
|
|||||||
|
|
||||||
For transparency, I hope to describe all the ways Libreddit handles user privacy.
|
For transparency, I hope to describe all the ways Libreddit handles user privacy.
|
||||||
|
|
||||||
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs fetched to aid troubleshooting but nothing else.
|
**Logging:** In production (when running the binary, hosting with docker, or using the official instances), Libreddit logs nothing. When debugging (running from source without `--release`), Libreddit logs post IDs and URL paths fetched to aid troubleshooting but nothing else.
|
||||||
|
|
||||||
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
|
**DNS:** Both official domains (`libredd.it` and `libreddit.spike.codes`) use Cloudflare as the DNS resolver. Though, the sites are not proxied through Cloudflare meaning Cloudflare doesn't have access to user traffic.
|
||||||
|
|
||||||
**Cookies:** Libreddit uses no cookies currently but eventually, I plan to add a configuration page where users can store an optional cookie to save their preferred theme, default sorting algorithm, or default layout.
|
**Cookies:** Libreddit uses optional cookies to store any configured settings in [the settings menu](https://libredd.it/settings). This is not a cross-site cookie and the cookie holds no personal data, only a value of the possible layout.
|
||||||
|
|
||||||
**Hosting:** The official instances (`libredd.it` and `libreddit.spike.codes`) are hosted on [Repl.it](https://repl.it/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting and browsing through Tor are welcomed.
|
**Hosting:** The official instances (`libredd.it` and `libreddit.spike.codes`) are hosted on [Repl.it](https://repl.it/) which monitors usage to prevent abuse. I can understand if this invalidates certain users' threat models and therefore, selfhosting and browsing through Tor are welcomed.
|
||||||
|
|
||||||
@ -194,11 +190,6 @@ Specify a custom address for the server by passing the `-a` or `--address` argum
|
|||||||
libreddit --address=0.0.0.0:8111
|
libreddit --address=0.0.0.0:8111
|
||||||
```
|
```
|
||||||
|
|
||||||
To disable the media proxy built into Libreddit, run:
|
|
||||||
```
|
|
||||||
libreddit --no-default-features
|
|
||||||
```
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
```
|
```
|
||||||
|
45
src/main.rs
45
src/main.rs
@ -2,9 +2,10 @@
|
|||||||
use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServer};
|
use actix_web::{get, middleware::NormalizePath, web, App, HttpResponse, HttpServer};
|
||||||
|
|
||||||
// Reference local files
|
// Reference local files
|
||||||
mod popular;
|
|
||||||
mod post;
|
mod post;
|
||||||
mod proxy;
|
mod proxy;
|
||||||
|
mod search;
|
||||||
|
mod settings;
|
||||||
mod subreddit;
|
mod subreddit;
|
||||||
mod user;
|
mod user;
|
||||||
mod utils;
|
mod utils;
|
||||||
@ -15,7 +16,9 @@ async fn style() -> HttpResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn robots() -> HttpResponse {
|
async fn robots() -> HttpResponse {
|
||||||
HttpResponse::Ok().body(include_str!("../static/robots.txt"))
|
HttpResponse::Ok()
|
||||||
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
|
.body(include_str!("../static/robots.txt"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/favicon.ico")]
|
#[get("/favicon.ico")]
|
||||||
@ -31,39 +34,53 @@ async fn main() -> std::io::Result<()> {
|
|||||||
if args.len() > 1 {
|
if args.len() > 1 {
|
||||||
for arg in args {
|
for arg in args {
|
||||||
if arg.starts_with("--address=") || arg.starts_with("-a=") {
|
if arg.starts_with("--address=") || arg.starts_with("-a=") {
|
||||||
let split: Vec<&str> = arg.split("=").collect();
|
address = arg.split('=').collect::<Vec<&str>>()[1].to_string();
|
||||||
address = split[1].to_string();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// start http server
|
// start http server
|
||||||
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), address.clone());
|
println!("Running Libreddit v{} on {}!", env!("CARGO_PKG_VERSION"), &address);
|
||||||
|
|
||||||
HttpServer::new(|| {
|
HttpServer::new(|| {
|
||||||
App::new()
|
App::new()
|
||||||
// TRAILING SLASH MIDDLEWARE
|
// TRAILING SLASH MIDDLEWARE
|
||||||
.wrap(NormalizePath::default())
|
.wrap(NormalizePath::default())
|
||||||
|
// DEFAULT SERVICE
|
||||||
|
.default_service(web::get().to(|| utils::error("Nothing here".to_string())))
|
||||||
// GENERAL SERVICES
|
// GENERAL SERVICES
|
||||||
.route("/style.css/", web::get().to(style))
|
.route("/style.css/", web::get().to(style))
|
||||||
.route("/favicon.ico/", web::get().to(|| HttpResponse::Ok()))
|
.route("/favicon.ico/", web::get().to(HttpResponse::Ok))
|
||||||
.route("/robots.txt/", web::get().to(robots))
|
.route("/robots.txt/", web::get().to(robots))
|
||||||
|
// SETTINGS SERVICE
|
||||||
|
.route("/settings/", web::get().to(settings::get))
|
||||||
|
.route("/settings/", web::post().to(settings::set))
|
||||||
// PROXY SERVICE
|
// PROXY SERVICE
|
||||||
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
|
.route("/proxy/{url:.*}/", web::get().to(proxy::handler))
|
||||||
|
// SEARCH SERVICES
|
||||||
|
.route("/search/", web::get().to(search::find))
|
||||||
|
.route("r/{sub}/search/", web::get().to(search::find))
|
||||||
// USER SERVICES
|
// USER SERVICES
|
||||||
.route("/u/{username}/", web::get().to(user::page))
|
.route("/u/{username}/", web::get().to(user::profile))
|
||||||
.route("/user/{username}/", web::get().to(user::page))
|
.route("/user/{username}/", web::get().to(user::profile))
|
||||||
|
// WIKI SERVICES
|
||||||
|
.route("/wiki/", web::get().to(subreddit::wiki))
|
||||||
|
.route("/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||||
|
.route("/r/{sub}/wiki/", web::get().to(subreddit::wiki))
|
||||||
|
.route("/r/{sub}/wiki/{page}/", web::get().to(subreddit::wiki))
|
||||||
// SUBREDDIT SERVICES
|
// SUBREDDIT SERVICES
|
||||||
.route("/r/{sub}/", web::get().to(subreddit::page))
|
.route("/r/{sub}/", web::get().to(subreddit::page))
|
||||||
|
.route("/r/{sub}/{sort:hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||||
// POPULAR SERVICES
|
// POPULAR SERVICES
|
||||||
.route("/", web::get().to(popular::page))
|
.route("/", web::get().to(subreddit::page))
|
||||||
|
.route("/{sort:best|hot|new|top|rising|controversial}/", web::get().to(subreddit::page))
|
||||||
// POST SERVICES
|
// POST SERVICES
|
||||||
.route("/{id:.{5,6}}/", web::get().to(post::short))
|
.route("/{id:.{5,6}}/", web::get().to(post::item))
|
||||||
.route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::page))
|
.route("/r/{sub}/comments/{id}/{title}/", web::get().to(post::item))
|
||||||
.route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::comment))
|
.route("/r/{sub}/comments/{id}/{title}/{comment_id}/", web::get().to(post::item))
|
||||||
})
|
})
|
||||||
.bind(address.clone())
|
.bind(&address)
|
||||||
.expect(format!("Cannot bind to the address: {}", address).as_str())
|
.unwrap_or_else(|_| panic!("Cannot bind to the address: {}", address))
|
||||||
.run()
|
.run()
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
// CRATES
|
|
||||||
use crate::utils::{fetch_posts, ErrorTemplate, Params, Post};
|
|
||||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
|
||||||
use askama::Template;
|
|
||||||
|
|
||||||
// STRUCTS
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "popular.html", escape = "none")]
|
|
||||||
struct PopularTemplate {
|
|
||||||
posts: Vec<Post>,
|
|
||||||
sort: String,
|
|
||||||
ends: (String, String),
|
|
||||||
}
|
|
||||||
|
|
||||||
// RENDER
|
|
||||||
async fn render(sub_name: String, sort: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
|
|
||||||
let sorting = sort.unwrap_or("hot".to_string());
|
|
||||||
let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before
|
|
||||||
|
|
||||||
// Build the Reddit JSON API url
|
|
||||||
let url = match ends.0 {
|
|
||||||
Some(val) => format!("r/{}/{}.json?before={}&count=25", sub_name, sorting, val),
|
|
||||||
None => match ends.1 {
|
|
||||||
Some(val) => format!("r/{}/{}.json?after={}&count=25", sub_name, sorting, val),
|
|
||||||
None => format!("r/{}/{}.json", sub_name, sorting),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let items_result = fetch_posts(url, String::new()).await;
|
|
||||||
|
|
||||||
if items_result.is_err() {
|
|
||||||
let s = ErrorTemplate {
|
|
||||||
message: items_result.err().unwrap().to_string(),
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
|
||||||
} else {
|
|
||||||
let items = items_result.unwrap();
|
|
||||||
|
|
||||||
let s = PopularTemplate {
|
|
||||||
posts: items.0,
|
|
||||||
sort: sorting,
|
|
||||||
ends: (before, items.1),
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// SERVICES
|
|
||||||
pub async fn page(params: web::Query<Params>) -> Result<HttpResponse> {
|
|
||||||
render("popular".to_string(), params.sort.clone(), (params.before.clone(), params.after.clone())).await
|
|
||||||
}
|
|
182
src/post.rs
182
src/post.rs
@ -1,11 +1,11 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{format_num, format_url, request, val, Comment, ErrorTemplate, Flair, Params, Post};
|
use crate::utils::{cookie, error, format_num, format_url, media, param, request, rewrite_url, val, Comment, Flags, Flair, Post};
|
||||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
|
|
||||||
use async_recursion::async_recursion;
|
use async_recursion::async_recursion;
|
||||||
|
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::{TimeZone, Utc};
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -16,131 +16,95 @@ struct PostTemplate {
|
|||||||
sort: String,
|
sort: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(id: String, sort: Option<String>, comment_id: Option<String>) -> Result<HttpResponse> {
|
pub async fn item(req: HttpRequest) -> HttpResponse {
|
||||||
|
// Build Reddit API path
|
||||||
|
let mut path: String = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||||
|
|
||||||
|
// Set sort to sort query parameter
|
||||||
|
let mut sort: String = param(&path, "sort");
|
||||||
|
|
||||||
|
// Grab default comment sort method from Cookies
|
||||||
|
let default_sort = cookie(&req, "comment_sort");
|
||||||
|
|
||||||
|
// If there's no sort query but there's a default sort, set sort to default_sort
|
||||||
|
if sort.is_empty() && !default_sort.is_empty() {
|
||||||
|
sort = default_sort;
|
||||||
|
path = format!("{}.json?{}&sort={}&raw_json=1", req.path(), req.query_string(), sort);
|
||||||
|
}
|
||||||
|
|
||||||
// Log the post ID being fetched in debug mode
|
// Log the post ID being fetched in debug mode
|
||||||
#[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
dbg!(&id);
|
dbg!(req.match_info().get("id").unwrap_or(""));
|
||||||
|
|
||||||
// Handling sort paramater
|
|
||||||
let sorting: String = sort.unwrap_or("confidence".to_string());
|
|
||||||
|
|
||||||
// Build the Reddit JSON API url
|
|
||||||
let url: String = match comment_id {
|
|
||||||
None => format!("{}.json?sort={}&raw_json=1", id, sorting),
|
|
||||||
Some(val) => format!("{}.json?sort={}&comment={}&raw_json=1", id, sorting, val),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url, receive JSON in response
|
||||||
let req = request(url).await;
|
match request(&path).await {
|
||||||
|
// Otherwise, grab the JSON output from the request
|
||||||
|
Ok(res) => {
|
||||||
|
// Parse the JSON into Post and Comment structs
|
||||||
|
let post = parse_post(&res[0]).await;
|
||||||
|
let comments = parse_comments(&res[1]).await;
|
||||||
|
|
||||||
// If the Reddit API returns an error, exit and send error page to user
|
// Use the Post and Comment structs to generate a website to show users
|
||||||
if req.is_err() {
|
let s = PostTemplate { comments, post, sort }.render().unwrap();
|
||||||
let s = ErrorTemplate {
|
HttpResponse::Ok().content_type("text/html").body(s)
|
||||||
message: req.err().unwrap().to_string(),
|
|
||||||
}
|
}
|
||||||
.render()
|
// If the Reddit API returns an error, exit and send error page to user
|
||||||
.unwrap();
|
Err(msg) => error(msg.to_string()).await,
|
||||||
return Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, grab the JSON output from the request
|
|
||||||
let res = req.unwrap();
|
|
||||||
|
|
||||||
// Parse the JSON into Post and Comment structs
|
|
||||||
let post = parse_post(res[0].clone()).await;
|
|
||||||
let comments = parse_comments(res[1].clone()).await;
|
|
||||||
|
|
||||||
// Use the Post and Comment structs to generate a website to show users
|
|
||||||
let s = PostTemplate {
|
|
||||||
comments: comments.unwrap(),
|
|
||||||
post: post.unwrap(),
|
|
||||||
sort: sorting,
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
|
||||||
}
|
|
||||||
|
|
||||||
// SERVICES
|
|
||||||
pub async fn short(web::Path(id): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
|
||||||
render(id, params.sort.clone(), None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn comment(web::Path((_sub, id, _title, comment_id)): web::Path<(String, String, String, String)>, params: web::Query<Params>) -> Result<HttpResponse> {
|
|
||||||
render(id, params.sort.clone(), Some(comment_id)).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn page(web::Path((_sub, id)): web::Path<(String, String)>, params: web::Query<Params>) -> Result<HttpResponse> {
|
|
||||||
render(id, params.sort.clone(), None).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// UTILITIES
|
|
||||||
async fn media(data: &serde_json::Value) -> (String, String) {
|
|
||||||
let post_type: &str;
|
|
||||||
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
|
|
||||||
post_type = "video";
|
|
||||||
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap().to_string()).await
|
|
||||||
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
|
|
||||||
post_type = "video";
|
|
||||||
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap().to_string()).await
|
|
||||||
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
|
||||||
post_type = "image";
|
|
||||||
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap().to_string()).await
|
|
||||||
} else {
|
|
||||||
post_type = "link";
|
|
||||||
data["url"].as_str().unwrap().to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
(post_type.to_string(), url)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSTS
|
// POSTS
|
||||||
async fn parse_post(json: serde_json::Value) -> Result<Post, &'static str> {
|
async fn parse_post(json: &serde_json::Value) -> Post {
|
||||||
// Retrieve post (as opposed to comments) from JSON
|
// Retrieve post (as opposed to comments) from JSON
|
||||||
let post_data: &serde_json::Value = &json["data"]["children"][0];
|
let post: &serde_json::Value = &json["data"]["children"][0];
|
||||||
|
|
||||||
// Grab UTC time as unix timestamp
|
// Grab UTC time as unix timestamp
|
||||||
let unix_time: i64 = post_data["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
|
||||||
// Parse post score
|
// Parse post score and upvote ratio
|
||||||
let score = post_data["data"]["score"].as_i64().unwrap();
|
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||||
|
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||||
|
|
||||||
// Determine the type of media along with the media URL
|
// Determine the type of media along with the media URL
|
||||||
let media = media(&post_data["data"]).await;
|
let (post_type, media) = media(&post["data"]).await;
|
||||||
|
|
||||||
// Build a post using data parsed from Reddit post API
|
// Build a post using data parsed from Reddit post API
|
||||||
let post = Post {
|
Post {
|
||||||
title: val(post_data, "title").await,
|
id: val(post, "id"),
|
||||||
community: val(post_data, "subreddit").await,
|
title: val(post, "title"),
|
||||||
body: val(post_data,"selftext_html").await,
|
community: val(post, "subreddit"),
|
||||||
author: val(post_data, "author").await,
|
body: rewrite_url(&val(post, "selftext_html")),
|
||||||
|
author: val(post, "author"),
|
||||||
author_flair: Flair(
|
author_flair: Flair(
|
||||||
val(post_data, "author_flair_text").await,
|
val(post, "author_flair_text"),
|
||||||
val(post_data, "author_flair_background_color").await,
|
val(post, "author_flair_background_color"),
|
||||||
val(post_data, "author_flair_text_color").await,
|
val(post, "author_flair_text_color"),
|
||||||
),
|
),
|
||||||
url: val(post_data, "permalink").await,
|
permalink: val(post, "permalink"),
|
||||||
score: format_num(score),
|
score: format_num(score),
|
||||||
post_type: media.0,
|
upvote_ratio: ratio as i64,
|
||||||
|
post_type,
|
||||||
|
thumbnail: format_url(val(post, "thumbnail")),
|
||||||
flair: Flair(
|
flair: Flair(
|
||||||
val(post_data, "link_flair_text").await,
|
val(post, "link_flair_text"),
|
||||||
val(post_data, "link_flair_background_color").await,
|
val(post, "link_flair_background_color"),
|
||||||
if val(post_data, "link_flair_text_color").await == "dark" {
|
if val(post, "link_flair_text_color") == "dark" {
|
||||||
"black".to_string()
|
"black".to_string()
|
||||||
} else {
|
} else {
|
||||||
"white".to_string()
|
"white".to_string()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
nsfw: post_data["data"]["over_18"].as_bool().unwrap_or(false),
|
flags: Flags {
|
||||||
media: media.1,
|
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
|
||||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
stickied: post["data"]["stickied"].as_bool().unwrap_or(false),
|
||||||
};
|
},
|
||||||
|
media,
|
||||||
Ok(post)
|
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d %Y %H:%M UTC"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
#[async_recursion]
|
#[async_recursion]
|
||||||
async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'static str> {
|
async fn parse_comments(json: &serde_json::Value) -> Vec<Comment> {
|
||||||
// Separate the comment JSON into a Vector of comments
|
// Separate the comment JSON into a Vector of comments
|
||||||
let comment_data = json["data"]["children"].as_array().unwrap();
|
let comment_data = json["data"]["children"].as_array().unwrap();
|
||||||
|
|
||||||
@ -154,28 +118,28 @@ async fn parse_comments(json: serde_json::Value) -> Result<Vec<Comment>, &'stati
|
|||||||
}
|
}
|
||||||
|
|
||||||
let score = comment["data"]["score"].as_i64().unwrap_or(0);
|
let score = comment["data"]["score"].as_i64().unwrap_or(0);
|
||||||
let body = val(comment, "body_html").await;
|
let body = rewrite_url(&val(comment, "body_html"));
|
||||||
|
|
||||||
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
|
let replies: Vec<Comment> = if comment["data"]["replies"].is_object() {
|
||||||
parse_comments(comment["data"]["replies"].clone()).await.unwrap_or(Vec::new())
|
parse_comments(&comment["data"]["replies"]).await
|
||||||
} else {
|
} else {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
};
|
};
|
||||||
|
|
||||||
comments.push(Comment {
|
comments.push(Comment {
|
||||||
id: val(comment, "id").await,
|
id: val(comment, "id"),
|
||||||
body: body,
|
body,
|
||||||
author: val(comment, "author").await,
|
author: val(comment, "author"),
|
||||||
score: format_num(score),
|
score: format_num(score),
|
||||||
time: Utc.timestamp(unix_time, 0).format("%b %e %Y %H:%M UTC").to_string(),
|
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d %Y %H:%M UTC"),
|
||||||
replies: replies,
|
replies,
|
||||||
flair: Flair(
|
flair: Flair(
|
||||||
val(comment, "author_flair_text").await,
|
val(comment, "author_flair_text"),
|
||||||
val(comment, "author_flair_background_color").await,
|
val(comment, "author_flair_background_color"),
|
||||||
val(comment, "author_flair_text_color").await,
|
val(comment, "author_flair_text_color"),
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(comments)
|
comments
|
||||||
}
|
}
|
||||||
|
60
src/proxy.rs
60
src/proxy.rs
@ -1,29 +1,47 @@
|
|||||||
use actix_web::{client::Client, web, Error, HttpResponse, Result};
|
use actix_web::{client::Client, error, web, Error, HttpResponse, Result};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
#[cfg(feature = "proxy")]
|
|
||||||
use base64::decode;
|
use base64::decode;
|
||||||
|
|
||||||
pub async fn handler(web::Path(url): web::Path<String>) -> Result<HttpResponse> {
|
pub async fn handler(web::Path(b64): web::Path<String>) -> Result<HttpResponse> {
|
||||||
if cfg!(feature = "proxy") {
|
let domains = vec![
|
||||||
let media: String;
|
// THUMBNAILS
|
||||||
|
"a.thumbs.redditmedia.com",
|
||||||
|
"b.thumbs.redditmedia.com",
|
||||||
|
// ICONS
|
||||||
|
"styles.redditmedia.com",
|
||||||
|
"www.redditstatic.com",
|
||||||
|
// PREVIEWS
|
||||||
|
"preview.redd.it",
|
||||||
|
"external-preview.redd.it",
|
||||||
|
// MEDIA
|
||||||
|
"i.redd.it",
|
||||||
|
"v.redd.it",
|
||||||
|
];
|
||||||
|
|
||||||
#[cfg(not(feature = "proxy"))]
|
match decode(b64) {
|
||||||
let media = url;
|
Ok(bytes) => {
|
||||||
|
let media = String::from_utf8(bytes).unwrap_or_default();
|
||||||
|
|
||||||
#[cfg(feature = "proxy")]
|
match Url::parse(media.as_str()) {
|
||||||
match decode(url) {
|
Ok(url) => {
|
||||||
Ok(bytes) => media = String::from_utf8(bytes).unwrap(),
|
let domain = url.domain().unwrap_or_default();
|
||||||
Err(_e) => return Ok(HttpResponse::Ok().body("")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = Client::default();
|
if domains.contains(&domain) {
|
||||||
client
|
Client::default().get(media.replace("&", "&")).send().await.map_err(Error::from).map(|res| {
|
||||||
.get(media.replace("&", "&"))
|
HttpResponse::build(res.status())
|
||||||
.send()
|
.header("Cache-Control", "public, max-age=1209600, s-maxage=86400")
|
||||||
.await
|
.header("Content-Length", res.headers().get("Content-Length").unwrap().to_owned())
|
||||||
.map_err(Error::from)
|
.header("Content-Type", res.headers().get("Content-Type").unwrap().to_owned())
|
||||||
.and_then(|res| Ok(HttpResponse::build(res.status()).streaming(res)))
|
.streaming(res)
|
||||||
} else {
|
})
|
||||||
Ok(HttpResponse::Ok().body(""))
|
} else {
|
||||||
|
Err(error::ErrorForbidden("Resource must be from Reddit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(error::ErrorBadRequest("Can't parse base64 into URL")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => Err(error::ErrorBadRequest("Can't decode base64")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
55
src/search.rs
Normal file
55
src/search.rs
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
// CRATES
|
||||||
|
use crate::utils::{error, fetch_posts, param, prefs, Post, Preferences};
|
||||||
|
use actix_web::{HttpRequest, HttpResponse};
|
||||||
|
use askama::Template;
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
struct SearchParams {
|
||||||
|
q: String,
|
||||||
|
sort: String,
|
||||||
|
t: String,
|
||||||
|
before: String,
|
||||||
|
after: String,
|
||||||
|
restrict_sr: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "search.html", escape = "none")]
|
||||||
|
struct SearchTemplate {
|
||||||
|
posts: Vec<Post>,
|
||||||
|
sub: String,
|
||||||
|
params: SearchParams,
|
||||||
|
prefs: Preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SERVICES
|
||||||
|
pub async fn find(req: HttpRequest) -> HttpResponse {
|
||||||
|
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||||
|
let sort = if param(&path, "sort").is_empty() {
|
||||||
|
"relevance".to_string()
|
||||||
|
} else {
|
||||||
|
param(&path, "sort")
|
||||||
|
};
|
||||||
|
let sub = req.match_info().get("sub").unwrap_or("").to_string();
|
||||||
|
|
||||||
|
match fetch_posts(&path, String::new()).await {
|
||||||
|
Ok((posts, after)) => HttpResponse::Ok().content_type("text/html").body(
|
||||||
|
SearchTemplate {
|
||||||
|
posts,
|
||||||
|
sub,
|
||||||
|
params: SearchParams {
|
||||||
|
q: param(&path, "q"),
|
||||||
|
sort,
|
||||||
|
t: param(&path, "t"),
|
||||||
|
before: param(&path, "after"),
|
||||||
|
after,
|
||||||
|
restrict_sr: param(&path, "restrict_sr"),
|
||||||
|
},
|
||||||
|
prefs: prefs(req),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap(),
|
||||||
|
),
|
||||||
|
Err(msg) => error(msg.to_string()).await,
|
||||||
|
}
|
||||||
|
}
|
57
src/settings.rs
Normal file
57
src/settings.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// CRATES
|
||||||
|
use crate::utils::{prefs, Preferences};
|
||||||
|
use actix_web::{cookie::Cookie, web::Form, HttpMessage, HttpRequest, HttpResponse};
|
||||||
|
use askama::Template;
|
||||||
|
use time::{Duration, OffsetDateTime};
|
||||||
|
|
||||||
|
// STRUCTS
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "settings.html")]
|
||||||
|
struct SettingsTemplate {
|
||||||
|
prefs: Preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(serde::Deserialize)]
|
||||||
|
pub struct SettingsForm {
|
||||||
|
front_page: Option<String>,
|
||||||
|
layout: Option<String>,
|
||||||
|
comment_sort: Option<String>,
|
||||||
|
hide_nsfw: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// FUNCTIONS
|
||||||
|
|
||||||
|
// Retrieve cookies from request "Cookie" header
|
||||||
|
pub async fn get(req: HttpRequest) -> HttpResponse {
|
||||||
|
let s = SettingsTemplate { prefs: prefs(req) }.render().unwrap();
|
||||||
|
HttpResponse::Ok().content_type("text/html").body(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set cookies using response "Set-Cookie" header
|
||||||
|
pub async fn set(req: HttpRequest, form: Form<SettingsForm>) -> HttpResponse {
|
||||||
|
let mut res = HttpResponse::Found();
|
||||||
|
|
||||||
|
let names = vec!["front_page", "layout", "comment_sort", "hide_nsfw"];
|
||||||
|
let values = vec![&form.front_page, &form.layout, &form.comment_sort, &form.hide_nsfw];
|
||||||
|
|
||||||
|
for (i, name) in names.iter().enumerate() {
|
||||||
|
match values[i] {
|
||||||
|
Some(value) => res.cookie(
|
||||||
|
Cookie::build(name.to_owned(), value)
|
||||||
|
.path("/")
|
||||||
|
.http_only(true)
|
||||||
|
.expires(OffsetDateTime::now_utc() + Duration::weeks(52))
|
||||||
|
.finish(),
|
||||||
|
),
|
||||||
|
None => match HttpMessage::cookie(&req, name.to_owned()) {
|
||||||
|
Some(cookie) => res.del_cookie(&cookie),
|
||||||
|
None => &mut res,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
res
|
||||||
|
.content_type("text/html")
|
||||||
|
.set_header("Location", "/settings")
|
||||||
|
.body(r#"Redirecting to <a href="/settings">settings</a>..."#)
|
||||||
|
}
|
165
src/subreddit.rs
165
src/subreddit.rs
@ -1,8 +1,7 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{fetch_posts, format_num, format_url, request, val, ErrorTemplate, Params, Post, Subreddit};
|
use crate::utils::{cookie, error, fetch_posts, format_num, format_url, param, prefs, request, rewrite_url, val, Post, Preferences, Subreddit};
|
||||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use std::convert::TryInto;
|
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -10,96 +9,104 @@ use std::convert::TryInto;
|
|||||||
struct SubredditTemplate {
|
struct SubredditTemplate {
|
||||||
sub: Subreddit,
|
sub: Subreddit,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
sort: String,
|
sort: (String, String),
|
||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
|
prefs: Preferences,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "wiki.html", escape = "none")]
|
||||||
|
struct WikiTemplate {
|
||||||
|
sub: String,
|
||||||
|
wiki: String,
|
||||||
|
page: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
// SERVICES
|
||||||
#[allow(dead_code)]
|
pub async fn page(req: HttpRequest) -> HttpResponse {
|
||||||
pub async fn page(web::Path(sub): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
let path = format!("{}.json?{}", req.path(), req.query_string());
|
||||||
render(sub, params.sort.clone(), (params.before.clone(), params.after.clone())).await
|
let default = cookie(&req, "front_page");
|
||||||
|
let sub_name = req
|
||||||
|
.match_info()
|
||||||
|
.get("sub")
|
||||||
|
.unwrap_or(if default.is_empty() { "popular" } else { default.as_str() })
|
||||||
|
.to_string();
|
||||||
|
let sort = req.match_info().get("sort").unwrap_or("hot").to_string();
|
||||||
|
|
||||||
|
let sub = if !&sub_name.contains('+') && sub_name != "popular" && sub_name != "all" {
|
||||||
|
subreddit(&sub_name).await.unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
Subreddit::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
match fetch_posts(&path, String::new()).await {
|
||||||
|
Ok((posts, after)) => {
|
||||||
|
let s = SubredditTemplate {
|
||||||
|
sub,
|
||||||
|
posts,
|
||||||
|
sort: (sort, param(&path, "t")),
|
||||||
|
ends: (param(&path, "after"), after),
|
||||||
|
prefs: prefs(req),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
HttpResponse::Ok().content_type("text/html").body(s)
|
||||||
|
}
|
||||||
|
Err(msg) => error(msg.to_string()).await,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn render(sub_name: String, sort: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
|
pub async fn wiki(req: HttpRequest) -> HttpResponse {
|
||||||
let sorting = sort.unwrap_or("hot".to_string());
|
let sub = req.match_info().get("sub").unwrap_or("reddit.com");
|
||||||
let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before
|
let page = req.match_info().get("page").unwrap_or("index");
|
||||||
|
let path: String = format!("r/{}/wiki/{}.json?raw_json=1", sub, page);
|
||||||
|
|
||||||
// Build the Reddit JSON API url
|
match request(&path).await {
|
||||||
let url = match ends.0 {
|
Ok(res) => {
|
||||||
Some(val) => format!("r/{}/{}.json?before={}&count=25", sub_name, sorting, val),
|
let s = WikiTemplate {
|
||||||
None => match ends.1 {
|
sub: sub.to_string(),
|
||||||
Some(val) => format!("r/{}/{}.json?after={}&count=25", sub_name, sorting, val),
|
wiki: rewrite_url(res["data"]["content_html"].as_str().unwrap_or_default()),
|
||||||
None => format!("r/{}/{}.json", sub_name, sorting),
|
page: page.to_string(),
|
||||||
},
|
}
|
||||||
};
|
.render()
|
||||||
|
.unwrap();
|
||||||
let sub_result = if !&sub_name.contains("+") {
|
HttpResponse::Ok().content_type("text/html").body(s)
|
||||||
subreddit(&sub_name).await
|
|
||||||
} else {
|
|
||||||
Ok(Subreddit::default())
|
|
||||||
};
|
|
||||||
let items_result = fetch_posts(url, String::new()).await;
|
|
||||||
|
|
||||||
if sub_result.is_err() || items_result.is_err() {
|
|
||||||
let s = ErrorTemplate {
|
|
||||||
message: sub_result.err().unwrap().to_string(),
|
|
||||||
}
|
}
|
||||||
.render()
|
Err(msg) => error(msg.to_string()).await,
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
|
||||||
} else {
|
|
||||||
let sub = sub_result.unwrap();
|
|
||||||
let items = items_result.unwrap();
|
|
||||||
|
|
||||||
let s = SubredditTemplate {
|
|
||||||
sub: sub,
|
|
||||||
posts: items.0,
|
|
||||||
sort: sorting,
|
|
||||||
ends: (before, items.1),
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SUBREDDIT
|
// SUBREDDIT
|
||||||
async fn subreddit(sub: &String) -> Result<Subreddit, &'static str> {
|
async fn subreddit(sub: &str) -> Result<Subreddit, &'static str> {
|
||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API url
|
||||||
let url: String = format!("r/{}/about.json?raw_json=1", sub);
|
let path: String = format!("r/{}/about.json?raw_json=1", sub);
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url
|
||||||
let req = request(url).await;
|
match request(&path).await {
|
||||||
|
// If success, receive JSON in response
|
||||||
|
Ok(res) => {
|
||||||
|
// Metadata regarding the subreddit
|
||||||
|
let members: i64 = res["data"]["subscribers"].as_u64().unwrap_or_default() as i64;
|
||||||
|
let active: i64 = res["data"]["accounts_active"].as_u64().unwrap_or_default() as i64;
|
||||||
|
|
||||||
// If the Reddit API returns an error, exit this function
|
// Fetch subreddit icon either from the community_icon or icon_img value
|
||||||
if req.is_err() {
|
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap_or("").split('?').collect::<Vec<&str>>()[0];
|
||||||
return Err(req.err().unwrap());
|
let icon = if community_icon.is_empty() { val(&res, "icon_img") } else { community_icon.to_string() };
|
||||||
|
|
||||||
|
let sub = Subreddit {
|
||||||
|
name: val(&res, "display_name"),
|
||||||
|
title: val(&res, "title"),
|
||||||
|
description: val(&res, "public_description"),
|
||||||
|
info: rewrite_url(&val(&res, "description_html").replace("\\", "")),
|
||||||
|
icon: format_url(icon),
|
||||||
|
members: format_num(members),
|
||||||
|
active: format_num(active),
|
||||||
|
wiki: res["data"]["wiki_enabled"].as_bool().unwrap_or_default(),
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(sub)
|
||||||
|
}
|
||||||
|
// If the Reddit API returns an error, exit this function
|
||||||
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, grab the JSON output from the request
|
|
||||||
let res = req.unwrap();
|
|
||||||
|
|
||||||
// Metadata regarding the subreddit
|
|
||||||
let members = res["data"]["subscribers"].as_u64().unwrap_or(0);
|
|
||||||
let active = res["data"]["accounts_active"].as_u64().unwrap_or(0);
|
|
||||||
|
|
||||||
// Fetch subreddit icon either from the community_icon or icon_img value
|
|
||||||
let community_icon: &str = res["data"]["community_icon"].as_str().unwrap().split("?").collect::<Vec<&str>>()[0];
|
|
||||||
let icon = if community_icon.is_empty() {
|
|
||||||
val(&res, "icon_img").await
|
|
||||||
} else {
|
|
||||||
community_icon.to_string()
|
|
||||||
};
|
|
||||||
|
|
||||||
let sub = Subreddit {
|
|
||||||
name: val(&res, "display_name").await,
|
|
||||||
title: val(&res, "title").await,
|
|
||||||
description: val(&res, "public_description").await,
|
|
||||||
info: val(&res, "description_html").await.replace("\\", ""),
|
|
||||||
icon: format_url(icon).await,
|
|
||||||
members: format_num(members.try_into().unwrap()),
|
|
||||||
active: format_num(active.try_into().unwrap()),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(sub)
|
|
||||||
}
|
}
|
||||||
|
116
src/user.rs
116
src/user.rs
@ -1,8 +1,8 @@
|
|||||||
// CRATES
|
// CRATES
|
||||||
use crate::utils::{fetch_posts, format_url, nested_val, request, ErrorTemplate, Params, Post, User};
|
use crate::utils::{error, fetch_posts, format_url, nested_val, param, prefs, request, Post, Preferences, User};
|
||||||
use actix_web::{http::StatusCode, web, HttpResponse, Result};
|
use actix_web::{HttpRequest, HttpResponse, Result};
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
use chrono::{TimeZone, Utc};
|
use time::OffsetDateTime;
|
||||||
|
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@ -10,80 +10,66 @@ use chrono::{TimeZone, Utc};
|
|||||||
struct UserTemplate {
|
struct UserTemplate {
|
||||||
user: User,
|
user: User,
|
||||||
posts: Vec<Post>,
|
posts: Vec<Post>,
|
||||||
sort: String,
|
sort: (String, String),
|
||||||
ends: (String, String),
|
ends: (String, String),
|
||||||
|
prefs: Preferences,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn render(username: String, sort: Option<String>, ends: (Option<String>, Option<String>)) -> Result<HttpResponse> {
|
// FUNCTIONS
|
||||||
let sorting = sort.unwrap_or("new".to_string());
|
pub async fn profile(req: HttpRequest) -> HttpResponse {
|
||||||
|
// Build the Reddit JSON API path
|
||||||
|
let path = format!("{}.json?{}&raw_json=1", req.path(), req.query_string());
|
||||||
|
|
||||||
let before = ends.1.clone().unwrap_or(String::new()); // If there is an after, there must be a before
|
// Retrieve other variables from Libreddit request
|
||||||
|
let sort = param(&path, "sort");
|
||||||
|
let username = req.match_info().get("username").unwrap_or("").to_string();
|
||||||
|
|
||||||
// Build the Reddit JSON API url
|
// Request user profile data and user posts/comments from Reddit
|
||||||
let url = match ends.0 {
|
let user = user(&username).await.unwrap_or_default();
|
||||||
Some(val) => format!("user/{}/.json?sort={}&before={}&count=25&raw_json=1", username, sorting, val),
|
let posts = fetch_posts(&path, "Comment".to_string()).await;
|
||||||
None => match ends.1 {
|
|
||||||
Some(val) => format!("user/{}/.json?sort={}&after={}&count=25&raw_json=1", username, sorting, val),
|
|
||||||
None => format!("user/{}/.json?sort={}&raw_json=1", username, sorting),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
let user = user(&username).await;
|
match posts {
|
||||||
let posts = fetch_posts(url, "Comment".to_string()).await;
|
Ok((posts, after)) => {
|
||||||
|
let s = UserTemplate {
|
||||||
if user.is_err() || posts.is_err() {
|
user,
|
||||||
let s = ErrorTemplate {
|
posts,
|
||||||
message: user.err().unwrap().to_string(),
|
sort: (sort, param(&path, "t")),
|
||||||
|
ends: (param(&path, "after"), after),
|
||||||
|
prefs: prefs(req),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
|
.unwrap();
|
||||||
|
HttpResponse::Ok().content_type("text/html").body(s)
|
||||||
}
|
}
|
||||||
.render()
|
// If there is an error show error page
|
||||||
.unwrap();
|
Err(msg) => error(msg.to_string()).await,
|
||||||
Ok(HttpResponse::Ok().status(StatusCode::NOT_FOUND).content_type("text/html").body(s))
|
|
||||||
} else {
|
|
||||||
let posts_unwrapped = posts.unwrap();
|
|
||||||
|
|
||||||
let s = UserTemplate {
|
|
||||||
user: user.unwrap(),
|
|
||||||
posts: posts_unwrapped.0,
|
|
||||||
sort: sorting,
|
|
||||||
ends: (before, posts_unwrapped.1)
|
|
||||||
}
|
|
||||||
.render()
|
|
||||||
.unwrap();
|
|
||||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SERVICES
|
|
||||||
pub async fn page(web::Path(username): web::Path<String>, params: web::Query<Params>) -> Result<HttpResponse> {
|
|
||||||
render(username, params.sort.clone(), (params.before.clone(), params.after.clone())).await
|
|
||||||
}
|
|
||||||
|
|
||||||
// USER
|
// USER
|
||||||
async fn user(name: &String) -> Result<User, &'static str> {
|
async fn user(name: &str) -> Result<User, &'static str> {
|
||||||
// Build the Reddit JSON API url
|
// Build the Reddit JSON API path
|
||||||
let url: String = format!("user/{}/about.json", name);
|
let path: String = format!("user/{}/about.json", name);
|
||||||
|
|
||||||
// Send a request to the url, receive JSON in response
|
// Send a request to the url
|
||||||
let req = request(url).await;
|
match request(&path).await {
|
||||||
|
// If success, receive JSON in response
|
||||||
|
Ok(res) => {
|
||||||
|
// Grab creation date as unix timestamp
|
||||||
|
let created: i64 = res["data"]["created"].as_f64().unwrap_or(0.0).round() as i64;
|
||||||
|
|
||||||
// If the Reddit API returns an error, exit this function
|
// Parse the JSON output into a User struct
|
||||||
if req.is_err() {
|
Ok(User {
|
||||||
return Err(req.err().unwrap());
|
name: name.to_string(),
|
||||||
|
title: nested_val(&res, "subreddit", "title"),
|
||||||
|
icon: format_url(nested_val(&res, "subreddit", "icon_img")),
|
||||||
|
karma: res["data"]["total_karma"].as_i64().unwrap_or(0),
|
||||||
|
created: OffsetDateTime::from_unix_timestamp(created).format("%b %d '%y"),
|
||||||
|
banner: nested_val(&res, "subreddit", "banner_img"),
|
||||||
|
description: nested_val(&res, "subreddit", "public_description"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// If the Reddit API returns an error, exit this function
|
||||||
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, grab the JSON output from the request
|
|
||||||
let res = req.unwrap();
|
|
||||||
|
|
||||||
// Grab creation date as unix timestamp
|
|
||||||
let created: i64 = res["data"]["created"].as_f64().unwrap().round() as i64;
|
|
||||||
|
|
||||||
// Parse the JSON output into a User struct
|
|
||||||
Ok(User {
|
|
||||||
name: name.to_string(),
|
|
||||||
icon: format_url(nested_val(&res, "subreddit", "icon_img").await).await,
|
|
||||||
karma: res["data"]["total_karma"].as_i64().unwrap(),
|
|
||||||
created: Utc.timestamp(created, 0).format("%b %e, %Y").to_string(),
|
|
||||||
banner: nested_val(&res, "subreddit", "banner_img").await,
|
|
||||||
description: nested_val(&res, "subreddit", "public_description").await,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
270
src/utils.rs
270
src/utils.rs
@ -1,31 +1,41 @@
|
|||||||
//
|
//
|
||||||
// CRATES
|
// CRATES
|
||||||
//
|
//
|
||||||
use chrono::{TimeZone, Utc};
|
use actix_web::{cookie::Cookie, HttpRequest, HttpResponse, Result};
|
||||||
use serde_json::{from_str, Value};
|
use askama::Template;
|
||||||
// use surf::{client, get, middleware::Redirect};
|
|
||||||
|
|
||||||
#[cfg(feature = "proxy")]
|
|
||||||
use base64::encode;
|
use base64::encode;
|
||||||
|
use regex::Regex;
|
||||||
|
use serde_json::from_str;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use time::OffsetDateTime;
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
//
|
//
|
||||||
// STRUCTS
|
// STRUCTS
|
||||||
//
|
//
|
||||||
// Post flair with text, background color and foreground color
|
// Post flair with text, background color and foreground color
|
||||||
pub struct Flair(pub String, pub String, pub String);
|
pub struct Flair(pub String, pub String, pub String);
|
||||||
|
// Post flags with nsfw and stickied
|
||||||
|
pub struct Flags {
|
||||||
|
pub nsfw: bool,
|
||||||
|
pub stickied: bool,
|
||||||
|
}
|
||||||
|
|
||||||
// Post containing content, metadata and media
|
// Post containing content, metadata and media
|
||||||
pub struct Post {
|
pub struct Post {
|
||||||
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub community: String,
|
pub community: String,
|
||||||
pub body: String,
|
pub body: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub author_flair: Flair,
|
pub author_flair: Flair,
|
||||||
pub url: String,
|
pub permalink: String,
|
||||||
pub score: String,
|
pub score: String,
|
||||||
|
pub upvote_ratio: i64,
|
||||||
pub post_type: String,
|
pub post_type: String,
|
||||||
pub flair: Flair,
|
pub flair: Flair,
|
||||||
pub nsfw: bool,
|
pub flags: Flags,
|
||||||
|
pub thumbnail: String,
|
||||||
pub media: String,
|
pub media: String,
|
||||||
pub time: String,
|
pub time: String,
|
||||||
}
|
}
|
||||||
@ -41,9 +51,11 @@ pub struct Comment {
|
|||||||
pub replies: Vec<Comment>,
|
pub replies: Vec<Comment>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
// User struct containing metadata about user
|
// User struct containing metadata about user
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub title: String,
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub karma: i64,
|
pub karma: i64,
|
||||||
pub created: String,
|
pub created: String,
|
||||||
@ -61,170 +73,230 @@ pub struct Subreddit {
|
|||||||
pub icon: String,
|
pub icon: String,
|
||||||
pub members: String,
|
pub members: String,
|
||||||
pub active: String,
|
pub active: String,
|
||||||
|
pub wiki: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
// Parser for query params, used in sorting (eg. /r/rust/?sort=hot)
|
||||||
#[derive(serde::Deserialize)]
|
#[derive(serde::Deserialize)]
|
||||||
pub struct Params {
|
pub struct Params {
|
||||||
|
pub t: Option<String>,
|
||||||
|
pub q: Option<String>,
|
||||||
pub sort: Option<String>,
|
pub sort: Option<String>,
|
||||||
pub after: Option<String>,
|
pub after: Option<String>,
|
||||||
pub before: Option<String>,
|
pub before: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error template
|
// Error template
|
||||||
#[derive(askama::Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "error.html", escape = "none")]
|
#[template(path = "error.html", escape = "none")]
|
||||||
pub struct ErrorTemplate {
|
pub struct ErrorTemplate {
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Preferences {
|
||||||
|
pub front_page: String,
|
||||||
|
pub layout: String,
|
||||||
|
pub hide_nsfw: String,
|
||||||
|
pub comment_sort: String,
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// FORMATTING
|
// FORMATTING
|
||||||
//
|
//
|
||||||
|
|
||||||
|
// Build preferences from cookies
|
||||||
|
pub fn prefs(req: HttpRequest) -> Preferences {
|
||||||
|
Preferences {
|
||||||
|
front_page: cookie(&req, "front_page"),
|
||||||
|
layout: cookie(&req, "layout"),
|
||||||
|
hide_nsfw: cookie(&req, "hide_nsfw"),
|
||||||
|
comment_sort: cookie(&req, "comment_sort"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab a query param from a url
|
||||||
|
pub fn param(path: &str, value: &str) -> String {
|
||||||
|
let url = Url::parse(format!("https://libredd.it/{}", path).as_str()).unwrap();
|
||||||
|
let pairs: HashMap<_, _> = url.query_pairs().into_owned().collect();
|
||||||
|
pairs.get(value).unwrap_or(&String::new()).to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse Cookie value from request
|
||||||
|
pub fn cookie(req: &HttpRequest, name: &str) -> String {
|
||||||
|
actix_web::HttpMessage::cookie(req, name).unwrap_or_else(|| Cookie::new(name, "")).value().to_string()
|
||||||
|
}
|
||||||
|
|
||||||
// Direct urls to proxy if proxy is enabled
|
// Direct urls to proxy if proxy is enabled
|
||||||
pub async fn format_url(url: String) -> String {
|
pub fn format_url(url: String) -> String {
|
||||||
if url.is_empty() {
|
if url.is_empty() || url == "self" || url == "default" || url == "nsfw" || url == "spoiler" {
|
||||||
return String::new();
|
String::new()
|
||||||
};
|
} else {
|
||||||
|
format!("/proxy/{}", encode(url).as_str())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "proxy")]
|
// Rewrite Reddit links to Libreddit in body of text
|
||||||
return "/proxy/".to_string() + encode(url).as_str();
|
pub fn rewrite_url(text: &str) -> String {
|
||||||
|
let re = Regex::new(r#"href="(https://|http://|)(www.|)(reddit).(com)/"#).unwrap();
|
||||||
#[cfg(not(feature = "proxy"))]
|
re.replace_all(text, r#"href="/"#).to_string()
|
||||||
return url.to_string();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append `m` and `k` for millions and thousands respectively
|
// Append `m` and `k` for millions and thousands respectively
|
||||||
pub fn format_num(num: i64) -> String {
|
pub fn format_num(num: i64) -> String {
|
||||||
if num > 1000000 {
|
if num > 1_000_000 {
|
||||||
format!("{}m", num / 1000000)
|
format!("{}m", num / 1_000_000)
|
||||||
} else if num > 1000 {
|
} else if num > 1000 {
|
||||||
format!("{}k", num / 1000)
|
format!("{}k", num / 1_000)
|
||||||
} else {
|
} else {
|
||||||
num.to_string()
|
num.to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn media(data: &serde_json::Value) -> (String, String) {
|
||||||
|
let post_type: &str;
|
||||||
|
let url = if !data["preview"]["reddit_video_preview"]["fallback_url"].is_null() {
|
||||||
|
post_type = "video";
|
||||||
|
format_url(data["preview"]["reddit_video_preview"]["fallback_url"].as_str().unwrap_or_default().to_string())
|
||||||
|
} else if !data["secure_media"]["reddit_video"]["fallback_url"].is_null() {
|
||||||
|
post_type = "video";
|
||||||
|
format_url(data["secure_media"]["reddit_video"]["fallback_url"].as_str().unwrap_or_default().to_string())
|
||||||
|
} else if data["post_hint"].as_str().unwrap_or("") == "image" {
|
||||||
|
post_type = "image";
|
||||||
|
format_url(data["preview"]["images"][0]["source"]["url"].as_str().unwrap_or_default().to_string())
|
||||||
|
} else {
|
||||||
|
post_type = "link";
|
||||||
|
data["url"].as_str().unwrap_or_default().to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
(post_type.to_string(), url)
|
||||||
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// JSON PARSING
|
// JSON PARSING
|
||||||
//
|
//
|
||||||
|
|
||||||
// val() function used to parse JSON from Reddit APIs
|
// val() function used to parse JSON from Reddit APIs
|
||||||
pub async fn val(j: &serde_json::Value, k: &str) -> String {
|
pub fn val(j: &serde_json::Value, k: &str) -> String {
|
||||||
String::from(j["data"][k].as_str().unwrap_or(""))
|
String::from(j["data"][k].as_str().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// nested_val() function used to parse JSON from Reddit APIs
|
// nested_val() function used to parse JSON from Reddit APIs
|
||||||
pub async fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
|
pub fn nested_val(j: &serde_json::Value, n: &str, k: &str) -> String {
|
||||||
String::from(j["data"][n][k].as_str().unwrap())
|
String::from(j["data"][n][k].as_str().unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch posts of a user or subreddit
|
// Fetch posts of a user or subreddit and return a vector of posts and the "after" value
|
||||||
pub async fn fetch_posts(url: String, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
|
pub async fn fetch_posts(path: &str, fallback_title: String) -> Result<(Vec<Post>, String), &'static str> {
|
||||||
// Send a request to the url, receive JSON in response
|
let res;
|
||||||
let req = request(url.clone()).await;
|
let post_list;
|
||||||
|
|
||||||
// If the Reddit API returns an error, exit this function
|
// Send a request to the url
|
||||||
if req.is_err() {
|
match request(&path).await {
|
||||||
return Err(req.err().unwrap());
|
// If success, receive JSON in response
|
||||||
|
Ok(response) => {
|
||||||
|
res = response;
|
||||||
|
}
|
||||||
|
// If the Reddit API returns an error, exit this function
|
||||||
|
Err(msg) => return Err(msg),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, grab the JSON output from the request
|
|
||||||
let res = req.unwrap();
|
|
||||||
|
|
||||||
// Fetch the list of posts from the JSON response
|
// Fetch the list of posts from the JSON response
|
||||||
let post_list = res["data"]["children"].as_array().unwrap();
|
match res["data"]["children"].as_array() {
|
||||||
|
Some(list) => post_list = list,
|
||||||
|
None => return Err("No posts found"),
|
||||||
|
}
|
||||||
|
|
||||||
let mut posts: Vec<Post> = Vec::new();
|
let mut posts: Vec<Post> = Vec::new();
|
||||||
|
|
||||||
|
// For each post from posts list
|
||||||
for post in post_list {
|
for post in post_list {
|
||||||
let img = if val(post, "thumbnail").await.starts_with("https:/") {
|
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap_or_default().round() as i64;
|
||||||
format_url(val(post, "thumbnail").await).await
|
let score = post["data"]["score"].as_i64().unwrap_or_default();
|
||||||
} else {
|
let ratio: f64 = post["data"]["upvote_ratio"].as_f64().unwrap_or(1.0) * 100.0;
|
||||||
String::new()
|
let title = val(post, "title");
|
||||||
};
|
|
||||||
let unix_time: i64 = post["data"]["created_utc"].as_f64().unwrap().round() as i64;
|
// Determine the type of media along with the media URL
|
||||||
let score = post["data"]["score"].as_i64().unwrap();
|
let (post_type, media) = media(&post["data"]).await;
|
||||||
let title = val(post, "title").await;
|
|
||||||
|
|
||||||
posts.push(Post {
|
posts.push(Post {
|
||||||
|
id: val(post, "id"),
|
||||||
title: if title.is_empty() { fallback_title.to_owned() } else { title },
|
title: if title.is_empty() { fallback_title.to_owned() } else { title },
|
||||||
community: val(post, "subreddit").await,
|
community: val(post, "subreddit"),
|
||||||
body: val(post, "body_html").await,
|
body: rewrite_url(&val(post, "body_html")),
|
||||||
author: val(post, "author").await,
|
author: val(post, "author"),
|
||||||
author_flair: Flair(
|
author_flair: Flair(
|
||||||
val(post, "author_flair_text").await,
|
val(post, "author_flair_text"),
|
||||||
val(post, "author_flair_background_color").await,
|
val(post, "author_flair_background_color"),
|
||||||
val(post, "author_flair_text_color").await,
|
val(post, "author_flair_text_color"),
|
||||||
),
|
),
|
||||||
score: format_num(score),
|
score: format_num(score),
|
||||||
post_type: "link".to_string(),
|
upvote_ratio: ratio as i64,
|
||||||
media: img,
|
post_type,
|
||||||
|
thumbnail: format_url(val(post, "thumbnail")),
|
||||||
|
media,
|
||||||
flair: Flair(
|
flair: Flair(
|
||||||
val(post, "link_flair_text").await,
|
val(post, "link_flair_text"),
|
||||||
val(post, "link_flair_background_color").await,
|
val(post, "link_flair_background_color"),
|
||||||
if val(post, "link_flair_text_color").await == "dark" {
|
if val(post, "link_flair_text_color") == "dark" {
|
||||||
"black".to_string()
|
"black".to_string()
|
||||||
} else {
|
} else {
|
||||||
"white".to_string()
|
"white".to_string()
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
nsfw: post["data"]["over_18"].as_bool().unwrap_or(false),
|
flags: Flags {
|
||||||
url: val(post, "permalink").await,
|
nsfw: post["data"]["over_18"].as_bool().unwrap_or_default(),
|
||||||
time: Utc.timestamp(unix_time, 0).format("%b %e '%y").to_string(),
|
stickied: post["data"]["stickied"].as_bool().unwrap_or_default(),
|
||||||
|
},
|
||||||
|
permalink: val(post, "permalink"),
|
||||||
|
time: OffsetDateTime::from_unix_timestamp(unix_time).format("%b %d '%y"), // %b %e '%y
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dbg!(url);
|
Ok((posts, res["data"]["after"].as_str().unwrap_or_default().to_string()))
|
||||||
|
|
||||||
Ok((posts, res["data"]["after"].as_str().unwrap_or("").to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
// NETWORKING
|
// NETWORKING
|
||||||
//
|
//
|
||||||
|
|
||||||
|
pub async fn error(msg: String) -> HttpResponse {
|
||||||
|
let body = ErrorTemplate { message: msg }.render().unwrap_or_default();
|
||||||
|
HttpResponse::NotFound().content_type("text/html").body(body)
|
||||||
|
}
|
||||||
|
|
||||||
// Make a request to a Reddit API and parse the JSON response
|
// Make a request to a Reddit API and parse the JSON response
|
||||||
pub async fn request(mut url: String) -> Result<serde_json::Value, &'static str> {
|
pub async fn request(path: &str) -> Result<serde_json::Value, &'static str> {
|
||||||
url = format!("https://www.reddit.com/{}", url);
|
let url = format!("https://www.reddit.com/{}", path);
|
||||||
|
|
||||||
// --- actix-web::client ---
|
// Send request using reqwest
|
||||||
// let client = actix_web::client::Client::default();
|
match reqwest::get(&url).await {
|
||||||
// let res = client
|
Ok(res) => {
|
||||||
// .get(url)
|
// Read the status from the response
|
||||||
// .send()
|
match res.status().is_success() {
|
||||||
// .await?
|
true => {
|
||||||
// .body()
|
// Parse the response from Reddit as JSON
|
||||||
// .limit(1000000)
|
match from_str(res.text().await.unwrap_or_default().as_str()) {
|
||||||
// .await?;
|
Ok(json) => Ok(json),
|
||||||
|
Err(_) => {
|
||||||
// let body = std::str::from_utf8(res.as_ref())?; // .as_ref converts Bytes to [u8]
|
#[cfg(debug_assertions)]
|
||||||
|
dbg!(format!("{} - Failed to parse page JSON data", url));
|
||||||
// --- surf ---
|
Err("Failed to parse page JSON data")
|
||||||
// let req = get(&url).header("User-Agent", "libreddit");
|
}
|
||||||
// let client = client().with(Redirect::new(5));
|
}
|
||||||
// let mut res = client.send(req).await.unwrap();
|
}
|
||||||
// let success = res.status().is_success();
|
// If Reddit returns error, tell user Page Not Found
|
||||||
// let body = res.body_string().await.unwrap();
|
false => {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
// --- reqwest ---
|
dbg!(format!("{} - Page not found", url));
|
||||||
let res = reqwest::get(&url).await.unwrap();
|
Err("Page not found")
|
||||||
// Read the status from the response
|
}
|
||||||
let success = res.status().is_success();
|
}
|
||||||
// Read the body of the response
|
}
|
||||||
let body = res.text().await.unwrap();
|
// If can't send request to Reddit, return this to user
|
||||||
|
Err(_e) => {
|
||||||
// Parse the response from Reddit as JSON
|
#[cfg(debug_assertions)]
|
||||||
let json: Value = from_str(body.as_str()).unwrap_or(Value::Null);
|
dbg!(format!("{} - {}", url, _e));
|
||||||
|
Err("Couldn't send request to Reddit")
|
||||||
if !success {
|
}
|
||||||
println!("! {} - {}", url, "Page not found");
|
|
||||||
Err("Page not found")
|
|
||||||
} else if json == Value::Null {
|
|
||||||
println!("! {} - {}", url, "Failed to parse page JSON data");
|
|
||||||
Err("Failed to parse page JSON data")
|
|
||||||
} else {
|
|
||||||
Ok(json)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
594
static/style.css
594
static/style.css
@ -2,51 +2,80 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--accent: aqua;
|
--accent: aqua;
|
||||||
--background: #0F0F0F;
|
--text: white;
|
||||||
--foreground: #222;
|
--foreground: #222;
|
||||||
|
--background: #0F0F0F;
|
||||||
--outside: #1F1F1F;
|
--outside: #1F1F1F;
|
||||||
--post: #161616;
|
--post: #161616;
|
||||||
--highlighted: #333;
|
--highlighted: #333;
|
||||||
--black-contrast: 0 1px 3px rgba(0,0,0,0.5);
|
--shadow: 0 1px 3px rgba(0,0,0,0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
::selection {
|
||||||
transition: 0.2s all;
|
color: var(--background);
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: white;
|
color: var(--text);
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
visibility: visible !important;
|
font-size: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
padding: 15px;
|
padding: 5px 15px;
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
min-height: 40px;
|
||||||
|
position: fixed;
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
top: 0;
|
||||||
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#lib, #github {
|
nav * { color: var(--text); }
|
||||||
color: white;
|
nav #reddit { color: var(--accent); }
|
||||||
|
nav #version { opacity: 25%; }
|
||||||
|
|
||||||
|
#settings_link {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-left: 20px;
|
||||||
|
opacity: 0.8;
|
||||||
}
|
}
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
max-width: 750px;
|
max-width: 1000px;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
margin: 20px auto;
|
margin: 60px auto 20px auto
|
||||||
|
}
|
||||||
|
|
||||||
|
#column_one {
|
||||||
|
max-width: 750px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer > a {
|
||||||
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -62,6 +91,7 @@ hr {
|
|||||||
a {
|
a {
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
transition: 0.2s all;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:not(.post_right):hover {
|
a:not(.post_right):hover {
|
||||||
@ -74,13 +104,17 @@ img[src=""] {
|
|||||||
|
|
||||||
aside {
|
aside {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
margin: 20px 20px 0 20px;
|
margin: 20px 20px 0 10px;
|
||||||
max-width: 350px;
|
max-width: 350px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#version {
|
.post, .panel {
|
||||||
color: white;
|
border: 1px solid var(--highlighted);
|
||||||
opacity: 25%;
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
font-size: 12px;
|
||||||
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* User & Subreddit */
|
/* User & Subreddit */
|
||||||
@ -90,22 +124,20 @@ aside {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 20px;
|
|
||||||
height: max-content;
|
height: max-content;
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar, #sidebar_contents {
|
#user *, #subreddit * { text-align: center; }
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#sidebar_label {
|
#user, #sub_meta, #sidebar_contents { padding: 20px; }
|
||||||
border: 2px solid var(--highlighted);
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#user_icon, #subreddit_icon {
|
#sidebar, #sidebar_contents { margin-top: 10px; }
|
||||||
|
#sidebar_label { padding: 10px; }
|
||||||
|
|
||||||
|
#user_icon, #sub_icon {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
border: 2px solid var(--accent);
|
border: 2px solid var(--accent);
|
||||||
@ -114,86 +146,198 @@ aside {
|
|||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_name, #subreddit_name {
|
#user_title, #sub_title {
|
||||||
margin-top: 10px;
|
margin: 0 20px;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_description, #subreddit_description {
|
#user_description, #sub_description {
|
||||||
margin: 10px 20px;
|
margin: 0 20px;
|
||||||
text-align: center;
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_details, #subreddit_details {
|
#user_name, #user_description:not(:empty), #user_icon
|
||||||
|
#sub_name, #sub_icon, #sub_description:not(:empty) {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user_details, #sub_details {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
margin-top: 15px;
|
|
||||||
grid-column-gap: 20px;
|
grid-column-gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#user_details > label, #subreddit_details > label {
|
#user_details > label, #sub_details > label {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sorting */
|
/* Wiki Pages */
|
||||||
|
|
||||||
#sort {
|
#wiki {
|
||||||
background: var(--outside);
|
background: var(--foreground);
|
||||||
box-shadow: var(--black-contrast);
|
padding: 35px;
|
||||||
border: 0;
|
|
||||||
padding: 0 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
height: 40px;
|
|
||||||
font-size: 15px;
|
|
||||||
border-radius: 5px 0 0 5px;
|
|
||||||
appearance: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort_submit {
|
#top {
|
||||||
background: var(--highlighted);
|
background: var(--highlighted);
|
||||||
border: 0;
|
width: 100%;
|
||||||
font-size: 15px;
|
display: flex;
|
||||||
height: 40px;
|
|
||||||
border-radius: 0 5px 5px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort:hover { background: var(--foreground); }
|
#top > * {
|
||||||
#sort_submit:hover { color: var(--accent); }
|
flex-grow: 1;
|
||||||
|
text-align: center;
|
||||||
|
height: 35px;
|
||||||
|
line-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
#sort > div, footer > a {
|
#top > div {
|
||||||
box-shadow: var(--black-contrast);
|
border-bottom: 2px solid var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sorting and Search */
|
||||||
|
|
||||||
|
select {
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
color: lightgrey;
|
transition: 0.2s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
select, #search {
|
||||||
|
border: none;
|
||||||
|
padding: 0 15px;
|
||||||
|
height: 40px;
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
border-radius: 5px 0px 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox {
|
||||||
|
display: flex;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
#searchbox > *, #sort_submit {
|
||||||
|
background: var(--highlighted);
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search {
|
||||||
|
border-right: 2px var(--outside) solid;
|
||||||
|
min-width: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#inside {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-right: 2px var(--outside) solid;
|
||||||
|
height: 40px;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#restrict_sr { margin-right: 5px; }
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0px 5px 5px 0px;
|
||||||
|
transition: 0.2s all;
|
||||||
|
}
|
||||||
|
|
||||||
|
select:hover { background: var(--foreground); }
|
||||||
|
input[type="submit"]:hover { color: var(--accent); }
|
||||||
|
|
||||||
|
#timeframe {
|
||||||
|
margin: 0 2px;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort_options + #timeframe:not(#search_sort > #timeframe) {
|
||||||
|
margin-left: 10px;
|
||||||
|
border-radius: 5px 0px 0px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_sort {
|
||||||
|
background: var(--highlighted);
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
margin-right: 5px;
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_sort > #search {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#search_sort > *, #searchbox > * { font-size: 15px; }
|
||||||
|
|
||||||
|
#search_sort > :not(:first-child), #search_sort > #sort_options {
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-left: 2px solid var(--background);
|
||||||
|
box-shadow: none;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort_options {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort, #search_sort {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort_options, footer > a {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
background: var(--outside);
|
||||||
|
display: flex;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sort_options > a, footer > a {
|
||||||
|
color: lightgrey;
|
||||||
padding: 10px 20px;
|
padding: 10px 20px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
transition: 0.2s all;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort > div.selected {
|
#sort_options > a.selected {
|
||||||
background: var(--accent);
|
background: var(--accent);
|
||||||
color: black;
|
color: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
#sort > div:hover {
|
#sort_options > a:not(.selected):hover {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Post */
|
/* Post */
|
||||||
|
|
||||||
|
.thread {
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
.post {
|
.post {
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background: var(--post);
|
background: var(--post);
|
||||||
box-shadow: var(--black-contrast);
|
box-shadow: var(--shadow);
|
||||||
display: flex;
|
display: flex;
|
||||||
|
transition: 0.2s all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post:not(:last-child) { margin-bottom: 10px; }
|
||||||
|
|
||||||
.post.highlighted {
|
.post.highlighted {
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.post.highlighted > .post_right {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
.post:hover {
|
.post:hover {
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
}
|
}
|
||||||
@ -204,22 +348,99 @@ aside {
|
|||||||
|
|
||||||
.post_left, .post_right {
|
.post_left, .post_right {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
overflow-wrap: break-word;
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_left {
|
.post_left {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
border-radius: 5px 0 0 5px;
|
border-radius: 5px 0 0 5px;
|
||||||
|
flex-direction: column;
|
||||||
min-width: 50px;
|
min-width: 50px;
|
||||||
padding: 5px;
|
transition: 0.2s all;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_score {
|
.post_score {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
#post_footer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#post_links {
|
||||||
|
display: flex;
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
#post_links > li {
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_subreddit {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_title {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_text {
|
||||||
|
padding: 15px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_right {
|
||||||
|
flex-grow: 1;
|
||||||
|
flex-shrink: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_right > * {
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_media {
|
||||||
|
max-width: 90%;
|
||||||
|
align-self: center;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_body {
|
||||||
|
opacity: 0.9;
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#post_url {
|
||||||
|
color: var(--accent);
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_thumbnail {
|
||||||
|
object-fit: cover;
|
||||||
|
width: auto;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--foreground);
|
||||||
|
max-width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post_flair {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--background);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nsfw {
|
.nsfw {
|
||||||
@ -232,65 +453,17 @@ aside {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_subreddit {
|
.stickied {
|
||||||
font-weight: bold;
|
--accent: #5cff85;
|
||||||
}
|
border: 1px solid #5cff85;
|
||||||
|
|
||||||
.post_title {
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_right {
|
|
||||||
padding: 20px 25px;
|
|
||||||
flex-grow: 1;
|
|
||||||
flex-shrink: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_right > * {
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_media {
|
|
||||||
max-width: 90%;
|
|
||||||
align-self: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_body {
|
|
||||||
opacity: 0.9;
|
|
||||||
font-weight: normal;
|
|
||||||
margin: 10px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#post_url {
|
|
||||||
color: var(--accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_thumbnail {
|
|
||||||
object-fit: cover;
|
|
||||||
width: auto;
|
|
||||||
flex-shrink: 0;
|
|
||||||
padding: 10px;
|
|
||||||
border-radius: 15px;
|
|
||||||
max-width: 20%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.post_flair {
|
|
||||||
background: var(--accent);
|
|
||||||
color: black;
|
|
||||||
padding: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
border-radius: 5px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Comment */
|
/* Comment */
|
||||||
|
|
||||||
.comment {
|
.comment {
|
||||||
margin-top: 15px;
|
margin: 10px 0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
font-size: 15px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_left, .comment_right {
|
.comment_left, .comment_right {
|
||||||
@ -316,7 +489,7 @@ aside {
|
|||||||
|
|
||||||
.author_flair {
|
.author_flair {
|
||||||
background: var(--highlighted);
|
background: var(--highlighted);
|
||||||
color: white;
|
color: var(--text);
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
@ -339,13 +512,13 @@ aside {
|
|||||||
|
|
||||||
.comment_right {
|
.comment_right {
|
||||||
word-wrap: anywhere;
|
word-wrap: anywhere;
|
||||||
padding: 10px 25px 10px 5px;
|
padding: 10px 0 10px 5px;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_data > * {
|
.comment_data > * {
|
||||||
margin: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.comment_image {
|
.comment_image {
|
||||||
@ -383,7 +556,7 @@ aside {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datetime {
|
.datetime {
|
||||||
opacity: 0.75;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line {
|
.line {
|
||||||
@ -392,22 +565,127 @@ aside {
|
|||||||
background: var(--foreground);
|
background: var(--foreground);
|
||||||
}
|
}
|
||||||
|
|
||||||
.post.comment {
|
/* Layouts */
|
||||||
background: #000;
|
|
||||||
border: 2px solid var(--foreground);
|
#compact .post:not(.highlighted) {
|
||||||
|
border-radius: 0;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post.comment > .post_left {
|
#compact .post:first-of-type {
|
||||||
background: black;
|
border-radius: 5px 5px 0 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#compact .post:last-of-type {
|
||||||
|
border-radius: 0 0 5px 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post.highlighted {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post:not(:last-of-type):not(.highlighted):not(.stickied) {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post_left {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post_header {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post_title {
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post_text {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact .post_thumbnail {
|
||||||
|
max-width: 75px;
|
||||||
|
max-height: 75px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#compact footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#card .post_right {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
#card .post:not(.highlighted) .post_media {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings */
|
||||||
|
|
||||||
|
#settings {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#settings_note {
|
||||||
|
font-size: 14px;
|
||||||
|
max-width: 300px;
|
||||||
|
margin-top: 10px;
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prefs {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--post);
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prefs > div {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
height: 35px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prefs > div:not(:last-of-type) {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#prefs select {
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#save {
|
||||||
|
background: var(--highlighted);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="submit"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
}
|
||||||
/* Markdown */
|
/* Markdown */
|
||||||
|
|
||||||
.md > *:not(:first-child) {
|
.md > *:not(:first-child) {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md p { font-size: 15px; }
|
|
||||||
.md h1 { font-size: 22px; }
|
.md h1 { font-size: 22px; }
|
||||||
.md h2 { font-size: 20px; }
|
.md h2 { font-size: 20px; }
|
||||||
.md h3 { font-size: 18px; }
|
.md h3 { font-size: 18px; }
|
||||||
@ -416,24 +694,38 @@ aside {
|
|||||||
.md h6 { font-size: 12px; }
|
.md h6 { font-size: 12px; }
|
||||||
|
|
||||||
.md blockquote {
|
.md blockquote {
|
||||||
padding-left: 8px;
|
padding-left: 6px;
|
||||||
margin: 4px 0 4px 8px;
|
margin: 4px 0 4px 5px;
|
||||||
border-left: 4px solid var(--highlighted);
|
border-left: 4px solid var(--highlighted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.md a {
|
.md a, .md a * {
|
||||||
text-decoration: underline;
|
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.md .md-spoiler-text {
|
||||||
|
background: var(--highlighted);
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md .md-spoiler-text:hover {
|
||||||
|
background: var(--foreground);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
.md li { margin: 10px 0; }
|
.md li { margin: 10px 0; }
|
||||||
|
.toc_child { list-style: none; }
|
||||||
|
|
||||||
.md pre {
|
.md pre {
|
||||||
background: var(--outside);
|
background: var(--outside);
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
box-shadow: var(--black-contrast);
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.md table {
|
||||||
|
margin: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.md code {
|
.md code {
|
||||||
@ -462,20 +754,23 @@ td, th {
|
|||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_left {
|
.post_header {
|
||||||
border-radius: 0 0 5px 5px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_right {
|
.post_left {
|
||||||
padding: 20px;
|
border-radius: 0 0 5px 5px;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nsfw {
|
||||||
|
margin: 5px 0px 5px 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.post_score {
|
.post_score {
|
||||||
margin-top: 0;
|
margin: 5px 0;
|
||||||
}
|
|
||||||
|
|
||||||
.post_thumbnail {
|
|
||||||
max-width: initial;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.replies > .comment {
|
.replies > .comment {
|
||||||
@ -491,14 +786,23 @@ td, th {
|
|||||||
@media screen and (max-width: 800px) {
|
@media screen and (max-width: 800px) {
|
||||||
main {
|
main {
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 100px 0 10px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
aside {
|
nav {
|
||||||
margin: 20px 0 0 0;
|
flex-direction: column;
|
||||||
|
padding: 10px;
|
||||||
|
width: calc(100% - 20px);
|
||||||
|
}
|
||||||
|
|
||||||
|
aside, #subreddit, #user {
|
||||||
|
margin: 0;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#sidebar {
|
#user, #sidebar { margin: 20px 0; }
|
||||||
margin: 20px 0;
|
#logo { margin: 5px auto; }
|
||||||
}
|
#searchbox { width: 100%; }
|
||||||
|
#github { display: none; }
|
||||||
}
|
}
|
@ -10,15 +10,22 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<link rel="stylesheet" href="/style.css">
|
<link rel="stylesheet" href="/style.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body style="visibility: hidden;">
|
<body id="{% block layout %}{% endblock %}">
|
||||||
{% block navbar %}
|
<!-- NAVIGATION BAR -->
|
||||||
<nav>
|
<nav>
|
||||||
<a href="/"><span id="lib">lib</span>reddit. <span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span></a>
|
<p id="logo">
|
||||||
|
<a id="libreddit" href="/">
|
||||||
|
<span id="lib">lib</span><span id="reddit">reddit.</span>
|
||||||
|
</a>
|
||||||
|
<span id="version">v{{ env!("CARGO_PKG_VERSION") }}</span>
|
||||||
|
<a id="settings_link" href="/settings">settings</a>
|
||||||
|
</p>
|
||||||
|
{% block search %}{% endblock %}
|
||||||
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
<a id="github" href="https://github.com/spikecodes/libreddit">GITHUB</a>
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
|
<!-- MAIN CONTENT -->
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<main>
|
<main>
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block content %}
|
|
||||||
<div id="column_one">
|
|
||||||
<form>
|
|
||||||
<select id="sort" name="sort">
|
|
||||||
<option value="confidence" {% if sort == "confidence" %}selected{% endif %}>Best</option>
|
|
||||||
<option value="hot" {% if sort == "hot" %}selected{% endif %}>Hot</option>
|
|
||||||
<option value="new" {% if sort == "new" %}selected{% endif %}>New</option>
|
|
||||||
<option value="top" {% if sort == "top" %}selected{% endif %}>Top</option>
|
|
||||||
</select><input id="sort_submit" type="submit" value="→">
|
|
||||||
</form>
|
|
||||||
{% for post in posts %}
|
|
||||||
<div class="post">
|
|
||||||
<div class="post_left">
|
|
||||||
<p class="post_score">{{ post.score }}</p>
|
|
||||||
{% if post.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="post_right">
|
|
||||||
<p>
|
|
||||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
|
||||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
|
||||||
{% if post.author_flair.0 != "" %}
|
|
||||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
|
||||||
{% endif %}
|
|
||||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="post_title">
|
|
||||||
{% if post.flair.0 != "" %}
|
|
||||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<img class="post_thumbnail" src="{{ post.media }}">
|
|
||||||
</div><br>
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<footer>
|
|
||||||
{% if ends.0 != "" %}
|
|
||||||
<a href="?sort={{ sort }}&before={{ ends.0 }}">PREV</a>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if ends.1 != "" %}
|
|
||||||
<a href="?sort={{ sort }}&after={{ ends.1 }}">NEXT</a>
|
|
||||||
{% endif %}
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
@ -1,12 +1,20 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
{% block title %}{{ post.title }} - r/{{ post.community }}{% endblock %}
|
{% block title %}{{ post.title }} - r/{{ post.community }}{% endblock %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search(["/r/", post.community.as_str()].concat(), "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block root %}/r/{{ post.community }}{% endblock %}{% block location %}r/{{ post.community }}{% endblock %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
{% call super() %}
|
{% call super() %}
|
||||||
<meta name="author" content="u/{{ post.author }}">
|
<meta name="author" content="u/{{ post.author }}">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- OPEN COMMENT MACRO -->
|
||||||
{% macro comment(item) -%}
|
{% macro comment(item) -%}
|
||||||
|
|
||||||
<div id="{{ item.id }}" class="comment">
|
<div id="{{ item.id }}" class="comment">
|
||||||
<div class="comment_left">
|
<div class="comment_left">
|
||||||
<p class="comment_score">{{ item.score }}</p>
|
<p class="comment_score">{{ item.score }}</p>
|
||||||
@ -17,78 +25,98 @@
|
|||||||
{% if item.flair.0 != "" %}
|
{% if item.flair.0 != "" %}
|
||||||
<small class="author_flair">{{ item.flair.0 }}</small>
|
<small class="author_flair">{{ item.flair.0 }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
• <span class="datetime">{{ item.time }}</span>
|
<span class="datetime">{{ item.time }}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<p class="comment_body">{{ item.body }}</p>
|
<p class="comment_body">{{ item.body }}</p>
|
||||||
|
|
||||||
{%- endmacro %}
|
{%- endmacro %}
|
||||||
|
|
||||||
|
<!-- CLOSE COMMENT MACRO -->
|
||||||
|
{% macro close() %}
|
||||||
|
</details></div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
|
|
||||||
|
<!-- POST CONTENT -->
|
||||||
<div class="post highlighted">
|
<div class="post highlighted">
|
||||||
<div class="post_left">
|
<div class="post_left">
|
||||||
<p class="post_score">{{ post.score }}</p>
|
<p class="post_score">{{ post.score }}</p>
|
||||||
{% if post.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="post_right">
|
<div class="post_right">
|
||||||
<p>
|
<div class="post_text">
|
||||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
<p class="post_header">
|
||||||
•
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
<span class="dot">•</span>
|
||||||
{% if post.author_flair.0 != "" %}
|
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
{% if post.author_flair.0 != "" %}
|
||||||
|
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
<span class="dot">•</span>
|
||||||
|
<span class="datetime">{{ post.time }}</span>
|
||||||
|
</p>
|
||||||
|
<a href="{{ post.permalink }}" class="post_title">
|
||||||
|
{{ post.title }}
|
||||||
|
{% if post.flair.0 != "" %}
|
||||||
|
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- POST MEDIA -->
|
||||||
|
{% if post.post_type == "image" %}
|
||||||
|
<img class="post_media" src="{{ post.media }}"/>
|
||||||
|
{% else if post.post_type == "video" %}
|
||||||
|
<video class="post_media" src="{{ post.media }}" controls autoplay loop></video>
|
||||||
|
{% else if post.post_type == "link" %}
|
||||||
|
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="datetime">{{ post.time }}</span>
|
|
||||||
</p>
|
<!-- POST BODY -->
|
||||||
<a href="{{ post.url }}" class="post_title">
|
<div class="post_body">{{ post.body }}</div>
|
||||||
{{ post.title }}
|
<div id="post_footer">
|
||||||
{% if post.flair.0 != "" %}
|
<ul id="post_links">
|
||||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
<li><a href="/{{ post.id }}">permalink</a></li>
|
||||||
{% endif %}
|
<li><a href="https://reddit.com/{{ post.id }}">reddit</a></li>
|
||||||
</a>
|
</ul>
|
||||||
{% if post.post_type == "image" %}
|
<p>{{ post.upvote_ratio }}% Upvoted</p>
|
||||||
<img class="post_media" src="{{ post.media }}"/>
|
</div>
|
||||||
{% else if post.post_type == "video" %}
|
|
||||||
<video class="post_media" src="{{ post.media }}" controls autoplay loop>
|
</div>
|
||||||
{% else if post.post_type == "link" %}
|
|
||||||
<a id="post_url" href="{{ post.media }}">{{ post.media }}</a>
|
|
||||||
{% endif %}
|
|
||||||
<div class="post_body">{{ post.body }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form>
|
|
||||||
<select id="sort" name="sort">
|
<!-- SORT FORM -->
|
||||||
<option value="confidence" {% if sort == "confidence" %}selected{% endif %}>Best</option>
|
<form id="sort">
|
||||||
<option value="top" {% if sort == "top" %}selected{% endif %}>Top</option>
|
<select name="sort">
|
||||||
<option value="new" {% if sort == "new" %}selected{% endif %}>New</option>
|
{% call utils::options(sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||||
<option value="controversial" {% if sort == "controversial" %}selected{% endif %}>Controversial</option>
|
|
||||||
<option value="old" {% if sort == "old" %}selected{% endif %}>Old</option>
|
|
||||||
</select><input id="sort_submit" type="submit" value="→">
|
</select><input id="sort_submit" type="submit" value="→">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- COMMENTS -->
|
||||||
{% for c in comments -%}
|
{% for c in comments -%}
|
||||||
<div class="thread">
|
<div class="thread">
|
||||||
{% call comment(c) %}
|
<!-- EACH COMMENT -->
|
||||||
<div class="replies">
|
{% call comment(c) %}
|
||||||
{% for reply1 in c.replies %}
|
<div class="replies">{% for reply1 in c.replies %}{% call comment(reply1) %}
|
||||||
{% call comment(reply1) %}
|
<!-- FIRST-LEVEL REPLIES -->
|
||||||
<div class="replies">
|
<div class="replies">{% for reply2 in reply1.replies %}{% call comment(reply2) %}
|
||||||
{% for reply2 in reply1.replies %}
|
<!-- SECOND-LEVEL REPLIES -->
|
||||||
{% call comment(reply2) %}
|
<div class="replies">{% for reply3 in reply2.replies %}{% call comment(reply3) %}
|
||||||
<div class="replies">
|
<!-- THIRD-LEVEL REPLIES -->
|
||||||
{% for reply3 in reply2.replies %}
|
{% if reply3.replies.len() > 0 %}
|
||||||
{% call comment(reply3) %}
|
<!-- LINK TO CONTINUE REPLIES -->
|
||||||
{% if reply3.replies.len() > 0 %}
|
<a class="deeper_replies" href="{{ post.permalink }}{{ reply3.id }}">→ More replies</a>
|
||||||
<a class="deeper_replies" href="{{ post.url }}{{ reply3.id }}">→ More replies</a>
|
{% endif %}
|
||||||
{% endif %}
|
{% call close() %}
|
||||||
</details></div>
|
|
||||||
{% endfor %}
|
|
||||||
</div></details></div>
|
|
||||||
{% endfor %}
|
|
||||||
</div></details></div>
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div></details></div>
|
</div>{% call close() %}
|
||||||
</div>
|
{% endfor %}
|
||||||
|
</div>{% call close() %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>{% call close() %}
|
||||||
|
</div>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
88
templates/search.html
Normal file
88
templates/search.html
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
|
{% block title %}Libreddit: search results - {{ params.q }}{% endblock %}
|
||||||
|
|
||||||
|
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div id="column_one">
|
||||||
|
<form id="search_sort">
|
||||||
|
<input id="search" type="text" name="q" placeholder="Search" value="{{ params.q }}">
|
||||||
|
{% if sub != "" %}
|
||||||
|
<div id="inside">
|
||||||
|
<input type="checkbox" name="restrict_sr" id="restrict_sr" {% if params.restrict_sr != "" %}checked{% endif %}>
|
||||||
|
<label for="restrict_sr">in r/{{ sub }}</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<select id="sort_options" name="sort">
|
||||||
|
{% call utils::options(params.sort, ["relevance", "hot", "top", "new", "comments"], "") %}
|
||||||
|
</select>{% if params.sort != "new" %}<select id="timeframe" name="t">
|
||||||
|
{% call utils::options(params.t, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||||
|
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||||
|
</form>
|
||||||
|
{% for post in posts %}
|
||||||
|
|
||||||
|
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
|
||||||
|
{% else if post.title != "Comment" %}
|
||||||
|
<div class="post">
|
||||||
|
<div class="post_left">
|
||||||
|
<p class="post_score">{{ post.score }}</p>
|
||||||
|
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="post_right">
|
||||||
|
<div class="post_text">
|
||||||
|
<p class="post_header">
|
||||||
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
|
<span class="dot">•</span>
|
||||||
|
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||||
|
{% if post.author_flair.0 != "" %}
|
||||||
|
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
<span class="dot">•</span>
|
||||||
|
<span class="datetime">{{ post.time }}</span>
|
||||||
|
</p>
|
||||||
|
<p class="post_title">
|
||||||
|
{% if post.flair.0 != "" %}
|
||||||
|
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||||
|
{% endif %}
|
||||||
|
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POST THUMBNAIL -->
|
||||||
|
<img class="post_thumbnail" src="{{ post.thumbnail }}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="comment">
|
||||||
|
<div class="comment_left">
|
||||||
|
<p class="comment_score">{{ post.score }}</p>
|
||||||
|
<div class="line"></div>
|
||||||
|
</div>
|
||||||
|
<details class="comment_right" open>
|
||||||
|
<summary class="comment_data">
|
||||||
|
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||||
|
<span class="datetime">{{ post.time }}</span>
|
||||||
|
</summary>
|
||||||
|
<p class="comment_body">{{ post.body }}</p>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
{% if params.before != "" %}
|
||||||
|
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||||
|
&sort={{ params.sort }}&t={{ params.t }}
|
||||||
|
&before={{ params.before }}">PREV</a>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if params.after != "" %}
|
||||||
|
<a href="?q={{ params.q }}&restrict_sr={{ params.restrict_sr }}
|
||||||
|
&sort={{ params.sort }}&t={{ params.t }}
|
||||||
|
&after={{ params.after }}">NEXT</a>
|
||||||
|
{% endif %}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
41
templates/settings.html
Normal file
41
templates/settings.html
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
|
{% block title %}Libreddit Settings{% endblock %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search("".to_owned(), "", "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<main>
|
||||||
|
<form id="settings" action="/settings" method="POST">
|
||||||
|
<div id="prefs">
|
||||||
|
<div id="front_page">
|
||||||
|
<label for="front_page">Front page:</label>
|
||||||
|
<select name="front_page">
|
||||||
|
{% call utils::options(prefs.front_page, ["popular", "all"], "popular") %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="layout">
|
||||||
|
<label for="layout">Layout:</label>
|
||||||
|
<select name="layout">
|
||||||
|
{% call utils::options(prefs.layout, ["card", "clean", "compact"], "clean") %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="comment_sort">
|
||||||
|
<label for="comment_sort">Default comment sort:</label>
|
||||||
|
<select name="comment_sort">
|
||||||
|
{% call utils::options(prefs.comment_sort, ["confidence", "top", "new", "controversial", "old"], "confidence") %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="hide_nsfw">
|
||||||
|
<label for="hide_nsfw">Hide NSFW posts:</label>
|
||||||
|
<input type="checkbox" name="hide_nsfw" {% if prefs.hide_nsfw == "on" %}checked{% endif %}>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p id="settings_note"><b>Note:</b> settings are saved in browser cookies. Clearing your cookie data will reset them.</p>
|
||||||
|
<input id="save" type="submit" value="Save">
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
@ -1,69 +1,105 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
{% if sub.name != "" %}
|
{% block title %}
|
||||||
{% block title %}r/{{ sub.name }}: {{ sub.description }}{% endblock %}
|
{% if sub.title != "" %}{{ sub.title }}
|
||||||
{% endif %}
|
{% else if sub.name != "" %}{{ sub.name }}
|
||||||
|
{% else %}Libreddit{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search(["/r/", sub.name.as_str()].concat(), "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<main style="max-width: 1000px;">
|
<main>
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form>
|
<form id="sort">
|
||||||
<select id="sort" name="sort">
|
<div id="sort_options">
|
||||||
<option value="hot" {% if sort == "hot" %}selected{% endif %}>Hot</option>
|
{% if sub.name.is_empty() %}
|
||||||
<option value="new" {% if sort == "new" %}selected{% endif %}>New</option>
|
{% call utils::sort("", ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||||
<option value="top" {% if sort == "top" %}selected{% endif %}>Top</option>
|
{% else %}
|
||||||
</select><input id="sort_submit" type="submit" value="→">
|
{% call utils::sort(["/r/", sub.name.as_str()].concat(), ["hot", "new", "top", "rising", "controversial"], sort.0) %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if sort.0 == "top" || sort.0 == "controversial" %}<select id="timeframe" name="t">
|
||||||
|
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "day") %}
|
||||||
|
<input id="sort_submit" type="submit" value="→">
|
||||||
|
</select>{% endif %}
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="posts">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
<div class="post">
|
{% if !(post.flags.nsfw && prefs.hide_nsfw == "on") %}
|
||||||
|
<div class="post {% if post.flags.stickied %}stickied{% endif %}">
|
||||||
<div class="post_left">
|
<div class="post_left">
|
||||||
<p class="post_score">{{ post.score }}</p>
|
<p class="post_score">{{ post.score }}</p>
|
||||||
{% if post.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="post_right">
|
<div class="post_right">
|
||||||
<p>
|
<div class="post_text">
|
||||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
<p class="post_header">
|
||||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
{% if post.author_flair.0 != "" %}
|
<span class="dot">•</span>
|
||||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
<a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
||||||
{% endif %}
|
<span class="dot">•</span>
|
||||||
<span class="datetime">{{ post.time }}</span>
|
<span class="datetime">{{ post.time }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p class="post_title">
|
<p class="post_title">
|
||||||
{% if post.flair.0 != "" %}
|
{% if post.flair.0 != "" %}
|
||||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||||
</p>
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POST MEDIA/THUMBNAIL -->
|
||||||
|
{% if prefs.layout == "card" && post.post_type == "image" %}
|
||||||
|
<img class="post_media" src="{{ post.media }}"/>
|
||||||
|
{% else if prefs.layout != "card" %}
|
||||||
|
<img class="post_thumbnail" src="{{ post.thumbnail }}">
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<img class="post_thumbnail" src="{{ post.media }}">
|
</div>
|
||||||
</div><br>
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% if ends.0 != "" %}
|
||||||
<a href="?sort={{ sort }}&before={{ ends.0 }}">PREV</a>
|
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ends.1 != "" %}
|
{% if ends.1 != "" %}
|
||||||
<a href="?sort={{ sort }}&after={{ ends.1 }}">NEXT</a>
|
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
{% if sub.name != "" %}
|
{% if sub.name != "" %}
|
||||||
<aside>
|
<aside>
|
||||||
<div id="subreddit">
|
<div class="panel" id="subreddit">
|
||||||
<img id="subreddit_icon" src="{{ sub.icon }}">
|
{% if sub.wiki %}
|
||||||
<p id="subreddit_name">r/{{ sub.name }}</p>
|
<div id="top">
|
||||||
<p id="subreddit_description">{{ sub.description }}</p>
|
<div>Posts</div>
|
||||||
<div id="subreddit_details">
|
<a href="/r/{{ sub.name }}/wiki/index">Wiki</a>
|
||||||
<label>Members</label>
|
</div>
|
||||||
<label>Active</label>
|
{% endif %}
|
||||||
<div>{{ sub.members }}</div>
|
<div id="sub_meta">
|
||||||
<div>{{ sub.active }}</div>
|
<img id="sub_icon" src="{{ sub.icon }}">
|
||||||
|
<p id="sub_title">{{ sub.title }}</p>
|
||||||
|
<p id="sub_name">r/{{ sub.name }}</p>
|
||||||
|
<p id="sub_description">{{ sub.description }}</p>
|
||||||
|
<div id="sub_details">
|
||||||
|
<label>Members</label>
|
||||||
|
<label>Active</label>
|
||||||
|
<div>{{ sub.members }}</div>
|
||||||
|
<div>{{ sub.active }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<details id="sidebar">
|
<details class="panel" id="sidebar">
|
||||||
<summary id="sidebar_label">Sidebar</summary>
|
<summary id="sidebar_label">Sidebar</summary>
|
||||||
<div id="sidebar_contents">{{ sub.info }}</div>
|
<div id="sidebar_contents">{{ sub.info }}</div>
|
||||||
</details>
|
</details>
|
||||||
|
@ -1,42 +1,63 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Libreddit: u/{{ user.name }}{% endblock %}
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search("".to_owned(), "", "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block title %}{{ user.name.replace("u/", "") }} (u/{{ user.name }}) - Libreddit{% endblock %}
|
||||||
|
|
||||||
|
{% block layout %}{% if prefs.layout != "" %}{{ prefs.layout }}{% endif %}{% endblock %}
|
||||||
|
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<main style="max-width: 1000px;">
|
<main style="max-width: 1000px;">
|
||||||
<div id="column_one">
|
<div id="column_one">
|
||||||
<form>
|
<form id="sort">
|
||||||
<select id="sort" name="sort">
|
<select name="sort">
|
||||||
<option value="hot" {% if sort == "hot" %}selected{% endif %}>Hot</option>
|
{% call utils::options(sort.0, ["hot", "new", "top"], "") %}
|
||||||
<option value="new" {% if sort == "new" %}selected{% endif %}>New</option>
|
</select>{% if sort.0 == "top" %}<select id="timeframe" name="t">
|
||||||
<option value="top" {% if sort == "top" %}selected{% endif %}>Top</option>
|
{% call utils::options(sort.1, ["hour", "day", "week", "month", "year", "all"], "all") %}
|
||||||
</select><input id="sort_submit" type="submit" value="→">
|
</select>{% endif %}<input id="sort_submit" type="submit" value="→">
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<div id="posts">
|
||||||
{% for post in posts %}
|
{% for post in posts %}
|
||||||
{% if post.title != "Comment" %}
|
|
||||||
<div class='post'>
|
{% if post.flags.nsfw && prefs.hide_nsfw == "on" %}
|
||||||
|
{% else if post.title != "Comment" %}
|
||||||
|
<div class="post">
|
||||||
<div class="post_left">
|
<div class="post_left">
|
||||||
<p class="post_score">{{ post.score }}</p>
|
<p class="post_score">{{ post.score }}</p>
|
||||||
{% if post.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
{% if post.flags.nsfw %}<div class="nsfw">NSFW</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="post_right">
|
<div class="post_right">
|
||||||
<p>
|
<div class="post_text">
|
||||||
<b><a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a></b>
|
<p class="post_header">
|
||||||
• <a class="post_author" href="/u/{{ post.author }}">u/{{ post.author }}</a>
|
<a class="post_subreddit" href="/r/{{ post.community }}">r/{{ post.community }}</a>
|
||||||
{% if post.author_flair.0 != "" %}
|
{% if post.author_flair.0 != "" %}
|
||||||
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
<small class="author_flair">{{ post.author_flair.0 }}</small>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="datetime" style="float: right;">{{ post.time }}</span>
|
<span class="dot">•</span>
|
||||||
</p>
|
<span class="datetime">{{ post.time }}</span>
|
||||||
<p class="post_title">
|
</p>
|
||||||
{% if post.flair.0 == "Comment" %}
|
<p class="post_title">
|
||||||
{% else if post.flair.0 == "" %}
|
{% if post.flair.0 == "Comment" %}
|
||||||
{% else %}
|
{% else if post.flair.0 == "" %}
|
||||||
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
{% else %}
|
||||||
{% endif %}
|
<small class="post_flair" style="color:{{ post.flair.2 }}; background:{{ post.flair.1 }}">{{ post.flair.0 }}</small>
|
||||||
<a href="{{ post.url }}">{{ post.title }}</a>
|
{% endif %}
|
||||||
</p>
|
<a href="{{ post.permalink }}">{{ post.title }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- POST MEDIA/THUMBNAIL -->
|
||||||
|
{% if prefs.layout == "card" && post.post_type == "image" %}
|
||||||
|
<img class="post_media" src="{{ post.media }}"/>
|
||||||
|
{% else if prefs.layout != "card" %}
|
||||||
|
<img class="post_thumbnail" src="{{ post.thumbnail }}">
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<img class="post_thumbnail" src="{{ post.media }}">
|
</div>
|
||||||
</div><br>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="comment">
|
<div class="comment">
|
||||||
<div class="comment_left">
|
<div class="comment_left">
|
||||||
@ -45,27 +66,31 @@
|
|||||||
</div>
|
</div>
|
||||||
<details class="comment_right" open>
|
<details class="comment_right" open>
|
||||||
<summary class="comment_data">
|
<summary class="comment_data">
|
||||||
<a class="comment_link" href="{{ post.url }}">COMMENT</a>
|
<a class="comment_link" href="{{ post.permalink }}">COMMENT</a>
|
||||||
<span class="datetime">{{ post.time }}</span>
|
<span class="datetime">{{ post.time }}</span>
|
||||||
</summary>
|
</summary>
|
||||||
<p class="comment_body">{{ post.body }}</p>
|
<p class="comment_body">{{ post.body }}</p>
|
||||||
</details>
|
</details>
|
||||||
</div><br>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
{% if ends.0 != "" %}
|
{% if ends.0 != "" %}
|
||||||
<a href="?sort={{ sort }}&before={{ ends.0 }}">PREV</a>
|
<a href="?sort={{ sort.0 }}&before={{ ends.0 }}">PREV</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if ends.1 != "" %}
|
{% if ends.1 != "" %}
|
||||||
<a href="?sort={{ sort }}&after={{ ends.1 }}">NEXT</a>
|
<a href="?sort={{ sort.0 }}&after={{ ends.1 }}">NEXT</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<aside>
|
<aside>
|
||||||
<div id="user">
|
<div class="panel" id="user">
|
||||||
<img id="user_icon" src="{{ user.icon }}">
|
<img id="user_icon" src="{{ user.icon }}">
|
||||||
|
<p id="user_title">{{ user.title }}</p>
|
||||||
<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">
|
||||||
|
28
templates/utils.html
Normal file
28
templates/utils.html
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
{% macro options(current, values, default) -%}
|
||||||
|
{% for value in values %}
|
||||||
|
<option value="{{ value }}" {% if current == value || (current == "" && value == default) %}selected{% endif %}>
|
||||||
|
{{ format!("{}{}", value.get(0..1).unwrap().to_uppercase(), value.get(1..).unwrap()) }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro sort(root, methods, selected) -%}
|
||||||
|
{% for method in methods %}
|
||||||
|
<a {% if method == selected %}class="selected"{% endif %} href="{{ root }}/{{ method }}">
|
||||||
|
{{ format!("{}{}", method.get(0..1).unwrap().to_uppercase(), method.get(1..).unwrap()) }}
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
{%- endmacro %}
|
||||||
|
|
||||||
|
{% macro search(root, search) -%}
|
||||||
|
<form action="{% if root != "/r/" && !root.is_empty() %}{{ root }}{% endif %}/search/" id="searchbox">
|
||||||
|
<input id="search" type="text" name="q" placeholder="Search" value="{{ search }}">
|
||||||
|
{% if root != "/r/" && !root.is_empty() %}
|
||||||
|
<div id="inside">
|
||||||
|
<input type="checkbox" name="restrict_sr" id="restrict_sr">
|
||||||
|
<label for="restrict_sr">in {{ root }}</label>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<input type="submit" value="→">
|
||||||
|
</form>
|
||||||
|
{%- endmacro %}
|
25
templates/wiki.html
Normal file
25
templates/wiki.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% import "utils.html" as utils %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
{% if sub != "" %}{{ page }} - {{ sub }}
|
||||||
|
{% else %}Libreddit{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block search %}
|
||||||
|
{% call utils::search(["/r/", sub.as_str()].concat(), "") %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<main>
|
||||||
|
<div class="panel" id="column_one">
|
||||||
|
<div id="top">
|
||||||
|
<a href="/r/{{ sub }}">Posts</a>
|
||||||
|
<div>Wiki</div>
|
||||||
|
</div>
|
||||||
|
<div id="wiki">
|
||||||
|
{{ wiki }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{% endblock %}
|
Reference in New Issue
Block a user