Sort posts by age. Reload empty pages.

This commit is contained in:
John Stephani 2025-12-28 01:14:59 -06:00
parent 6ad0b5673e
commit dc5220fa05
3 changed files with 421 additions and 376 deletions

View File

@ -175,7 +175,7 @@ def front_page():
hidden = ? AND hidden = ? AND
saved = ? saved = ?
ORDER BY ORDER BY
score desc created_utc asc
LIMIT ? LIMIT ?
""" """
binds = [False, False, config.posts_per_page_load] binds = [False, False, config.posts_per_page_load]
@ -217,7 +217,7 @@ def other_page():
count <= ? count <= ?
) )
ORDER BY ORDER BY
score desc created_utc asc
LIMIT ? LIMIT ?
""" """
binds = [False, False, False, config.other_posts_cutoff, config.posts_per_page_load] binds = [False, False, False, config.other_posts_cutoff, config.posts_per_page_load]
@ -264,7 +264,7 @@ def get_subreddit(subreddit):
hidden = ? AND hidden = ? AND
saved = ? saved = ?
ORDER BY ORDER BY
score desc created_utc asc
LIMIT ? LIMIT ?
""" """
binds = [subreddit, False, False, config.posts_per_page_load] binds = [subreddit, False, False, config.posts_per_page_load]

View File

@ -4,7 +4,7 @@ max_age_seconds = max_age_days * 24 * 60 * 60
other_posts_cutoff = 4 #subreddits with this many unread posts or fewer are merged to /r/other other_posts_cutoff = 4 #subreddits with this many unread posts or fewer are merged to /r/other
# Webpage configuration # Webpage configuration
posts_per_page_load = 25 posts_per_page_load = 10
db_dir = "/reddit/db" db_dir = "/reddit/db"
media_dir = "/reddit/media" media_dir = "/reddit/media"

View File

