diff --git a/apiserver/server.py b/apiserver/server.py index efacb1e..9537c6c 100644 --- a/apiserver/server.py +++ b/apiserver/server.py @@ -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) + ']}') diff --git a/webclient/src/App.js b/webclient/src/App.js index a62369b..fef9d8a 100644 --- a/webclient/src/App.js +++ b/webclient/src/App.js @@ -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() { +
+ handleFeedSourceChange('hackernews')} /> + +
+
+ handleFeedSourceChange('reddit')} /> + +
+
+ handleFeedSourceChange('lobsters')} /> + +
+
+ handleFeedSourceChange('tildes')} /> + +

Font Size

@@ -251,7 +284,7 @@ function App() {
- } /> + } />
} /> diff --git a/webclient/src/Feed.js b/webclient/src/Feed.js index 6e5d76c..944121d 100644 --- a/webclient/src/Feed.js +++ b/webclient/src/Feed.js @@ -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 (