From 1851be22a5f87ba316eb1bbbb325d0e2df2509f3 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Thu, 18 Jun 2026 19:35:19 +0200 Subject: [PATCH 1/2] Added option for user to add RSS feeds --- app_settings.py | 4 +- ui/main_window.py | 140 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 99 insertions(+), 45 deletions(-) diff --git a/app_settings.py b/app_settings.py index fbb624b..9b215ab 100644 --- a/app_settings.py +++ b/app_settings.py @@ -2,7 +2,6 @@ from settings import APP_DATA_DIR - SETTINGS_FILE = APP_DATA_DIR / "settings.json" @@ -13,6 +12,7 @@ "notification_keywords": ["deadline", "scholarship", "application", "exam"], "theme": "light", "max_cached_articles": 500, + "custom_rss_feeds": [], } @@ -39,4 +39,4 @@ def save_app_settings(settings: dict) -> None: SETTINGS_FILE.parent.mkdir(parents=True, exist_ok=True) with open(SETTINGS_FILE, "w", encoding="utf-8") as file: - json.dump(settings, file, indent=4) \ No newline at end of file + json.dump(settings, file, indent=4) diff --git a/ui/main_window.py b/ui/main_window.py index dcb9a1c..a182efa 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,28 +1,30 @@ import json -from PyQt6.QtCore import Qt, QUrl +from PyQt6.QtCore import Qt, QTimer, QUrl +from PyQt6.QtGui import QDesktopServices, QPixmap from PyQt6.QtWidgets import ( QFrame, QHBoxLayout, + QInputDialog, QLabel, + QLineEdit, QListWidget, QListWidgetItem, QMainWindow, QMessageBox, QPushButton, + QScrollArea, QVBoxLayout, - QWidget, QScrollArea, QLineEdit, + QWidget, ) -from PyQt6.QtGui import QDesktopServices, QPixmap +from app_settings import load_app_settings, save_app_settings from database import Database -from settings import FEEDS_FILE +from feed_config import get_all_feeds +from notifications import send_article_notification, should_notify from rss_service import fetch_feed -from ui.settings_dialog import SettingsDialog -from app_settings import load_app_settings -from PyQt6.QtCore import QTimer -from notifications import should_notify, send_article_notification from scrapers.scraper_service import run_scraper +from ui.settings_dialog import SettingsDialog ARTICLE_LINK_ROLE = 1000 @@ -34,16 +36,12 @@ def __init__(self): self.setWindowTitle("UniNews") self.resize(1050, 720) - self.settings_button = QPushButton("Settings") - self.settings_button.clicked.connect(self.open_settings) - self.database = Database() self.articles = [] self.selected_university = "All" self.selected_source = "All" self.search_text = "" - self.app_settings = load_app_settings() self.setup_ui() self.apply_theme() @@ -238,7 +236,9 @@ def update_source_list(self): self.source_list.clear() all_item = QListWidgetItem("All") - all_item.setData(Qt.ItemDataRole.UserRole, {"university": "All", "source": "All"}) + all_item.setData( + Qt.ItemDataRole.UserRole, {"university": "All", "source": "All"} + ) self.source_list.addItem(all_item) universities = self.database.get_universities() @@ -334,17 +334,22 @@ def create_header(self): title_area.addWidget(self.title_label) title_area.addWidget(self.subtitle_label) - self.refresh_button = QPushButton("Refresh news") - self.refresh_button.setCursor(Qt.CursorShape.PointingHandCursor) - self.refresh_button.setObjectName("RefreshButton") - self.refresh_button.clicked.connect(self.refresh_news) + self.add_rss_button = QPushButton("Add RSS") + self.add_rss_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.add_rss_button.clicked.connect(self.open_add_rss_dialog) self.settings_button = QPushButton("Settings") self.settings_button.setCursor(Qt.CursorShape.PointingHandCursor) self.settings_button.clicked.connect(self.open_settings) + self.refresh_button = QPushButton("Refresh news") + self.refresh_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.refresh_button.setObjectName("RefreshButton") + self.refresh_button.clicked.connect(self.refresh_news) + header_layout.addLayout(title_area) header_layout.addStretch() + header_layout.addWidget(self.add_rss_button) header_layout.addWidget(self.settings_button) header_layout.addWidget(self.refresh_button) @@ -517,19 +522,19 @@ def apply_light_theme(self): QScrollBar::sub-line:vertical { height: 0px; } - + #Sidebar { background-color: #FFFFFF; border: 1px solid #E6DDF3; border-radius: 22px; } - + #SidebarTitle { color: #171124; font-size: 18px; font-weight: 900; } - + #SearchInput { background-color: #F9F5FF; color: #171124; @@ -538,28 +543,28 @@ def apply_light_theme(self): padding: 9px 12px; font-size: 13px; } - + #SearchInput:focus { border: 1px solid #A855F7; } - + #SourceList { background-color: transparent; border: none; outline: none; } - + #SourceList::item { color: #4B5563; padding: 10px 12px; border-radius: 10px; } - + #SourceList::item:hover { background-color: #F3E8FF; color: #171124; } - + #SourceList::item:selected { background-color: #A855F7; color: white; @@ -579,13 +584,13 @@ def apply_dark_theme(self): border: 1px solid #1E293B; border-radius: 22px; } - + #SidebarTitle { color: #FFFFFF; font-size: 18px; font-weight: 900; } - + #SearchInput { background-color: #111827; color: #F8FAFC; @@ -594,28 +599,28 @@ def apply_dark_theme(self): padding: 9px 12px; font-size: 13px; } - + #SearchInput:focus { border: 1px solid #C084FC; } - + #SourceList { background-color: transparent; border: none; outline: none; } - + #SourceList::item { color: #CBD5E1; padding: 10px 12px; border-radius: 10px; } - + #SourceList::item:hover { background-color: #1E293B; color: #FFFFFF; } - + #SourceList::item:selected { background-color: #C084FC; color: #080A14; @@ -754,15 +759,7 @@ def apply_dark_theme(self): def load_feeds(self) -> list[dict]: try: - with open(FEEDS_FILE, "r", encoding="utf-8") as file: - return json.load(file) - except FileNotFoundError: - QMessageBox.critical( - self, - "Missing feeds file", - f"Could not find:\n{FEEDS_FILE}", - ) - return [] + return get_all_feeds(self.app_settings) except json.JSONDecodeError as error: QMessageBox.critical( self, @@ -783,7 +780,6 @@ def refresh_news(self): university_name = feed.get("name", "Unknown university") feed_url = feed.get("url", "") - try: source_type = feed.get("type", "rss") @@ -914,7 +910,9 @@ def clean_text(self, text: str) -> str: def open_settings(self): dialog = SettingsDialog(self) - dialog.setStyleSheet(self.get_theme_stylesheet(self.app_settings.get("theme", "light"))) + dialog.setStyleSheet( + self.get_theme_stylesheet(self.app_settings.get("theme", "light")) + ) if dialog.exec(): self.app_settings = load_app_settings() @@ -956,4 +954,60 @@ def apply_theme(self) -> None: bg = "#080A14" if theme == "dark" else "#F4F0FA" self.article_container.setStyleSheet(f"background-color: {bg};") + def open_add_rss_dialog(self): + name, ok = QInputDialog.getText( + self, + "Add RSS feed", + "Feed name:", + ) + + if not ok or not name.strip(): + return + + url, ok = QInputDialog.getText( + self, + "Add RSS feed", + "RSS URL:", + ) + + if not ok or not url.strip(): + return + + name = name.strip() + url = url.strip() + + if not url.startswith(("http://", "https://")): + QMessageBox.warning( + self, + "Invalid URL", + "The RSS URL must start with http:// or https://", + ) + return + + custom_feeds = self.app_settings.setdefault("custom_rss_feeds", []) + + if any(feed.get("url") == url for feed in custom_feeds): + QMessageBox.information( + self, + "RSS already exists", + "This RSS feed has already been added.", + ) + return + + custom_feeds.append( + { + "name": name, + "type": "rss", + "url": url, + } + ) + + save_app_settings(self.app_settings) + + QMessageBox.information( + self, + "RSS added", + f"{name} was added successfully.", + ) + self.refresh_news() From fd1bd2974645f87a1b37af134b6b2d1a73f3ac21 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Wed, 1 Jul 2026 12:05:39 +0200 Subject: [PATCH 2/2] Added missing feed_config.py --- feed_config.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 feed_config.py diff --git a/feed_config.py b/feed_config.py new file mode 100644 index 0000000..5a7852c --- /dev/null +++ b/feed_config.py @@ -0,0 +1,51 @@ +import json + +from settings import FEEDS_FILE + + +def load_builtin_feeds() -> list[dict]: + if not FEEDS_FILE.exists(): + return [] + + with open(FEEDS_FILE, "r", encoding="utf-8") as file: + return json.load(file) + + +def get_all_feeds(app_settings: dict) -> list[dict]: + builtin_feeds = load_builtin_feeds() + custom_feeds = app_settings.get("custom_rss_feeds", []) + + feeds = [] + seen = set() + + for feed in builtin_feeds + custom_feeds: + source_type = feed.get("type", "rss") + + if source_type == "rss": + name = feed.get("name", "").strip() + url = feed.get("url", "").strip() + + if not name or not url: + continue + + key = ("rss", url.lower()) + + elif source_type == "scraper": + name = feed.get("name", "").strip() + scraper = feed.get("scraper", "").strip() + + if not name or not scraper: + continue + + key = ("scraper", scraper.lower()) + + else: + continue + + if key in seen: + continue + + seen.add(key) + feeds.append(feed) + + return feeds