@ -1,188 +1,222 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reddit, but better</title> <title>Reddit, but better</title>
<style> <style>
:root { :root {
--dark: #2c2c2c; --dark: #2c2c2c;
--darker: #171717; --darker: #171717;
--light: #bfbfbf; --light: #bfbfbf;
--confirm: #970000; --confirm: #970000;
--saved: #9f9400; --saved: #9f9400;
} }
html {
height: 100%; html {
} height: 100%;
body { }
background-color: var(--darker);
color: var(--light); body {
min-height: 100%; background-color: var(--darker);
margin: 0; /* Removes default browser margin */ color: var(--light);
} min-height: 100%;
img, video { margin: 0;
max-width: 100%; /* Removes default browser margin */
max-height: 80vh; }
width: auto;
height: auto; img,
} video {
div.post { max-width: 100%;
background-color: var(--dark); max-height: 80vh;
border: 2px solid var(--light); width: auto;
border-radius: 15px; height: auto;
padding: 10px; }
margin-bottom: 20px;
} div.post {
.sidebar { background-color: var(--dark);
outline: 2px solid var(--light); border: 2px solid var(--light);
position: sticky; border-radius: 15px;
top: 0; padding: 10px;
left: 0; margin-bottom: 20px;
background-color: var(--dark); }
display: flex;
flex-wrap: nowrap; .sidebar {
z-index: 1000; outline: 2px solid var(--light);
} position: sticky;
.content { top: 0;
display: flex; left: 0;
flex-grow: 1; /* Takes up remaining space */ background-color: var(--dark);
flex-direction: column; display: flex;
} flex-wrap: nowrap;
/* desktop */ z-index: 1000;
@media (min-aspect-ratio: 1) { }
div.post {
width: 70vw; .content {
} display: flex;
.container { flex-grow: 1;
display: flex; /* Takes up remaining space */
flex-direction: row; flex-direction: column;
} }
.sidebar {
width: fit-content; /* desktop */
height: 100vh; @media (min-aspect-ratio: 1) {
flex-direction: column; div.post {
overflow-y: auto; width: 70vw;
padding: 5px; }
margin-right: 20px;
} .container {
} display: flex;
/* phone */ flex-direction: row;
@media (max-aspect-ratio: 1) { }
div.post {
width: calc(100vw - 50px); .sidebar {
margin-top: 10px; width: fit-content;
} height: 100vh;
.container { flex-direction: column;
display: flex; overflow-y: auto;
flex-direction: column; padding: 5px;
} margin-right: 20px;
.sidebar { }
width: 100vw; }
height: 50px;
flex-direction: row; /* phone */
overflow-x: auto; @media (max-aspect-ratio: 1) {
align-items: center; div.post {
padding-top: 5px; width: calc(100vw - 50px);
padding-bottom: 5px; margin-top: 10px;
margin-bottom: 10px; }
}
.content { .container {
align-items: center; display: flex;
} flex-direction: column;
} }
.sidebar a {
display: block; .sidebar {
color: var(--light); width: 100vw;
text-decoration: none; height: 50px;
white-space: nowrap; flex-direction: row;
margin: 5px; overflow-x: auto;
padding: 5px; align-items: center;
} padding-top: 5px;
.sidebar a:hover { padding-bottom: 5px;
background-color: var(--darker); margin-bottom: 10px;
color: var(--light); }
}
.content {
align-items: center;
}
}
.sidebar a {
display: block;
color: var(--light);
text-decoration: none;
white-space: nowrap;
margin: 5px;
padding: 5px;
}
.sidebar a:hover {
background-color: var(--darker);
color: var(--light);
}
a.no-style-link { a.no-style-link {
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
cursor: pointer; cursor: pointer;
} }
.invert {
filter: invert(1); .invert {
transition: filter 0.3s; filter: invert(1);
} transition: filter 0.3s;
.button-wrapper { }
display: flex;
width: 100%; .button-wrapper {
gap: 10px; display: flex;
margin-top: 10px; width: 100%;
} gap: 10px;
.button-wrapper.gallery { margin-top: 10px;
gap: 5px; }
}
.button-wrapper button { .button-wrapper.gallery {
flex: 1; gap: 5px;
padding: 10px; }
cursor: pointer;
background-color: var(--darker); .button-wrapper button {
color: var(--light); flex: 1;
border: 2px solid var(--light); padding: 10px;
border-radius: 10px; cursor: pointer;
font-size: 1.25rem; background-color: var(--darker);
font-weight: bold; color: var(--light);
} border: 2px solid var(--light);
.button-wrapper button.confirm { border-radius: 10px;
background-color: var(--confirm) font-size: 1.25rem;
} font-weight: bold;
.button-wrapper button.saved { }
background-color: var(--saved)
} .button-wrapper button.confirm {
.button-wrapper button.gallery { background-color: var(--confirm)
padding: 5px; }
background-color: var(--darker);
border-radius: 5px; .button-wrapper button.saved {
border: none; background-color: var(--saved)
cursor: none; }
}
.button-wrapper button.gallery.selected { .button-wrapper button.gallery {
background-color: var(--light); padding: 5px;
} background-color: var(--darker);
.text-content { border-radius: 5px;
overflow: hidden; border: none;
transition: max-height 0.3s ease-out; /* Smooth transition */ cursor: none;
max-height: 20vh; }
position: relative;
} .button-wrapper button.gallery.selected {
.text-content::after { background-color: var(--light);
content: ""; }
position: absolute;
bottom: 0; .text-content {
left: 0; overflow: hidden;
width: 100%; transition: max-height 0.3s ease-out;
height: 30px; /* Smooth transition */
background: linear-gradient(to bottom, rgba(255,255,255,0), var(--dark)); max-height: 20vh;
} position: relative;
.text-content.expanded { }
max-height: 1000vh;
} .text-content::after {
.text-content.expanded::after { content: "";
display: none; position: absolute;
} bottom: 0;
</style> left: 0;
width: 100%;
height: 30px;
background: linear-gradient(to bottom, rgba(255, 255, 255, 0), var(--dark));
}
.text-content.expanded {
max-height: 1000vh;
}
.text-content.expanded::after {
display: none;
}
</style>
</head> </head>
<body> <body>
<div class="container"> <div class="container">
<div class="sidebar"> <div class="sidebar">
{% for link in sidebar_links %} {% for link in sidebar_links %}
<a href="{{ link }}">{{ link }}</a> <a href="{{ link }}">{{ link }}</a>
{% endfor %} {% endfor %}
</div> </div>
<div class="content"> <div class="content">
<h1>{{ title }}</h1> <h1>{{ title }}</h1>
{% for post in posts %} {% for post in posts %}
<div class="post"> <div class="post">
<h3>{{ post.title }}</h3> <h3>{{ post.title }}</h3>
<span> <span>
{% if post.subreddit %} {% if post.subreddit %}
<h5> <h5>
@ -196,219 +230,230 @@
</h5> </h5>
{% endif %} {% endif %}
</span> </span>
{% if post.media_html|length > 0 %} {% if post.media_html|length > 0 %}
<div class="media-div"> <div class="media-div">
{% for media in post.media_html %} {% for media in post.media_html %}
{{ media|safe }} {{ media|safe }}
{% endfor %} {% endfor %}
{% if post.media_html|length > 1 %} {% if post.media_html|length > 1 %}
<span class="button-wrapper gallery"> <span class="button-wrapper gallery">
</span> </span>
{% endif %} {% endif %}
</div> </div>
{% endif %} {% endif %}
{% if post.body %} {% if post.body %}
<div class="text-content" onclick="expand(this)"> <div class="text-content" onclick="expand(this)">
{{ post.body }} {{ post.body }}
</div> </div>
{% endif %} {% endif %}
<span class="button-wrapper"> <span class="button-wrapper">
<button type="button" name="comments" onpointerdown='commentsDown(this, "{{ post.permalink }}")' onpointerup='commentsUp(this, "{{ post.permalink }}")'>Comments</button> <button type="button" name="comments" onpointerdown='commentsDown(this, "{{ post.permalink }}")'
<button type="button" name="save" onclick='save(this, "{{ post.permalink }}")' {% if saved %}class='saved'{% endif %}>Save</button> onpointerup='commentsUp(this, "{{ post.permalink }}")'>Comments</button>
<button type="button" name="hide" onclick='hide(this, "{{ post.permalink }}")'>Hide</button> <button type="button" name="save" onclick='save(this, "{{ post.permalink }}")' {% if saved %}class='saved' {% endif %}>Save</button>
</span> {% if not saved %}
</div> <button type="button" name="hide" onclick='hide(this, "{{ post.permalink }}")'>Hide</button>
{% endfor %} {% endif %}
</div> </span>
</div> </div>
<script> {% endfor %}
// setup galleries </div>
mediaDivs = document.querySelectorAll('.media-div'); </div>
<script>
// setup galleries
mediaDivs = document.querySelectorAll('.media-div');
mediaDivs.forEach(div => { mediaDivs.forEach(div => {
images = Array.from(div.querySelectorAll('img')); images = Array.from(div.querySelectorAll('img'));
if (images.length > 1) { if (images.length > 1) {
buttonSpan = div.querySelector('.button-wrapper:first-of-type'); buttonSpan = div.querySelector('.button-wrapper:first-of-type');
images.forEach(image => { images.forEach(image => {
image.addEventListener('click', (e) => { image.addEventListener('click', (e) => {
if (e.offsetX > image.offsetWidth * 2 / 3) { if (e.offsetX > image.offsetWidth * 2 / 3) {
// scroll right // scroll right
div = e.target.closest('.media-div'); div = e.target.closest('.media-div');
images = Array.from(div.querySelectorAll('img')); images = Array.from(div.querySelectorAll('img'));
currentIndex = images.indexOf(e.target) currentIndex = images.indexOf(e.target)
if (currentIndex < (images.length -1)) { if (currentIndex < (images.length - 1)) {
buttons = Array.from(div.querySelectorAll('button')); buttons = Array.from(div.querySelectorAll('button'));
images[currentIndex].style.display = "none"; images[currentIndex].style.display = "none";
images[currentIndex+1].style.display = "block"; images[currentIndex + 1].style.display = "block";
buttons[currentIndex].classList.remove('selected'); buttons[currentIndex].classList.remove('selected');
buttons[currentIndex+1].classList.add('selected'); buttons[currentIndex + 1].classList.add('selected');
} else { } else {
e.target.classList.toggle('invert'); e.target.classList.toggle('invert');
} }
} else if (e.offsetX < image.offsetWidth / 3) { } else if (e.offsetX < image.offsetWidth / 3) {
// scroll left // scroll left
div = e.target.closest('.media-div'); div = e.target.closest('.media-div');
images = Array.from(div.querySelectorAll('img')); images = Array.from(div.querySelectorAll('img'));
currentIndex = images.indexOf(e.target) currentIndex = images.indexOf(e.target)
if (currentIndex > 0) { if (currentIndex > 0) {
buttons = Array.from(div.querySelectorAll('button')); buttons = Array.from(div.querySelectorAll('button'));
images[currentIndex].style.display = "none"; images[currentIndex].style.display = "none";
images[currentIndex-1].style.display = "block"; images[currentIndex - 1].style.display = "block";
buttons[currentIndex].classList.remove('selected'); buttons[currentIndex].classList.remove('selected');
buttons[currentIndex-1].classList.add('selected'); buttons[currentIndex - 1].classList.add('selected');
} else { } else {
e.target.classList.toggle('invert'); e.target.classList.toggle('invert');
} }
} else { } else {
image.classList.toggle('invert'); image.classList.toggle('invert');
} }
}); });
}); });
firstImage = images.shift(); firstImage = images.shift();
firstButton = document.createElement('button'); firstButton = document.createElement('button');
firstButton.classList.add('gallery'); firstButton.classList.add('gallery');
firstButton.classList.add('selected'); firstButton.classList.add('selected');
buttonSpan.appendChild(firstButton); buttonSpan.appendChild(firstButton);
images.forEach(image => { images.forEach(image => {
image.style.display = "none"; image.style.display = "none";
button = document.createElement('button'); button = document.createElement('button');
button.classList.add('gallery'); button.classList.add('gallery');
buttonSpan.appendChild(button); buttonSpan.appendChild(button);
}); });
} else { } else {
images.forEach(image => { images.forEach(image => {
image.addEventListener('click', () => { image.addEventListener('click', () => {
image.classList.toggle('invert'); image.classList.toggle('invert');
}); });
}); });
} }
}); });
// main button code // main button code
function hide(button, permalink){ function hide(button, permalink) {
div = button.closest('.post'); div = button.closest('.post');
hideButton = div.querySelector('[name="save"]'); hideButton = div.querySelector('[name="save"]');
if (hideButton.classList.contains('saved')){ if (hideButton.classList.contains('saved')) {
return; return;
} }
if (button.classList.contains('confirm')) { if (button.classList.contains('confirm')) {
div = button.closest('.post');
div.remove();
try {
fetch('/hide' + permalink);
} catch (error) {
console.error('Could not hide', error);
}
} else {
button.classList.add('confirm');
}
}
commentTimer = null;
function commentsDown(button, permalink) {
commentTimer = setTimeout(() => {
navigator.clipboard.writeText("https://reddit.com" + permalink);
commentTimer = null;
button.textContent = "Copied!"
}, 500);
}
function commentsUp(button, permalink) {
if (commentTimer != null) {
clearTimeout(commentTimer);
commentTimer = null;
pauseVideo(button);
window.open("https://reddit.com" + permalink, '_blank');
} else {
setTimeout(() => {
button.textContent = "Comments";
}, 500);
}
}
function save(button, permalink) {
if (button.classList.contains("saved")) {
button.classList.remove("saved");
try {
fetch('/save' + permalink + '?action=unsave');
} catch (error) {
console.error('Could not unsave', error);
}
} else {
button.classList.add("saved");
div = button.closest('.post'); div = button.closest('.post');
hideButton = div.querySelector('[name="hide"]'); div.remove();
hideButton.classList.remove("confirm"); try {
try { fetch('/hide' + permalink)
fetch('/save' + permalink + '?action=save'); .then(checkPostCount());
} catch (error) { } catch (error) {
console.error('Could not save', error); console.error('Could not hide', error);
} }
} else {
button.classList.add('confirm');
} }
} }
// text expand code
function checkHeight(){ commentTimer = null;
const divs = document.querySelectorAll('.text-content');
divs.forEach(div => {
height = div.offsetHeight;
style = window.getComputedStyle(div);
maxHeight = parseInt(style.maxHeight);
if (height < maxHeight) {
div.classList.add('expanded');
}
});
}
function expand(div) { function commentsDown(button, permalink) {
div.classList.add('expanded'); commentTimer = setTimeout(() => {
} navigator.clipboard.writeText("https://reddit.com" + permalink);
commentTimer = null;
button.textContent = "Copied!"
}, 500);
}
window.addEventListener('load', (event) => { function commentsUp(button, permalink) {
checkHeight() if (commentTimer != null) {
}); clearTimeout(commentTimer);
commentTimer = null;
pauseVideo(button);
window.open("https://reddit.com" + permalink, '_blank');
} else {
setTimeout(() => {
button.textContent = "Comments";
}, 500);
}
}
window.addEventListener('resize', (event) => { function save(button, permalink) {
checkHeight() div = button.closest('.post');
}); div.remove();
if (button.classList.contains("saved")) {
try {
fetch('/save' + permalink + '?action=unsave')
.then(checkPostCount());
} catch (error) {
console.error('Could not unsave', error);
}
} else {
try {
fetch('/save' + permalink + '?action=save')
.then(checkPostCount());
} catch (error) {
console.error('Could not save', error);
}
}
}
// text expand code
// audio/video sync code function checkHeight() {
function pauseVideo(element){ const divs = document.querySelectorAll('.text-content');
div = element.closest('.post'); divs.forEach(div => {
video = div.querySelector('video:first-of-type'); height = div.offsetHeight;
if (video) { style = window.getComputedStyle(div);
video.pause(); maxHeight = parseInt(style.maxHeight);
pauseAudio(video); if (height < maxHeight) {
} div.classList.add('expanded');
} }
});
}
function findAudio(video){ function expand(div) {
div = video.closest('.post'); div.classList.add('expanded');
return div.querySelector('audio:first-of-type'); }
}
function playAudio(video){ window.addEventListener('load', (event) => {
audio = findAudio(video); checkHeight()
if (audio) { });
audio.play();
audio.currentTime = video.currentTime; window.addEventListener('resize', (event) => {
} checkHeight()
} });
function pauseAudio(video){
audio = findAudio(video); // audio/video sync code
if (audio) { function pauseVideo(element) {
audio.pause(); div = element.closest('.post');
audio.currentTime = video.currentTime; video = div.querySelector('video:first-of-type');
} if (video) {
} video.pause();
function seekAudio(video){ pauseAudio(video);
audio = findAudio(video); }
if (audio) { }
audio.currentTime = video.currentTime;
} function findAudio(video) {
} div = video.closest('.post');
</script> return div.querySelector('audio:first-of-type');
}
function playAudio(video) {
audio = findAudio(video);
if (audio) {
audio.play();
audio.currentTime = video.currentTime;
}
}
function pauseAudio(video) {
audio = findAudio(video);
if (audio) {
audio.pause();
audio.currentTime = video.currentTime;
}
}
function seekAudio(video) {
audio = findAudio(video);
if (audio) {
audio.currentTime = video.currentTime;
}
}
function checkPostCount() {
posts = document.querySelectorAll('.post').length;
if (posts == 0) {
setTimeout(() => {window.location.href = window.location.href;}, 1000);
}
}
</script>
</body> </body>
</html> </html>