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:
@@ -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,13 +94,19 @@ def api():
|
||||
|
||||
for story_str in stories_batch:
|
||||
story = json.loads(story_str)
|
||||
|
||||
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 hostname:
|
||||
hostname = hostname.replace('www.', '')
|
||||
if hostname in smallweb_set:
|
||||
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
|
||||
@@ -106,8 +116,6 @@ def api():
|
||||
|
||||
current_skip += limit
|
||||
stories = filtered_stories
|
||||
else:
|
||||
stories = database.get_stories(limit, skip)
|
||||
|
||||
# hacky nested json
|
||||
res = Response('{"stories":[' + ','.join(stories) + ']}')
|
||||
|
||||
@@ -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} />} />
|
||||
|
||||
@@ -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'>
|
||||
|
||||
Reference in New Issue
Block a user