Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

from settings import APP_DATA_DIR


SETTINGS_FILE = APP_DATA_DIR / "settings.json"


Expand All @@ -13,6 +12,7 @@
"notification_keywords": ["deadline", "scholarship", "application", "exam"],
"theme": "light",
"max_cached_articles": 500,
"custom_rss_feeds": [],
}


Expand All @@ -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)
json.dump(settings, file, indent=4)
51 changes: 51 additions & 0 deletions feed_config.py
Original file line number Diff line number Diff line change
@@ -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
140 changes: 97 additions & 43 deletions ui/main_window.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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()
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand All @@ -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")

Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Loading