feat: Add feed source filtering to settings and API

Co-authored-by: aider (gemini/gemini-2.5-pro) <aider@aider.chat>
This commit is contained in:
2026-01-03 00:01:46 +00:00
parent 93903eedf9
commit ab15868397
3 changed files with 72 additions and 18 deletions

View File

@@ -76,8 +76,12 @@ cors = CORS(flask_app)
def api():
skip = request.args.get('skip', 0)
limit = request.args.get('limit', settings.FEED_LENGTH)
is_smallweb_filter = request.args.get('smallweb') == 'true' and smallweb_set
sources_filter = request.args.getlist('source')
if request.args.get('smallweb') == 'true' and smallweb_set:
if not is_smallweb_filter and not sources_filter:
stories = database.get_stories(limit, skip)
else:
limit = int(limit)
skip = int(skip)
filtered_stories = []
@@ -90,24 +94,28 @@ def api():
for story_str in stories_batch:
story = json.loads(story_str)
story_url = story.get('url') or story.get('link') or ''
if not story_url:
continue
hostname = urlparse(story_url).hostname
if hostname:
hostname = hostname.replace('www.', '')
if hostname in smallweb_set:
filtered_stories.append(story_str)
if len(filtered_stories) == limit:
break
if is_smallweb_filter:
story_url = story.get('url') or story.get('link') or ''
if not story_url:
continue
hostname = urlparse(story_url).hostname
if not hostname or hostname.replace('www.', '') not in smallweb_set:
continue
if sources_filter:
if story.get('source') not in sources_filter:
continue
filtered_stories.append(story_str)
if len(filtered_stories) == limit:
break
if len(filtered_stories) == limit:
break
current_skip += limit
stories = filtered_stories
else:
stories = database.get_stories(limit, skip)
# hacky nested json
res = Response('{"stories":[' + ','.join(stories) + ']}')

View File

@@ -26,6 +26,15 @@ function App() {
const [bodyFont, setBodyFont] = useState(localStorage.getItem('bodyFont') || 'Sans Serif');
const [articleFont, setArticleFont] = useState(localStorage.getItem('articleFont') || 'Apparatus SIL');
const [filterSmallweb, setFilterSmallweb] = useState(() => localStorage.getItem('filterSmallweb') === 'true');
const [feedSources, setFeedSources] = useState(() => {
const saved = localStorage.getItem('feedSources');
return saved ? JSON.parse(saved) : {
hackernews: true,
reddit: true,
lobsters: true,
tildes: true,
};
});
const updateCache = useCallback((key, value) => {
cache.current[key] = value;
@@ -57,6 +66,14 @@ function App() {
localStorage.setItem('filterSmallweb', isChecked);
};
const handleFeedSourceChange = (source) => {
setFeedSources(prevSources => {
const newSources = { ...prevSources, [source]: !prevSources[source] };
localStorage.setItem('feedSources', JSON.stringify(newSources));
return newSources;
});
};
const changeBodyFont = (font) => {
setBodyFont(font);
localStorage.setItem('bodyFont', font);
@@ -177,6 +194,22 @@ function App() {
<input className="checkbox" type="checkbox" id="filter-smallweb" checked={filterSmallweb} onChange={handleFilterChange} />
<label htmlFor="filter-smallweb">Small websites</label>
</div>
<div className="font-option">
<input className="checkbox" type="checkbox" id="filter-hackernews" name="feed-source" checked={feedSources.hackernews} onChange={() => handleFeedSourceChange('hackernews')} />
<label htmlFor="filter-hackernews">Hacker News</label>
</div>
<div className="font-option">
<input className="checkbox" type="checkbox" id="filter-reddit" name="feed-source" checked={feedSources.reddit} onChange={() => handleFeedSourceChange('reddit')} />
<label htmlFor="filter-reddit">Reddit</label>
</div>
<div className="font-option">
<input className="checkbox" type="checkbox" id="filter-lobsters" name="feed-source" checked={feedSources.lobsters} onChange={() => handleFeedSourceChange('lobsters')} />
<label htmlFor="filter-lobsters">Lobsters</label>
</div>
<div className="font-option">
<input className="checkbox" type="checkbox" id="filter-tildes" name="feed-source" checked={feedSources.tildes} onChange={() => handleFeedSourceChange('tildes')} />
<label htmlFor="filter-tildes">Tildes</label>
</div>
</div>
<div className="setting-group">
<h4>Font Size</h4>
@@ -251,7 +284,7 @@ function App() {
<Route path='/(|search)' component={Submit} />
</div>
<Route path='/' exact render={(props) => <Feed {...props} updateCache={updateCache} filterSmallweb={filterSmallweb} />} />
<Route path='/' exact render={(props) => <Feed {...props} updateCache={updateCache} filterSmallweb={filterSmallweb} feedSources={feedSources} />} />
<Switch>
<Route path='/search' component={Results} />
<Route path='/:id' exact render={(props) => <Article {...props} cache={cache.current} />} />

View File

@@ -4,7 +4,7 @@ import { Helmet } from 'react-helmet';
import localForage from 'localforage';
import { sourceLink, infoLine, logos } from './utils.js';
function Feed({ updateCache, filterSmallweb }) {
function Feed({ updateCache, filterSmallweb, feedSources }) {
const [stories, setStories] = useState(() => JSON.parse(localStorage.getItem('stories')) || false);
const [error, setError] = useState('');
const [loadingStatus, setLoadingStatus] = useState(null);
@@ -16,7 +16,7 @@ function Feed({ updateCache, filterSmallweb }) {
} else {
setStories(false);
}
}, [filterSmallweb]);
}, [filterSmallweb, feedSources]);
useEffect(() => {
const controller = new AbortController();
@@ -30,7 +30,20 @@ function Feed({ updateCache, filterSmallweb }) {
});
}
fetch(filterSmallweb ? '/api?smallweb=true' : '/api', { signal: controller.signal })
const params = new URLSearchParams();
if (filterSmallweb) {
params.append('smallweb', 'true');
}
const allSources = Object.keys(feedSources);
const enabledSources = allSources.filter(key => feedSources[key]);
if (enabledSources.length > 0 && enabledSources.length < allSources.length) {
enabledSources.forEach(source => params.append('source', source));
}
const apiUrl = `/api?${params.toString()}`;
fetch(apiUrl, { signal: controller.signal })
.then(res => {
if (!res.ok) {
throw new Error(`Server responded with ${res.status} ${res.statusText}`);
@@ -117,7 +130,7 @@ function Feed({ updateCache, filterSmallweb }) {
);
return () => controller.abort();
}, [updateCache, filterSmallweb]);
}, [updateCache, filterSmallweb, feedSources]);
return (
<div className='container'>