From e1b39cf18318bf7cc1268f5f49df0404eaa2080b Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Mon, 1 Jun 2026 22:34:47 +0200 Subject: [PATCH 1/7] First implementation --- .idea/.gitignore | 8 + .idea/UniNews.iml | 10 + .idea/deployment.xml | 21 + .idea/inspectionProfiles/Project_Default.xml | 21 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/vcs.xml | 7 + __init__.py | 0 __pycache__/app_settings.cpython-310.pyc | Bin 0 -> 1246 bytes __pycache__/database.cpython-310.pyc | Bin 0 -> 3940 bytes __pycache__/notifications.cpython-310.pyc | Bin 0 -> 980 bytes __pycache__/rss_service.cpython-310.pyc | Bin 0 -> 761 bytes __pycache__/settings.cpython-310.pyc | Bin 0 -> 783 bytes app_settings.py | 43 ++ data/feeds.json | 10 + data/settings.json | 7 + database.py | 130 ++++++ main.py | 18 + notifications.py | 24 + requirements.txt | Bin 0 -> 412 bytes rss_service.py | 29 ++ settings.py | 30 ++ ui/__init__.py | 0 ui/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 160 bytes ui/__pycache__/main_window.cpython-310.pyc | Bin 0 -> 11520 bytes .../settings_dialog.cpython-310.pyc | Bin 0 -> 3626 bytes ui/main_window.py | 415 ++++++++++++++++++ ui/settings_dialog.py | 134 ++++++ 29 files changed, 928 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/UniNews.iml create mode 100644 .idea/deployment.xml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 __init__.py create mode 100644 __pycache__/app_settings.cpython-310.pyc create mode 100644 __pycache__/database.cpython-310.pyc create mode 100644 __pycache__/notifications.cpython-310.pyc create mode 100644 __pycache__/rss_service.cpython-310.pyc create mode 100644 __pycache__/settings.cpython-310.pyc create mode 100644 app_settings.py create mode 100644 data/feeds.json create mode 100644 data/settings.json create mode 100644 database.py create mode 100644 main.py create mode 100644 notifications.py create mode 100644 requirements.txt create mode 100644 rss_service.py create mode 100644 settings.py create mode 100644 ui/__init__.py create mode 100644 ui/__pycache__/__init__.cpython-310.pyc create mode 100644 ui/__pycache__/main_window.cpython-310.pyc create mode 100644 ui/__pycache__/settings_dialog.cpython-310.pyc create mode 100644 ui/main_window.py create mode 100644 ui/settings_dialog.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/UniNews.iml b/.idea/UniNews.iml new file mode 100644 index 0000000..8217bd6 --- /dev/null +++ b/.idea/UniNews.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/deployment.xml b/.idea/deployment.xml new file mode 100644 index 0000000..4ff1d38 --- /dev/null +++ b/.idea/deployment.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..03a1660 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..aa60783 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..0d37d4b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..8306744 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__pycache__/app_settings.cpython-310.pyc b/__pycache__/app_settings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ec2c91ef909860522b080053cdce5aa9c8bd683e GIT binary patch literal 1246 zcmY*YL2uhO6ecN3RvbHVlC>Dtc89INro#&CRusdqpiYVmg%{KgiU|e;MWih{k|>aL zofOWgy{7vEJ0!dHFY!8HK<_){ub;wdXc#p89I<+4bX&P5%sr*&P3EHy$_G3BReY5hWsb0hx62t|S2 zR(zzm&J$TB+N8=%Yy0KWCo03$tF%Z;QB4ik_D&^@7=4PT6JDjq1!uS2$q7H3NyW7F zP0mYhcgplM$A^>cyBO9#=E3f zsE=NMZ+rtZFv7eG?~nF5GQnkIj;`=IehFkym~T9cs_;joAI{*Uo4}Sd93)G|Y^S@ozkBq2kStGFn#uaiQd#rLQb(wzg-n?ZrZr0q zx0}EAf9rKQVDUYrq>4OCN21_1Tn3FdORmPy-lgL`Qum>M*WIU2hDVw!JsfkH*2AvM zrX{ZotcA&^$BE*#)WS$5xE>x=qQ_@?xSH=HNNvldbE}Fk977Hv#BLJO!7Y6MW9T(; zKsHDSEApTE8aAkVpfb2?p9cZ!Gs^>57&O5X0&D`G7x0*j2i$s$G9S3zI>&}i{5hIq z7O=(zu)4}H17?8ZZ&x{Xz9{SsB4QhLs$99il_MmPCl=3C8$i_ts1NYi5xlNIR1CI7 zf$D(628&bHFHe|I>WsDKAa1}Mjtze*C7TxfXV+NmG6_)&w@By_ QN-)6z22&PX%8_&@2nM_hX7p_6NFb@S0(fCLFyZf*sI$9?;Dm;2t^-|lKq zt(GNtzWvub`~R*=(m!#s_{cE118;N-1|~5QNL~D<0qN5&B}mJG+*i6vN-Kfd*SZ>! z?n+E$+Ofp6=d@d5q$Qc9Z@`9Rn

0yYVCV)^~=nKM36b=;~IzUH`b=vYh+%_NN|- zE&M}g{eJ-EwM6ibhgZw zc}aLY?7d}{0pyTF%u1y)z4-xqn6DVau3XRB-t?AO5lC3YF~E&#R8eg@PE zdjloYUwV_h1)Pgah3&qR=v!{=K6E3VRN0hrhOl#w!X9Q1(s;_7@J7FeVL~R7BgN!| zPNaS56lZcw_7#}PjyjP}v{MPsx+T3JwpkJy=p?1+An;@Uhos~U!jOA$Ql1iUzFoOF zc-mbSgQrf<_2PjzOsbyXZpylVGMIWel&XM2tFJ{KA7`)wA%@Nn?H?qXB*uZ9wxfu_@N&=&JylThe0DXQYIBL zzBoIYjhRfM2_BC`XnO<%XHVgD-UTirM{pwh(g~T6Q$nParn3_ z_D;hFbN4K}eXXD|3jHT=On!W5w5>1O>2KJb?d_se?8gEBUjosn-*-i3qo^AA;p3vF z(XktU>R3gw;pk!DM~^s5Ws7S)9(%CjxN*@?z7g|APQH)@qAe-$XWSdbJkh*CzwgH; z6&2*G2O};H(;&%)8gRuj3>V;yFdh|Bsnp1LJ-_8bxFx0O*%yF-AVB*m5O#rpt4<*R zS4}4X2gLYkMgiKU6rg=d={W^>pYoQG*V9fRQKf+*=YdkO$N$K8+^}0#vu*4&jmBMj zrwNf_w|8C_AoJ@k9&viXMb`96i%h)DA}7DNB71&kxua%IeO}-0SS{n~?Q2HiZ{Z%3 z^4!(&YA1Xg4xWa_oR_Gn%Zc6zQ-VPj$zm{(5(V8%mU7p|m6-uRIPaF{&Y4OYE@Bm~ zERpwkEY^TBLO$eipW%yQ@{730XnW(UnECW*UE8r@~W$J|f87l8zo}6Y% zg=x8F#*y~vi97*Yr}U^aDV->%(hBrJU-;$nV)n2m4f6t??Wo=uV?R=W$%VN~d zRAk|4KefL@J>+2IWUTU%C_efs33oV|y z*y155@R^=uHkprwlg;5VThDR>fCn4HhHYHUG91F`C!d1lpOl77@EWKts-A`I;+lD5 zahB3)pv(%_@8JB-Ewz_<*c%8hueMHFd!FEpWzE`38E);}{MODDhwg6$G;CA0ZSixU z%vEggL$}WzCn-Blf51io(iO)!7`Z{VA}}lk28O`yFl`gX`$%EU#Wo{~g@JQKO(MfA zk^L|hzd_z_alr5rAK);DLQ185Css`S*CM)_604-Ft*)%UO*Kj=QNGhC{XvuI_f(H-zJCH$W(@E@jLCR%(r^uxW(9X;Z446`WP25Oz>M55o!c7F@u PA7S^b0~X;!SLOc#1SoBv literal 0 HcmV?d00001 diff --git a/__pycache__/notifications.cpython-310.pyc b/__pycache__/notifications.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ef129c9fc68d227844b0319f9c8111813316270f GIT binary patch literal 980 zcmY*X&2AGh5VpPEpQMTS5o*-}Qm%W6khq~JLTb6@QpzC;inePf-Lkvh+TONX1qsw5 z;J_0gq~_Q+;Te44zyb9YDuftsLrJ}wPaf}hzVSCB+U+KS_2bv$eN7PhX`SWef%6

3O%n74w z*Pd&=HFS>W=rhvfBbs35G5;)>;VpEEPvKS=3|<;nCmM*+748^cm01?aD#noe-?F5Z z@F|#j1n?dRUN_$289qmI^a&lJR|um6!Vsjy19+U9iX$#VVo3Li@fT0Jz=mpmtS`{j zi_K@F9mS;@O}U7Q(UyqIjOQBsMDLZmW62Al5-nuH)o3SA2K-2kmK9UoqB4!%l;`}o zko}gejRKH&8|b8?_C%Soaeblc_EHf?sp@xc!b8V6Q?8}$x^bes3W>B&ls1LDW1(%~ zE`Vv@%4Q>!rK6bQ4Toa#XO+qaa4@)~noVcNW)rL)o4Q4qkvRe7Y`YvrVNW#PC(NDU zujm_@friZcKwy{%^I7nP!iZT_8w1lU^W+dDOLS%Yg(T84oUDL4oy7p1+*@)w6DhLJ z<#!0W3;;q}Uod{gm5Qc(7(_)e&ZCSQUndzCr4Ai=A0nk~`p~-;5YB3GESX%lP%BhJ zt5_5s2~E@Az(rHyQeIkNWg}eb@vc>3?oN6o zZ-h3|?g~}6&6VJHM+^>@OoRhOqzDx>ZY_V5z1;QjEgqH zI#za_P<)zb>N~^$%Soui3tw74@io#?TUpgWiamQ5#`%u)89d(4X75%D=x=vvrc>$|QNO3uZtS!rVmAl=AHyy_! zA>K%ib4-HKd-jJ;^m9mq*}deTR-b_9B4f2*g0YTrS=o>lhJ6Ijd}jK-X?b6Z4;Uij LJD;MIyomk+`lrhS literal 0 HcmV?d00001 diff --git a/__pycache__/settings.cpython-310.pyc b/__pycache__/settings.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2b3a3a959997a6e9bfc12b87044a7fc5fdb843ac GIT binary patch literal 783 zcmYjN&2Abo5VpO}vcR&XRc}=vfD6YSsw&Dh5H(VYppc7&v|8c`ELqko+cZr?PW0M$ zNRB=AA^H|RRqCm)&`WERfSA>c=V$%-n@=N7fayfg_G{ zw7?8wmy?_ihWG~Q5xZpM1&IQCbzXGUD|xoYeh~PJ literal 0 HcmV?d00001 diff --git a/app_settings.py b/app_settings.py new file mode 100644 index 0000000..d38355e --- /dev/null +++ b/app_settings.py @@ -0,0 +1,43 @@ +import json +from pathlib import Path + +from settings import APP_DATA_DIR + + +SETTINGS_FILE = APP_DATA_DIR / "settings.json" + + +DEFAULT_SETTINGS = { + "refresh_on_startup": True, + "refresh_interval_minutes": 0, + "notifications_enabled": False, + "notification_keywords": ["deadline", "scholarship", "application", "exam"], + "theme": "light", + "max_cached_articles": 500, +} + + +def load_app_settings() -> dict: + if not SETTINGS_FILE.exists(): + save_app_settings(DEFAULT_SETTINGS) + return DEFAULT_SETTINGS.copy() + + try: + with open(SETTINGS_FILE, "r", encoding="utf-8") as file: + settings = json.load(file) + + merged_settings = DEFAULT_SETTINGS.copy() + merged_settings.update(settings) + + return merged_settings + + except json.JSONDecodeError: + save_app_settings(DEFAULT_SETTINGS) + return DEFAULT_SETTINGS.copy() + + +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 diff --git a/data/feeds.json b/data/feeds.json new file mode 100644 index 0000000..869295c --- /dev/null +++ b/data/feeds.json @@ -0,0 +1,10 @@ +[ + { + "name": "University of Oxford", + "url": "http://feeds.reuters.com/reuters/topNews" + }, + { + "name": "MIT News", + "url": "https://news.mit.edu/rss/feed" + } +] \ No newline at end of file diff --git a/data/settings.json b/data/settings.json new file mode 100644 index 0000000..b80724d --- /dev/null +++ b/data/settings.json @@ -0,0 +1,7 @@ +{ + "refresh_interval_minutes": 30, + "notifications_enabled": true, + "notification_keywords": ["scholarship", "deadline", "exam", "application"], + "theme": "light", + "max_cached_articles": 500 +} \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..bbb48bc --- /dev/null +++ b/database.py @@ -0,0 +1,130 @@ +import sqlite3 +from pathlib import Path +from typing import Optional + +from settings import DATABASE_PATH + + +class Database: + def __init__(self, database_path: Path = DATABASE_PATH): + self.database_path = database_path + self.connection = sqlite3.connect(self.database_path) + self.connection.row_factory = sqlite3.Row + self.create_tables() + + def create_tables(self) -> None: + query = """ + CREATE TABLE IF NOT EXISTS articles ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + university TEXT NOT NULL, + title TEXT NOT NULL, + summary TEXT, + link TEXT NOT NULL UNIQUE, + published TEXT, + fetched_at TEXT NOT NULL + ); + """ + + self.connection.execute(query) + self.connection.commit() + + def save_article(self, article: dict) -> None: + query = """ + INSERT OR IGNORE INTO articles ( + university, + title, + summary, + link, + published, + fetched_at + ) + VALUES (?, ?, ?, ?, ?, ?); + """ + + self.connection.execute( + query, + ( + article.get("university", "Unknown university"), + article.get("title", "Untitled"), + article.get("summary", ""), + article.get("link", ""), + article.get("published", "Unknown date"), + article.get("fetched_at", ""), + ), + ) + + self.connection.commit() + + def save_articles(self, articles: list[dict]) -> None: + for article in articles: + self.save_article(article) + + def get_articles( + self, + university: Optional[str] = None, + search_text: Optional[str] = None, + limit: int = 200, + ) -> list[dict]: + query = """ + SELECT + id, + university, + title, + summary, + link, + published, + fetched_at + FROM articles + WHERE 1 = 1 + """ + + params = [] + + if university and university != "All Universities": + query += " AND university = ?" + params.append(university) + + if search_text: + query += """ + AND ( + title LIKE ? + OR summary LIKE ? + OR university LIKE ? + ) + """ + + search_pattern = f"%{search_text}%" + params.extend([search_pattern, search_pattern, search_pattern]) + + query += """ + ORDER BY id DESC + LIMIT ? + """ + + params.append(limit) + + cursor = self.connection.execute(query, params) + rows = cursor.fetchall() + + return [dict(row) for row in rows] + + def delete_all_articles(self) -> None: + self.connection.execute("DELETE FROM articles;") + self.connection.commit() + + def close(self) -> None: + self.connection.close() + + def enforce_article_limit(self, max_articles: int) -> None: + query = """ + DELETE \ + FROM articles + WHERE id NOT IN (SELECT id \ + FROM articles \ + ORDER BY id DESC + LIMIT ? + ); \ + """ + + self.connection.execute(query, (max_articles,)) + self.connection.commit() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..68796a7 --- /dev/null +++ b/main.py @@ -0,0 +1,18 @@ +import sys + +from PyQt6.QtWidgets import QApplication + +from ui.main_window import UniNewsWindow + + +def main(): + app = QApplication(sys.argv) + + window = UniNewsWindow() + window.show() + + sys.exit(app.exec()) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/notifications.py b/notifications.py new file mode 100644 index 0000000..a51a844 --- /dev/null +++ b/notifications.py @@ -0,0 +1,24 @@ +from plyer import notification + + +def should_notify(article: dict, keywords: list[str]) -> bool: + if not keywords: + return True + + text = ( + article.get("title", "") + " " + article.get("summary", "") + ).lower() + + return any(keyword.lower() in text for keyword in keywords) + + +def send_article_notification(article: dict) -> None: + title = article.get("university", "UniNews") + message = article.get("title", "New article") + + notification.notify( + title=f"New article from {title}", + message=message[:200], + app_name="UniNews", + timeout=8, + ) \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..b9935732b73e23bc9b9c08cb09e4592416cf8416 GIT binary patch literal 412 zcmZvY-3o$G5QOJC=uwJP61wmaf_j3eJcVfamq;&P&FoRTsEfse^X=^HzF#RE&M48~ zh=+X>Bv@mKh!tRt87^cTQdDFbw0t*Y62v4{Muo<0P0vY|=y5m3yNL2kg#ypXA+&C} zXwHtxo+;wVJz#=gb@7+-S-&0!Ce4^xNrPb list[dict]: + parsed_feed = feedparser.parse(feed_url) + + fetched_at = datetime.now(timezone.utc).isoformat() + articles = [] + + for entry in parsed_feed.entries: + link = entry.get("link", "") + + if not link: + continue + + articles.append( + { + "university": university_name, + "title": entry.get("title", "Untitled"), + "summary": entry.get("summary", ""), + "link": link, + "published": entry.get("published", "Unknown date"), + "fetched_at": fetched_at, + } + ) + + return articles \ No newline at end of file diff --git a/settings.py b/settings.py new file mode 100644 index 0000000..323acfb --- /dev/null +++ b/settings.py @@ -0,0 +1,30 @@ +from pathlib import Path + +from PyQt6.QtCore import QStandardPaths + + +APP_NAME = "UniNews" + +BASE_DIR = Path(__file__).resolve().parent + +DATA_DIR = BASE_DIR / "data" +FEEDS_FILE = DATA_DIR / "feeds.json" + + +def get_app_data_dir() -> Path: + app_data_location = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.AppLocalDataLocation + ) + + if app_data_location: + app_data_dir = Path(app_data_location) / APP_NAME + else: + app_data_dir = Path.home() / ".uninews" + + app_data_dir.mkdir(parents=True, exist_ok=True) + + return app_data_dir + + +APP_DATA_DIR = get_app_data_dir() +DATABASE_PATH = APP_DATA_DIR / "uninews.db" \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/__pycache__/__init__.cpython-310.pyc b/ui/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2f152cd7409d971e9a523a18d3f60e71de1932f1 GIT binary patch literal 160 zcmd1j<>g`kf_aZ*vq1D?5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HsvsFxJacWU< zOnPd5VnK{cesXDUYF-JDpIMSoniOA@T999yS(0CrnOYnZnwRO9T3#Geni&%xpP83g c5+AQuPzD z0~}clwKY|qtQ6r{c;@#Za0lBPCNh z<*LoJkrr{)%`~${wy7CfGiT(Qc_ZH}7=>ohC^kz*saZD4%_(E5SurZ2e%hUGR*kBN zXWW_QtT8L%S@&4;xN%&>HTOjGq;XQjbMC3;Y2&nr=P{0VjCVx5;J({DW1MN8HO?Yk zG~WBU#7eCEoy5v^@m0#unY=93re0$vQoW{x%2E)jOOLr*lf%l=9oydsy!Nuqzi}G2 zU(1J?rA2Nv?Xa-)#VzmUJ!{MB1jxK+J-1zC-gErmiNjXyAext81 z-X(>-ZDy`Jf3@zkhRkb8wTW`32zLQoW`5_&1ey-mf>O7vVW z^`}X2I|~0CwW^dG%=9GD`z(5$T|Xvzuf}6I9=AWSr@b|3&!lB+PfR*n53}*Cozs}0 z^UsPTh1x)Yj4HQRxb3f*Ud!|Ys6?mzsCF_;`<=GUL(MdumJ^t!QHEYcVteESu5Dz{ z$m!a=2vLPo1En*cM+%;8xK3lkW?{DBwOV!~2uohu#v5Yg;m3}K7%zITYe@=B&vQdhpKP8D|qHiv*B94Z<^mrZx(OQKYQ%k+<&%edsh3|9k0=8 z+N}U-Cs^w|H@V&Rd?)a@WBbqIHF(x>o;3jx=BB`ix%L)+7o$6ipZ^hpyqs5x_*G<0 z$>68S^k2p^%Ay0QiI!8V4G{U#h`&RadH4wF3{JgP#@@kTW*A%{Kt3? zzqlJ*qS%KBeuAG*T2aQuH_)(z_y`Vm#is)xNPOFJPwoTv#70+o5aPDd zQ~ClH1T>HU6$n&_FG!rGMlutZ(x?jYJ5Hcvs0_urk^e%h+ii<8egy*LR}s7>QSws=jJ$vkVN1IoK!?A< z26BQeoi?iOv~;g!>o7`IOK-bY!!}CGo#z1|j%f0x_%MPiQQs)VsmQGI(|AGcBv0c3 zi)RO4K3@m))M3jxtt0=Kf*TZ&TJkIcqaZqma>BR+h_=`d$87Au&*x$*!QBA7FB zq=y~udz=GR(OHyOUbEV^QGMV!Ewa*ISS=RUqJ7TaqkuL$KSP0zz@Rrpo`#V}bUEN; z@A-9<)l`0gsuZFfVKyLIqZB8iO=aYxSyFr`B1@9b)#@Ia$Cp^drhlSD{>7F7>C%OA&B5VMV44=;}{m{E1SwJce%km*S+sC z+sKc^%TfG+%^x@~p$F3roVhz*=ecX&jtrStw6G|QSo|KbIQ%nu&m{DjN5MLNK8-P! zCL~)`y6;bv=;-M8mn%UTY&;xtl8``PrEcYNupIix--sKk|7D_tDm}Lv8>`&wwAh7) z=X!izKeu>o@#Do?KS@;G&L@)RhCBO6eQvHnipd_u%RLTUzrZc#bo_b!(v9}Z{VJif z{ZjWm*J1j(g=-5p7q29%j*oDNEfYpKnChj4%b#2sn<@=#1xD!t`84zTmCKU`B`R#% z&gxn)uivyuP`xD6(PTT^(w^ZFi;IaCOLi0UrhSiHG-d;cXGf!5o%?p#~Al&pIASa2qScXd=% z1bAOwyqbK+P@oCfAA(HhCIR@^aF-&3e#_$X*z3adAE-d{`hjY_>9Al8W1NUpgXKI7 zq({ayzXq4kwU0WsbCVWmD7;;5yf&_k`%7hf1V_*;cnL?qlhIki8e9+?ey|2VPB;{! zRpEFit4>(1wb=CTS%vLHw;;aA`ClfIV0q29gNg3hd2-F@M<)YSR%GpM_jJ-Ri@1Q8 zYyjU8ba*S_nUS|kG^+4;{~nR99!P;8#aQaMslP67Yf)~H?q_-u1F06szmxt}*+I)yoJgOdHR>tr*{x++ z+6EWZt;TgTEZb9tb)SgZ40V2`5FbR(O?gwQY4uR%-E=2dxo`vW((G2lBg@^b-gkT- z5uKPuUte)tyIZ+U+#x_aK)S`|^WEy*);AV;Wh1qI^wjI{C}VEj_gbQ@zGl&>E~@;e z%U{;#^EEA0$&Ka}2rN{oYN!&E6HYJUZFMhLB+qn#bDXLQsAFgi?uadHOi>qiY>d?w z<+YSJ8igv2Gfdl}O_Bu&^MWN31N|5DCRi2)j<_a&^tPzb%46+i_5ZYVMINRqY3*C( zP4{F{BZgfi^ngZ5KcNTLaSp&PWyAU^x_(>ov&m>^D3l3C9Z2l!k0 z4tT$9WtC_H#J~QC2>1F)uFV1(*2Dx6kmwBaZXM0(+C~s>$ z4J~j)?RvTKBgBDeS5-ax7zXN^V%R!64 zZ<1aJKV`fFBrZmh7rjpP2E+rn&>hq;D>$i4$E;B`rW8m9DOQQk zcO;ML-IGkUzStcc>7j#jb8`tX(-E7__RIMB#}TaIC0}O+BZDFC$bX}}fSN;}RiMsB zP3AvBB_jOiE{55`sclR>a-RY#8eX&g*?&>Dbdt=+d zr2pC6ZhPmiX7D!INV`tc!3{$Tyu+L7Q?V)ONXL$Mz5Piaas{3ae@nurq4KS$gk3T>e!d_yCP_#^ve!0)4kzfV<* zU_}Rnzc1v%rzt-Jki%VoQSz;C?A;1sZsBFaZqtQ8m~L~>82lX7K4!O8Jl@!?Xo{)w z52$Pk9w82P!#-vsBbT^d@N0#L%t(t3Y2-XyO3-5Y#uA7CW>9tB@fkX&oQx%>z6eLA$~1?>TrRVgZ&AJnu45(Lbu0z3dUqA6vN zB+9oHrL5>Ou;6VuMMMelutiWL<_T8{S@sFI>&!6 z*TMAAV)3UGkWl$ADVW4A{1FPK(YH^`8Vy{KY4zQ^jk$^3xJsUMgN8HsBNIkl2h57KydkdU; zG_r*9xMkz?ju@%Ag*bmi!Pwf|Lf$c&5beM`)}cx)_n1Wk00skBiM&+<+}6ajDy@Y!BrwrR0fIgjlx{A%=>ZOd{v<05q=P6Bhm{prOmU6n z1QtucSdh8f*#PMJ3MA}?%J28GxFXH}S~(^4i#rl4oREOgDd20drwWuMtS$Dkz}oD# z7NmL_#+d8n`lX;O-c+XE>9z@ZQ@~o_v6z|G6Aamlvw{Jt)VrV8Jv~M^SZy+(bNSnA zc4I@w?IqVAE-%Nn+T*atfz4ZXfTO;+o53!h3)6x@G_=Q{EWx(2FhAUCoDet6wZVru zJ}z6j#Q~~80qijAcbag5xAxXhE~xA0I>xJ0C=|OTC0A5E&89avFVP!;ZB{*A1^ zkxMl-%;F-8xOl#S9^kQo)`m3Ot`iJ)wTl8nSfkeXzIIUb(*aPOl*{tCPc>Yp#EvG= zAQ%xln)(l*`SA|Mkona>5qDX*zv`>8j{%QL?2lAG6HOBPn66p0-hgk;_H#Sv$Arhy zFT|~|GyA3Jx`uX>*imXdG9XXz;_lRPd~2zbkz{kVGOr>(RPC2`LtI!2>&X8Ynfy;E zAipDm@~9IzY>UriA!^bF7+o`Fdg09Bx#cSgPTFvqv(Dx zvQ1S*{hx8dUSKBFTzoGO7DugYJ)tZl?Olog42cLsBT4!>l1rSET9tr#xcmZE7PeEk zO$0{1QurAlSc-hc7u8sb#xe25LHu!nZ`zTEvY5;_H|(uV(1Jb;;5ocFlHW0k(LCb| z5FQ{NYJ{YXyslimbnV*F#(m%=2lYRoJNIJ% zsm=(&KKZg^x6-~B9#mtJTo1eWpmu{H?ni7C+N+7lp|F? z_(DWe?vxi|BDf-H?Bi_s4l8Re}@WZx6)w!_FR%sjY%RLq27-yzysM+ zdeMMl-^=_{Dg7OAIY7mSjC9{i0%|*9!4KNV1LZtu{|x3c!lGZcfWDVPOO><<1b5u`!y+m9AeIMfM2sE7WwKG~l`%ueOf2qxA7s{%ZseYiT z$1C}KRpV5<_CEgt52N^CYbm%sw}j8ExGj!M;s|aO2KhhlIO2#NP7QLRk6e85=}h#k z3?1CWcNNjZ$a4^EtoYzB`hJGKMTopy`kGIi(u9*Km|dg5@tqt#kE5?@1=nO$JI>s| z5jA5cmmKNH#{qdJi7)kxQo?8CyMCZJvWc0F2uvsk2`tn;jaKtB@}d0G2sEjJ!_`H( PSQ;F$=nEHBnN9sa)?4kF literal 0 HcmV?d00001 diff --git a/ui/__pycache__/settings_dialog.cpython-310.pyc b/ui/__pycache__/settings_dialog.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..71eee1ee0629a0018d6bcd5f213a4ab8c5abd82a GIT binary patch literal 3626 zcmaJ^TW=f36`s9vxx9#yCE1bWl<1v>BDhZD0*Y!Bv79!AtXiRx01Fk1)y_ylO?^A%*J!f`FlPXXOJ9{oOXU?2G^PQut(Qpa; ze*e#hXMbBHe*-wA13b@T|we^G~uNl38+` zAz3-tjeLUGoI`Pm)Sr7^IK4sjD4^d9J3f!?L{ejP9xO69OS)E20~=1bPl@x zC`DtngDQTQy2uf zA!F@IyVW#uMaDXvPzG*!t7}A3M2gtp8T(%5fH^2?OGEW)!?mP=@4xl<1UOFa%~8c8aEn7TG%S$l`C>q89VsN z@jH|peE1m-KK~5o(pvGwXkA&V3RhXG!P?rmP8({s!g>Q_4Y1y%3#A3*T69raXsUG! zFlS-hqDw_~0KG`>P3fglZjA5IWtdrbiQEcsg_jdu;U#jbpi^F6A$K3RhT0#w2f!(B z#4<$06f5*Sbq-v!dUk(|`uEfsR_L12tbt^$)T}Gd%<+cGQgi$eSCulP*A%URTt)p# z8^3>Ll(J$CN=%ff++N?8&g4svesGed;seao?e^ox{o%)LCAeVGX-{mokF%J5Jlgo2 zoj^MG+9}?LiylkcfC24X1YG2!+KV6^1aUh{<4caIGHN}4#rS3W=O49`C>;ovzgd_O zfB0xBejmm4a(26y4fvZK6m2|DgX5UBDa+4A)^Deoh)$v~5K)%qqjkyx8Uu1|w)3!; z#R1QIQGc_|hC$+4d+52eVtje#Zotn0R$zSd7%WGBya(5jj20&+ic-NiT*B?q>i*0o z?Q?c{k#U-DkL<%9OW5{k`9&~nhe6n5v<>Hu!kFdTp8YIHLl%2x7htgSJzj%Ncm)R8 zfV%54jpQXMEA>aX64bP9WAx8j{tCJZM@B9$aT~-20KNBpG zt8)Q)x7xwg&57>-P5puJmcWshaS$>H6=nR84FT>29;z_Qv7pJtBpCYYczzDx559OV zEPMg*mJE`D4u*(yb7}&EYEH0Ss3eH5>lU1OVuO<9Yv`vM){$C<;UPTv9t;j(p{v=t3y9d%ia(?gBauT2|C&>6Q_XGx zjiVJ~k*6$lKyp7d#ZoTm2ff=GemO zhImWObBljp$?OsX5Md`aENai$sGJh8VZeN6T&2#~MYK?OQS4ErF6d3r*I*C8jce=5 zQhOXKnSEuIv+H9-TlBL=>%|@lVH*2#X_CV4lKfCUmhS@|d?FZt~tVgP}z!FKF~HEZ0r`Lztj)SanQ{a)1FM4M-^dBUCvjP`Y;m z5qJymm7WbaR^iW5%7*+Wpyxlu=@n3^+hd+(QOY~0_?cP_iZ8K{ckCi1TXzpCEwImq z(wTVAu@~~nYp8U0^Ku0ybm7T=41*0hXTEKkn1^-9xPn`PJbWi{-j3F~^A&ui#h6Pv zM44$3|1&V~86oLzp;5Xe*HFl*{+d7`F`!k_cCozcFauKV7H8Vj+$qa;$y-Ic!eNLI?&-wCvIcj71)fUy0i9PFyT$R@gOYx=)!&3-$2 zbSJ8FIJLDCXJHWMPl1@2`A*y9SZ(-+IQ#;Kk8r?m&VLC*XQ9X_QvewAUx5T%?Z=TQ z615uTvw2#iXe}(V60@{O%4q^hOSO@_frjiL3|U`vj3N~qlZ^A#URYl51b}}4vJ9Ra z%Q51aquc*+D$ZNSEFRZrX^3#pBX0b^Sah^~XpNyKP6H?|zI1&*$><O@mt&eM4`ie4c%huezCYvS1Dv9;x(O8g(?xPDSq}K>)SM@+4UY2t&VpQcd3hk7 zY#l%+j>RHZ1y|Knj_F<$VO6KFL~|^cs$!`UsA{*Y&;EC#x_1>;wZ)82)A}Kuo#Kp7 PvGvK(_#@J4EF1p?y5-8v literal 0 HcmV?d00001 diff --git a/ui/main_window.py b/ui/main_window.py new file mode 100644 index 0000000..f787c46 --- /dev/null +++ b/ui/main_window.py @@ -0,0 +1,415 @@ +import json + +from PyQt6.QtCore import Qt, QUrl +from PyQt6.QtGui import QDesktopServices +from PyQt6.QtWidgets import ( + QFrame, + QHBoxLayout, + QLabel, + QListWidget, + QListWidgetItem, + QMainWindow, + QMessageBox, + QPushButton, + QVBoxLayout, + QWidget, +) + +from database import Database +from settings import FEEDS_FILE +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 + +ARTICLE_LINK_ROLE = 1000 + + +class UniNewsWindow(QMainWindow): + def __init__(self): + super().__init__() + + 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.app_settings = load_app_settings() + self.setup_ui() + self.apply_styles() + self.load_cached_articles() + + if self.app_settings.get("refresh_on_startup", True): + self.refresh_news() + + self.refresh_timer = QTimer(self) + self.refresh_timer.timeout.connect(self.refresh_news) + + self.apply_theme() + self.apply_refresh_timer() + + if self.app_settings.get("refresh_on_startup", True): + self.refresh_news() + + def setup_ui(self): + self.root = QWidget() + self.root_layout = QVBoxLayout(self.root) + self.root_layout.setContentsMargins(28, 24, 28, 24) + self.root_layout.setSpacing(20) + + self.create_header() + self.create_article_list() + + self.setCentralWidget(self.root) + + def create_header(self): + header_card = QFrame() + header_card.setObjectName("HeaderCard") + + header_layout = QHBoxLayout(header_card) + header_layout.setContentsMargins(24, 20, 24, 20) + header_layout.setSpacing(20) + + title_area = QVBoxLayout() + title_area.setSpacing(4) + + self.title_label = QLabel("UniNews") + self.title_label.setObjectName("TitleLabel") + + self.subtitle_label = QLabel("University news in one clean place") + self.subtitle_label.setObjectName("SubtitleLabel") + + 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.settings_button = QPushButton("Settings") + self.settings_button.setCursor(Qt.CursorShape.PointingHandCursor) + self.settings_button.clicked.connect(self.open_settings) + + header_layout.addLayout(title_area) + header_layout.addStretch() + header_layout.addWidget(self.settings_button) + header_layout.addWidget(self.refresh_button) + + self.root_layout.addWidget(header_card) + + def create_article_list(self): + self.article_list = QListWidget() + self.article_list.setObjectName("ArticleList") + self.article_list.setSpacing(12) + self.article_list.setVerticalScrollMode(QListWidget.ScrollMode.ScrollPerPixel) + self.article_list.itemDoubleClicked.connect(self.open_article) + + self.root_layout.addWidget(self.article_list) + + def apply_styles(self): + self.setStyleSheet( + """ + QMainWindow { + background-color: #F5F7FB; + } + + #HeaderCard { + background-color: white; + border-radius: 18px; + border: 1px solid #E5EAF3; + } + + #TitleLabel { + color: #1E293B; + font-size: 32px; + font-weight: 800; + } + + #SubtitleLabel { + color: #64748B; + font-size: 14px; + } + + #RefreshButton { + background-color: #385963; + color: white; + border: none; + padding: 11px 20px; + border-radius: 10px; + font-size: 14px; + font-weight: 600; + } + + #RefreshButton:hover { + background-color: #2E4A52; + } + + #RefreshButton:pressed { + background-color: #22383F; + } + + #RefreshButton:disabled { + background-color: #94A3B8; + } + + #ArticleList { + background-color: transparent; + border: none; + outline: none; + } + + #ArticleList::item { + background-color: white; + color: #1E293B; + border: 1px solid #E5EAF3; + border-radius: 16px; + padding: 18px; + margin: 2px; + } + + #ArticleList::item:hover { + background-color: #F8FAFC; + border: 1px solid #CBD5E1; + } + + #ArticleList::item:selected { + background-color: #EAF2F4; + border: 1px solid #385963; + color: #1E293B; + } + + QScrollBar:vertical { + background: transparent; + width: 10px; + margin: 4px; + } + + QScrollBar::handle:vertical { + background: #CBD5E1; + border-radius: 5px; + } + + QScrollBar::handle:vertical:hover { + background: #94A3B8; + } + + QScrollBar::add-line:vertical, + QScrollBar::sub-line:vertical { + height: 0px; + } + """ + ) + + 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 [] + except json.JSONDecodeError as error: + QMessageBox.critical( + self, + "Invalid feeds file", + f"Your feeds.json file has invalid JSON:\n{error}", + ) + return [] + + def refresh_news(self): + self.article_list.clear() + + self.refresh_button.setEnabled(False) + self.refresh_button.setText("Refreshing...") + + feeds = self.load_feeds() + + old_links = { + article.get("link") + for article in self.database.get_articles(limit=5000) + } + + new_articles_for_notification = [] + + for feed in feeds: + university_name = feed.get("name", "Unknown university") + feed_url = feed.get("url", "") + + if not feed_url: + continue + + try: + articles = fetch_feed(university_name, feed_url) + + for article in articles: + if article.get("link") not in old_links: + new_articles_for_notification.append(article) + + self.database.save_articles(articles) + + except Exception as error: + print(f"Could not fetch {university_name}: {error}") + + max_cached_articles = self.app_settings.get("max_cached_articles", 500) + self.database.enforce_article_limit(max_cached_articles) + + self.articles = self.database.get_articles() + self.render_articles() + + self.handle_notifications(new_articles_for_notification) + + self.refresh_button.setEnabled(True) + self.refresh_button.setText("Refresh news") + + def load_cached_articles(self): + self.articles = self.database.get_articles() + self.render_articles() + + def open_article(self, item: QListWidgetItem): + url = item.data(ARTICLE_LINK_ROLE) + + if url: + QDesktopServices.openUrl(QUrl(url)) + + def render_articles(self): + self.article_list.clear() + + if not self.articles: + empty_item = QListWidgetItem( + "No articles found.\nCheck your RSS feeds or internet connection." + ) + empty_item.setFlags(Qt.ItemFlag.NoItemFlags) + self.article_list.addItem(empty_item) + return + + for article in self.articles: + title = article.get("title", "Untitled") + university = article.get("university", "Unknown university") + published = article.get("published", "Unknown date") + summary = article.get("summary", "") + + clean_summary = self.clean_text(summary) + + if len(clean_summary) > 220: + clean_summary = clean_summary[:220].strip() + "..." + + item_text = ( + f"{title}\n\n" + f"{university} • {published}\n\n" + f"{clean_summary}" + ) + + item = QListWidgetItem(item_text) + item.setData(ARTICLE_LINK_ROLE, article.get("link", "")) + item.setToolTip("Double-click to open article") + + self.article_list.addItem(item) + + def clean_text(self, text: str) -> str: + clean = text.replace("\n", " ").replace("\r", " ").strip() + return " ".join(clean.split()) + + def open_settings(self): + dialog = SettingsDialog(self) + + if dialog.exec(): + self.app_settings = load_app_settings() + self.apply_theme() + self.apply_refresh_timer() + + max_cached_articles = self.app_settings.get("max_cached_articles", 500) + self.database.enforce_article_limit(max_cached_articles) + + self.articles = self.database.get_articles() + self.render_articles() + + self.statusBar().showMessage("Settings saved.") + + def handle_notifications(self, new_articles: list[dict]) -> None: + if not self.app_settings.get("notifications_enabled", False): + return + + keywords = self.app_settings.get("notification_keywords", []) + + for article in new_articles: + if should_notify(article, keywords): + send_article_notification(article) + + def apply_refresh_timer(self) -> None: + interval_minutes = self.app_settings.get("refresh_interval_minutes", 0) + + self.refresh_timer.stop() + + if interval_minutes and interval_minutes > 0: + self.refresh_timer.start(interval_minutes * 60 * 1000) + + def apply_theme(self) -> None: + theme = self.app_settings.get("theme", "light") + + if theme == "dark": + self.setStyleSheet( + """ + QMainWindow { + background-color: #0F172A; + } + + #HeaderCard { + background-color: #1E293B; + border-radius: 18px; + border: 1px solid #334155; + } + + #TitleLabel { + color: #F8FAFC; + font-size: 32px; + font-weight: 800; + } + + #SubtitleLabel { + color: #CBD5E1; + font-size: 14px; + } + + #RefreshButton { + background-color: #385963; + color: white; + border: none; + padding: 11px 20px; + border-radius: 10px; + font-size: 14px; + font-weight: 600; + } + + #ArticleList { + background-color: transparent; + border: none; + outline: none; + } + + #ArticleList::item { + background-color: #1E293B; + color: #F8FAFC; + border: 1px solid #334155; + border-radius: 16px; + padding: 18px; + margin: 2px; + } + + #ArticleList::item:selected { + background-color: #334155; + border: 1px solid #385963; + } + """ + ) + else: + self.apply_styles() + diff --git a/ui/settings_dialog.py b/ui/settings_dialog.py new file mode 100644 index 0000000..d84c03e --- /dev/null +++ b/ui/settings_dialog.py @@ -0,0 +1,134 @@ +from PyQt6.QtWidgets import ( + QCheckBox, + QComboBox, + QDialog, + QFormLayout, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QSpinBox, + QVBoxLayout, +) + +from app_settings import load_app_settings, save_app_settings + + +class SettingsDialog(QDialog): + def __init__(self, parent=None): + super().__init__(parent) + + self.setWindowTitle("UniNews Settings") + self.resize(420, 300) + + self.settings = load_app_settings() + + self.setup_ui() + self.load_values() + + def setup_ui(self): + main_layout = QVBoxLayout(self) + + title = QLabel("Settings") + title.setStyleSheet("font-size: 22px; font-weight: bold;") + + form_layout = QFormLayout() + + self.refresh_on_startup_checkbox = QCheckBox("Refresh news when app starts") + + self.refresh_interval_combo = QComboBox() + self.refresh_interval_combo.addItem("Manual only", 0) + self.refresh_interval_combo.addItem("Every 15 minutes", 15) + self.refresh_interval_combo.addItem("Every 30 minutes", 30) + self.refresh_interval_combo.addItem("Every 1 hour", 60) + + self.notifications_checkbox = QCheckBox("Enable desktop notifications") + + self.keywords_input = QLineEdit() + self.keywords_input.setPlaceholderText("deadline, scholarship, exam") + + self.theme_combo = QComboBox() + self.theme_combo.addItem("Light", "light") + self.theme_combo.addItem("Dark", "dark") + + self.max_cache_spinbox = QSpinBox() + self.max_cache_spinbox.setMinimum(50) + self.max_cache_spinbox.setMaximum(5000) + self.max_cache_spinbox.setSingleStep(50) + + form_layout.addRow("", self.refresh_on_startup_checkbox) + form_layout.addRow("Refresh interval:", self.refresh_interval_combo) + form_layout.addRow("", self.notifications_checkbox) + form_layout.addRow("Notification keywords:", self.keywords_input) + form_layout.addRow("Theme:", self.theme_combo) + form_layout.addRow("Max cached articles:", self.max_cache_spinbox) + + button_layout = QHBoxLayout() + + self.cancel_button = QPushButton("Cancel") + self.cancel_button.clicked.connect(self.reject) + + self.save_button = QPushButton("Save") + self.save_button.clicked.connect(self.save_settings) + + button_layout.addStretch() + button_layout.addWidget(self.cancel_button) + button_layout.addWidget(self.save_button) + + main_layout.addWidget(title) + main_layout.addLayout(form_layout) + main_layout.addStretch() + main_layout.addLayout(button_layout) + + def load_values(self): + self.refresh_on_startup_checkbox.setChecked( + self.settings.get("refresh_on_startup", True) + ) + + refresh_interval = self.settings.get("refresh_interval_minutes", 0) + index = self.refresh_interval_combo.findData(refresh_interval) + + if index >= 0: + self.refresh_interval_combo.setCurrentIndex(index) + + self.notifications_checkbox.setChecked( + self.settings.get("notifications_enabled", False) + ) + + keywords = self.settings.get("notification_keywords", []) + self.keywords_input.setText(", ".join(keywords)) + + theme = self.settings.get("theme", "light") + theme_index = self.theme_combo.findData(theme) + + if theme_index >= 0: + self.theme_combo.setCurrentIndex(theme_index) + + self.max_cache_spinbox.setValue( + self.settings.get("max_cached_articles", 500) + ) + + def save_settings(self): + keywords_text = self.keywords_input.text().strip() + + keywords = [ + keyword.strip() + for keyword in keywords_text.split(",") + if keyword.strip() + ] + + self.settings["refresh_on_startup"] = ( + self.refresh_on_startup_checkbox.isChecked() + ) + self.settings["refresh_interval_minutes"] = ( + self.refresh_interval_combo.currentData() + ) + self.settings["notifications_enabled"] = ( + self.notifications_checkbox.isChecked() + ) + self.settings["notification_keywords"] = keywords + self.settings["theme"] = self.theme_combo.currentData() + self.settings["max_cached_articles"] = self.max_cache_spinbox.value() + + save_app_settings(self.settings) + self.accept() \ No newline at end of file From c48cfa2f69f40b4155e7e4a9c2013825360ee97e Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Tue, 2 Jun 2026 21:35:21 +0200 Subject: [PATCH 2/7] changed style, fixed some bugs, needs improvement --- .idea/dataSources.xml | 12 + __pycache__/database.cpython-310.pyc | Bin 3940 -> 4996 bytes __pycache__/rss_service.cpython-310.pyc | Bin 761 -> 1149 bytes __pycache__/settings.cpython-310.pyc | Bin 783 -> 449 bytes data/feeds.json | 22 +- database.py | 91 +++- local_data/settings.json | 13 + local_data/uninews.db | Bin 0 -> 135168 bytes rss_service.py | 18 +- settings.py | 22 +- test.py | 15 + ui/__pycache__/main_window.cpython-310.pyc | Bin 11520 -> 16607 bytes ui/main_window.py | 482 +++++++++++++++------ 13 files changed, 487 insertions(+), 188 deletions(-) create mode 100644 .idea/dataSources.xml create mode 100644 local_data/settings.json create mode 100644 local_data/uninews.db create mode 100644 test.py diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..cab4090 --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,12 @@ + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:C:\Users\geoap\Documents\github_repositories\UniNews\local_data\uninews.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/__pycache__/database.cpython-310.pyc b/__pycache__/database.cpython-310.pyc index 5e25043f577de63e1f717be0196feb335e3ce148..9d714c4dfa647aa974d8cfb344b7f2074d03d516 100644 GIT binary patch literal 4996 zcmc&2O>^7E6}$K#M3J;Ce7nChMmjo&{*GRI#kD8@gr=#}_W%+kM5R9XP>@)>ef##|?c4XY z=$6Yx39h~BJ6q2dCF!5onZ9J`+=M&40u7TG@g)nN)F&NkQG&4S%N@m15}5d^rQVj9 z!qh#9sZXh;F|sCAwHGj7YE((A+;yUd@YGj(k=G3z-^G#X3njVzsDOCmZXPsvcS z<)O4CZz)^wM)=V`g2p3lzkt{S9M>alYYuw4T^vgL^9K^{0_a0xq^^ixA7i@-BdAZiA-Kwpb_T5_RGl4P*6`EK}=f!I05%WY`2p{`i#6O7( zZZ`mehL&7kUymU-K3&fxDh>vw9+`!o2@%~@Qn-CC_TK)gF< zqjh0sN#PXA7d(1B z31()u8};9{&DmG=`VV|Be8^`v5m!7*zriCHsBI^jdBtn0$m=+p-0lni)r_lGb2NjG zYAY?XIWpq0*&4OwWn*b|rG2MiWV{U&@@iETWpJ&yz@KopAMsdoyPb{~iFv?U3#&aY zoHQGlLUhG*B96g85-QT>ICjy=Pr)6YghnHjC_fh8qqIa8NCkYK{x~?D`Ft(}#f6cK zxfqPO4W&vve**wUc4_M6Lx{ioltHFBNFm5)BM36i2+}hMa?S`hpW#XfZNNQ>B61d+ z%MIrYM4g;Q(XZ=`HM7}5D0i(kjr#4zY7?SGqqUj^2_qjEChnOxp~SiJsF7>rk>~+= zOlGKg^u!+X_-x0sbyYLyi`q)tTrN8M zecb0_=a5ClrO^4B+au$u0bTeOGz!rw6jD)6x)U_MsUA&5DFK zf$@+mOJ5bxNcW{-l87EtO!vibASa3epur2^m{`aC_)Vyos__vru-*I z_Tulz2_=Zj9O!P%7uXb#^9T526`P57B`ki`fN}gOj zH?FML!1j_vu?bb&R~ppRU}{gvH}o-GOT7XQBg}InC{XFek1ZKW*u&mZyau^Cf?UdYWCkGJP0FK~jQ0nF^_v&+bqJbpL ziqpX9@B&US?sGUofO0bZ{u$^(j5Q@l_k*+99vnfMvj`L$H!cwyYlu|raXuFB0jf&V z0!4NGE*wja5J_AHvcJHc7y$@S$plE|#HEh&B$M^0<2XjgS}?Ibz<6?Y6XoI)0guxsbCT|1IIXEIE9=8bJBe?z)3;?)z61IYkLfh2tSe-Z=zpLuOUmHwOnXPo8++0T znA(8DZ$o?lP<0W{>VebYwjCF3yVGTTAK{X1KkhqzI+A{UfKO0qnJ=mcy$cOibYUdk z$G(hx8G5k{OHI{q2?x+O#jmiz?^IKZ#8rso@PL5>&-vjm6W}B%mKHCZK1Vf*2Z51Ly1ckyyjS(Hl^f%B?`^q>OYQ=*5y_aG5A)}oyrd`C&7639BCJ?iDL Wel1<~b&N$J+LR7atOW6QQT{K@nMG9q delta 1593 zcmaJ>O>7%Q6rP!#U9Z=>o5V@nrunHU3S{FD8gW4oA`CH2)UuSsQK>~`jAvY&uH!Ph z3A74|5V>-Q=EkLo;F1Gedgj29E4OYB96;j2h2DTt@n)TvII0+HpTGC!{mpxCX7~AD zC)~EW;mE zc%N*eb#$OOEyC**IIXI0qkZE5VZ_iXI>f%05(e1FNwQ=raEOE>H%ZwxfibX)Eo@`? zkc__N*D;8W7Xi?XIspvBpFyN`w2!y(0mdl7I)VKP5u9@lM|=-r@X!y@z|)h zTdii~5n(I2{_!RkcZ37DLtKS2K>FYdRk10yZ5Q9ob$j{1n6i{{(#!W{CUtqLVH}X7 zYHHnT+Wy_2yNN9OtEH8){5tiO{8mrP8|DQK(v)ACZSK(|N09}ev38S3Vi1t0B^iog zz+y;|-Xvo?tZngXO+?Llz|Vt1coqo7hE1?VESQPTkNicSxVF5VzBVsXpy6SmM&a1M zvg`yo<=;8!KYT@EYX;}!to1$qN&ar#OOEklU+(@7nV0cP$`&p$Wz9O&HYT$dE1GD3 zayn;gK4^v!wBYtaOU^h;1=XD7;1x4zYb^-8bm!nXwRJ&}XB8Qe_nc|(RE{=O>$n8r z0;q*!K&U|-`N;WvP7DBdh&%pHnGP}@@CXVR1YHkp`JH=hCMgo>%#B_T+94M+DuZ4J zVpfH_Ki9W~=T(^FdcLSspVqHozR_rldbefOpb3MxC4cFkbzvO)!Or(I-xtrwT6TJe zDnIH13pI(!NQy~?RP$o0>L~JxR~J0ifQz-}MkESK_JSfRL2*%$|1wWdGUGaIsOKY` zN8j1A1&hu3R-0`Gd_lcVp=!E`Y0%{C{FpULlzSWQr+XvWksZ z#YdczE=n;HD$v1HD5=7aawJux0~I3$I@HlFn?@>8J+#D1yk^7n8)jld;S!ZuZHr2~ zg!sIv^`VPE+ZO7alhf}b@J4KGAIZ#-Dc-Sj20I-)`^}ZuWlOFCC0AHOh2Fl0b^)GW zu}9wi9PPmYdvkrkJC4yYMK7gFRve!Bmma+c#@TE3?y-`D{?KhzS{2zOZ)&S+3xw1K zEtHPA{@P?u+v>Q^iqeH;QNJ}#+RK@qM@5yL=%*uj3VJ#WfxgKFC)z@S67)z-jm^NC zi^85_i$W@J=_FKN1>Pe9KaQ`iOyZN^7+ zai}lM7>t=<(0P&TC$md;O~1F0<#ysBO%Q#Axy=n^+kC*c#egRwMQMkNpE7`JgstD! z#W@x(?B%4+Xl_%j6$R%sqCbaLW&_EO>l_7(qv`mHFN77#z{<05#h1~FErJuakexV- zaR`qVVHYkF#g$lzMbt&+?z=%Jm%WbpcRyM$h2+siUXE0Za$wUg{J>T6DPG}PQXGz| zcL>{99SDyLZDC79d{lcux!AN-m0@#xv-Y?w%&AuM&7=Bl-CWdrw3)-$rT;z{s5Nkl zm`{3}lBQE_a}1nh*7g2&a=1YTt!5cu$6dI-sY5Qn>=1aVwK>L^X4*i7;W14b2L{wR z7iF`Vu9a{5FoprX!F6P#4WCR+C_n6yYl7=ir#U^q=gwsyB@Dy?RJ^Cp(;Q=}h6CmgwHQYhT1;)a9A#g8^by5|%^}g$xSE?s3 Wsz$Y?en~%yp-tamiP#SA^?w2Eiy{*M delta 416 zcmYL^ze@u#6vyw9Tzb9QS{%f-{s6mpC^#vC#YG%kx(E(k%6WO!)AoA4q(a5f&8gh# z;^^d`;-1M({1X(UCRH%-J|W5HeINPuKfTJZTn-$wUf)(Oi*!<1c6TS6YkPtL6&^6~ zYEF*}E(-Uo!v#5ULjS}1A<@2w6sTkX+PAzNrqIr}NTaDaf*N5O6Knphg4s)^4o%4{ zKO-A%Gk+khCE-8vC)ahZ7w(veFSa%7C+U^W-^tE4*TE=FZUAMX##l7HXhYDZ;hcdE zdZ6M95Zy?HL4Hd2k2F08rFoQPkczN?%(|XIr<6q~joJecSh3xHger;qAhidn!tpHl z*}+==;vUz3vu8Rtwp=#|+$v#iKBbFY_Qj{Hgbj0R#@NpywU{^1R4TMTkxDa>#A>Et b&A$JmZcV>67>ZFJ4qS7Rc2L#v$twK;Ez)I| diff --git a/__pycache__/settings.cpython-310.pyc b/__pycache__/settings.cpython-310.pyc index 2b3a3a959997a6e9bfc12b87044a7fc5fdb843ac..ce7e0f42c4e09dfa2947cbd5bbfb44841b94cdf2 100644 GIT binary patch delta 313 zcmYjNyH3ME5ZpVz&c?RrQy?K47f{fG5Jm|i1(8KuU1K@yNwMPxvK{egD5w!a`2i^N z8GHe~TPps7b;qoBM>E=;V&BX!?YZ_m2ME@|UVh8~eg^R$I*D`YHll%mtbme9#sInG ziYe`dedwQ%8`4xJY9o-AvQQ@$qzz(Q*|~!zmG#OQC>sU2J7~`#@SR_Zg@E;Xly=Ma z?Pw~UqP|Uw1k)~!1KwMAX``yH?S*>C+AgVAPtLl^Ds|r;;bIoJ!$!9(vPHuwN{TRw zl392?**x--L+K839L8ZXp5F9Exx%dJy}`H$=aVD~# literal 783 zcmYjN&2Abo5VpO}vcR&XRc}=vfD6YSsw&Dh5H(VYppc7&v|8c`ELqko+cZr?PW0M$ zNRB=AA^H|RRqCm)&`WERfSA>c=V$%-n@=N7fayfg_G{ zw7?8wmy?_ihWG~Q5xZpM1&IQCbzXGUD|xoYeh~PJ diff --git a/data/feeds.json b/data/feeds.json index 869295c..9ef12bc 100644 --- a/data/feeds.json +++ b/data/feeds.json @@ -1,10 +1,22 @@ [ - { - "name": "University of Oxford", - "url": "http://feeds.reuters.com/reuters/topNews" - }, { "name": "MIT News", "url": "https://news.mit.edu/rss/feed" - } + }, + { + "name": "Harvard Gazette", + "url": "https://news.harvard.edu/gazette/feed/" + }, + { + "name": "Nature News", + "url": "https://www.nature.com/nature.rss" +}, +{ + "name": "Science Magazine", + "url": "https://www.science.org/action/showFeed?type=etoc&feed=rss&jc=science" +}, +{ + "name": "NIH News", + "url": "https://www.nih.gov/news-events/news-releases/feed" +} ] \ No newline at end of file diff --git a/database.py b/database.py index bbb48bc..5d73e7f 100644 --- a/database.py +++ b/database.py @@ -2,44 +2,80 @@ from pathlib import Path from typing import Optional -from settings import DATABASE_PATH +import settings class Database: - def __init__(self, database_path: Path = DATABASE_PATH): - self.database_path = database_path + def __init__(self, database_path: Path | None = None): + if database_path is None: + database_path = settings.DATABASE_PATH + + self.database_path = Path(database_path) + self.database_path.parent.mkdir(parents=True, exist_ok=True) + self.connection = sqlite3.connect(self.database_path) self.connection.row_factory = sqlite3.Row + self.create_tables() def create_tables(self) -> None: - query = """ - CREATE TABLE IF NOT EXISTS articles ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - university TEXT NOT NULL, - title TEXT NOT NULL, - summary TEXT, - link TEXT NOT NULL UNIQUE, - published TEXT, - fetched_at TEXT NOT NULL - ); - """ - - self.connection.execute(query) + self.connection.execute( + """ + CREATE TABLE IF NOT EXISTS articles + ( + id + INTEGER + PRIMARY + KEY + AUTOINCREMENT, + university + TEXT + NOT + NULL, + title + TEXT + NOT + NULL, + summary + TEXT, + link + TEXT + NOT + NULL + UNIQUE, + published + TEXT, + fetched_at + TEXT + NOT + NULL, + image_url + TEXT + ); + """ + ) self.connection.commit() + try: + self.connection.execute("ALTER TABLE articles ADD COLUMN image_url TEXT;") + self.connection.commit() + except sqlite3.OperationalError: + pass + def save_article(self, article: dict) -> None: query = """ - INSERT OR IGNORE INTO articles ( + INSERT \ + OR IGNORE INTO articles ( university, title, summary, link, published, - fetched_at + fetched_at, + image_url ) - VALUES (?, ?, ?, ?, ?, ?); - """ + VALUES (?, ?, ?, ?, ?, ?, ?); \ + """ self.connection.execute( query, @@ -50,6 +86,7 @@ def save_article(self, article: dict) -> None: article.get("link", ""), article.get("published", "Unknown date"), article.get("fetched_at", ""), + article.get("image_url", ""), ), ) @@ -73,7 +110,8 @@ def get_articles( summary, link, published, - fetched_at + fetched_at, + image_url FROM articles WHERE 1 = 1 """ @@ -127,4 +165,13 @@ def enforce_article_limit(self, max_articles: int) -> None: """ self.connection.execute(query, (max_articles,)) - self.connection.commit() \ No newline at end of file + self.connection.commit() + + def load_cached_articles(self): + try: + self.articles = self.database.get_articles() + except Exception as error: + print(f"Could not load cached articles: {error}") + self.articles = [] + + self.render_articles() \ No newline at end of file diff --git a/local_data/settings.json b/local_data/settings.json new file mode 100644 index 0000000..cde3c07 --- /dev/null +++ b/local_data/settings.json @@ -0,0 +1,13 @@ +{ + "refresh_on_startup": true, + "refresh_interval_minutes": 0, + "notifications_enabled": false, + "notification_keywords": [ + "deadline", + "scholarship", + "application", + "exam" + ], + "theme": "light", + "max_cached_articles": 500 +} \ No newline at end of file diff --git a/local_data/uninews.db b/local_data/uninews.db new file mode 100644 index 0000000000000000000000000000000000000000..d18b69d0c0bc1f725db1d07809e61940e41b45fe GIT binary patch literal 135168 zcmeFa3vgU%cHajO1m7ffc9%OlJIkFrOO0kn#3fz?L2`F?1cD%mAwgmYa5&uA5!L9v z0D905Og{*MtJSCh5=oU6IVCHRWJxKfT#4;CPD)Btr7D$NiLFGB=oveXA}fitvLhvy zY&~Mhb}T!;|M~9i?u%|T*nEtT7_40ki3aZDJLfy!dHm1+OuuzCT?)Lpd?A}Cc|#o^ z>*(z4_*T#B=s0-2qodq9iP=n0{2)V! zc&8YAP!4j*jsM&|c4K^WX55<@J%4rF+wikr$R)BtFTeTP>Q_7WK7Z_3=fi_`%|xM; zPIBq!zq?{riN5Q7;ec27C!O*puFZ^J8o%L9-I$mhy>Z*SGJe||y*YDz;u;s29KSZx z8~crNE`2vB6w{>@Z)W_R4Ih@$rA*M|`^9oLn+DG9 zQo+4Y2#InjZ~wj%t>>MA=x<%9+CHgUH~;@s&#M0Va?k(M^T$1Z(DS=JzuohVp5N&C zdp*C>^YcAF-Sanlexm2cdj9jC|Fq{X_58)2@9X)Vo`2W8!yU&e z`&VD8yn1DFb#!uK#=90Q7pId!F7QxxzH!bQ^>T?^zL-o0xl+1V^5zSPR5?)!ir#X% zwBRiS1&{5y;?3vtDQ`Bv@^(6xTj})@Ij@|{&&{Qi=|slM2H9DDFhA#&76Na&RN#`{ z)x>OgmtHSb2#Q5EUwO$Z<-KH(31$o2#2d?JvoctGDS3%ZJ~v-Xrvh&wUGx^p*+j0_ zn@qDeGZ=xl$~>YswQ#}v#&`YT;OUbKrP5OITwh;~S2~$Zmre$$a^GP8;OV~D3;ER- z^7TSKFXZcme7%s*3;F%SgJ&-m(!Ji`S#L72;^`XRz|gt=e*WkA{ipi-^)0`j{|(Lz zoITe+eD2in$-$BSp@HEO_ESxt1(OyO`%=MNqMRx9&80D=eZ|s>{-bA(cKn??$vh8q z#~GSC`9gX=ol9i;%!DNS?At|qvHsyc-qTOkt(pIUR>i+7q4@m+C*NP1eF?egjX)iBlPLyLO!37Bu5L$ z1>PW8Di_d}T*{luByzcQZoW=MkYy%Fq@1cOV#socxz}6B=kf(pg;JqhTKL9y{qShU zu30X6xd1ib_vV5i<1KS#u6{R>DeDyL{l<6wh*wOk6rCE4o|uX&NO37hECvO%&l_7{ zJyF6$^px%q3+d8*e!`n7r0)kjRxTG5T4+ilS_MDhR>3bOmf{QCTsuykJGBk91L6A9 zqpu8}pYrQB)}8*EUfj7iSe)pem|VNreX!%$d#eY(hF08)v|?%@U&^ zlp@%EKnrqCEK8h+7ZcfZ1}iXEK>Jci9BYC&FdunL%WRNAqf_2eP^xY9P)TGT(Ed6d zN|g&zs?tIs2)y_6D|6+Hmy!-}>-x9wf_p(BnJxy!76Og!jU~N}k47do+vLOBa+4#C zR|fm1^W*;X*cg8sP2EjkBl+8fV4hXFgHDcMkcQ6G4=-ajyz=V<94 z@ZK!vLVG{3T~)!q!f{Xx*wOfnxuT9{EWN*^6SXG)t0Q;bygxN@c5S6g@_)55UYSPz zZ$vwrgWp?1(B(`KS$f5lVkyWXw{j-sWihHauDKO#ZwXQ9+~TN55Odnnvi)=AAmayi z+#~0USogXgO%%OiHi3obF9~lJn=qH;iF>{Ipu}b8G1B=m7fDOK5=nM>S&l}YOMA-; zdGs*D-!UKQmc|;h!CObK;Ja7ASn~?LUcuKZV8EkS@b&b5{K+>F{>ZrjnSyO~*q`o0 zc6TnZ-*f)VLV+bCO2Py2r*4c-PuJV@+oz{5Wf!LJu6@U@LmkJyzS>tgf#~(?=I@Pq zOPt&~bEPvu;VuVbP~uxD=i}weC9hb@FD*$_so*ZwdrAJ-bg7&|B^L6_&hWFnQvog? z_Pw;sQ)$=hatT=n(~@xe;=JMHpcJJp2}GPkMOM5-O48Tqkk6!(EBFBk&KBL; zG|p}!UjXwzSCmzf`jry*TAKav#kh(ey_$J@yqX`qnqT*7QiH*nGAi0XShE4<%56_I zxD^V_pR!epxq8=64}E1YeSdE4?M`j|uFBcUS>!(!$^T}CMJ;Lv`m=0HR-Qem4O8%P zc^taCY&?XHs7v^AX*(ov!2{w@`2jkuQ-Mr!Xf6;8znbuh@)&WX=X7uI`Sy&yP9CJL!^BI%{G(!)|KvyP)2dsM&rsDA8G5vqGs+<6{z6LCJh)1CP7rS%!U zes28&UyrSi@b$Cn{e1n*`fGfBVg2)b{q*{0`1<_%$NBoH_5FPP!*N;D3 z;_Gvj5BSHVMCl{xrZ`{s{~?fL6n5TMdW*INNkk2Yk zBuLDnIpzvp$#LkH{L2B(b+Nc&Ap?#=ab}_1>kXW$i=j@R8?KqvZ!YXPRykU`{4~~~ zL_CNlYWEj{a?$oZu^@Vt!!#`9%SC;J8(*}C_@;sW{wru<7SGbO%Z~AZGqqdBx9}^o z-N%+|chngu3#ga@N5t}G=y4&B<*~4IfhW=l#<7KlbFvhQfmw-xy2vnoi*LHqee8qU zEk@Y$UKn@P{fjM<}4o*!j@l{vvn-c+JgNGBJ)+d(-u-*EkbQ|AV2vBmKmt9H0{ zgEujrTz(oiEGw(Kv_$?$<`hpTPb^MMol;!HZG#+vWDuBbB{oi)oo>7xXJ74hpHFrl zTdCdcd^w#-Nd%)4UN)Z!@Z!vY#FlUvm1LTU&a-JuJBLd;mrUbH7H;1%+zLQt+YnPn~k_$O`$SYpCShn*niQ_4M>izsI zJ~NKf3YK`j*DEfj$!ZW4J^OYvA5Ne9Vjq@kn|Ky3FxcF5u^I zolGXbtlOEpTx20_b1y5PB9BoFf@ZfEuC3r_?(sq|$6qMGhstM_ce_QhB^Rv7W8-UL znM0Ge8!uq|$h-u6y%vxwD-^@r2HzNWznpBck|Xt6`a+r;cg1g96_x(7A`cN4-dkE& zDW;P+|6~ZY8Ati{VuE}ds>CuB5-v|RSf`Mr!)mYi>G%n9(R@2z<1R_P{5dm(9NgrU z$nz{``A$JHLjCtzy{4@5oEPX>1<% zpY!ZL6}N7i8tdsy9usQO_pv**JIvsTSC8pLG8rtD z+&O)*oFNwDsCL>^Y@JwgrzcOygX0|OCAnk9S22KOhOd@fwA>HIz*)+X zx@8%xHy`rDqW zf5k&z?LM|xyX&+av{&+l+-%0u&6uksz8sbzv@Wq+X5;$BtmVNX@AQ1_>hc=H!weaP z6%8+u&f*K9ur4>}Me>Q>-p(Cu62(OMT(f)Dd3gsGxMyeWp0T_q^3ZYV&8~P?)5U~0 zT~-C;Y=5seJ!yX%=#9!EXHPY|QJwABU-=Yws&j-!Wjc#XX-;tLbnL!}A6G)f9=%6w z2|Yr5MSphulHMCyajS8{hSPULldTz-%GE6na?BTysTEeC3Yf z52Pyg#|k{pQ0#d|*{s+k>@6>>U2@Kw=J_oFiK(4WD8ZzEyLTiG#6wn{fFBA|iQy4K zq7Nr6pIwQqgW~9eAGLw!o(HaVpLpZSuFDB>W9*1aiTe`Zt%W>dBO-HqnnBdh?@t_4V>~QF823#{L{l+v$N+9I{4qtpPd!hS%IAu*ja&{71&vUofX(w zft?lDS%IAu*ja&{71&vU|G%uj;m)pOyGRXK`^CX)ivRcS8tFLl>Y*<-@*T%rr!%y*unot zZGAiV{~i2)^N!CQ{QnOA-{5Vrc8;fq|9^Tdyw=s@TT8~YS~8}hUW_I^T?-5O*|*A9 z<`w_%gXZt>&mH{j{lB^Q54(TA>yJDCsE+qf`?=P_gvvW`y#iK&t#OlAENT05O+o!771wlqLzB zq<#PZV}>XM~im?3stRrh>-H{Zp?{5#kBCsS+7 zhqbBqRz9qJ5gj-y5ePH(M0n1dhT9UN7ooGk0E8l=us0i+?=0#e`h-$>HbCqVno;O8 zbq717sJeiKBhz<*_|aOz6+kCC$Y|K@3ye*A6~akdNE}`A9Xd7a<#gy&KSZa}wilvP z{qS1q?gg7D-i9U?AclqAR?%a99d3l;vb6D5eDg}0RwTy?K-HJ@Lq?*3XAcX}e2%w0{txt2MI`CnXpgVwLQPTn?w&ghFt zN!~G5SPU!0MYd{{5rrmCy7RS7t^EQw@N4Zo3;0+ z@b1k_iL_~>H31JZA@;YaFwCbyIP?}SV5bKK+aJEkL4 zAyM1(*KVF)E@X$2Yj^fb3p!U{q*-fn^+covF;IS6%>ZoDnQB)NK=WddnWMi@U2Sx{ zrs!+JH?sydZTAqvoyq4HY2Z&UIyxSG!Y)kH@r{hf#;?m3(6J)3kP zmGXFp`h{4tKnopkF!yj#!dJt}7ZZN5ys4z?+8MT!q*Q-P3i4Z|wZ}$W~zOo#*k$ zvd}7tORpM?wcFw9f;XC&v(Q;BW%8VE&>YZKjM2;_I3HZ_jiY~^^LES_2yUs1oABMG zSrYSkyls99*9`+K2Wug*1YZYNQ4bJ3<@CMLpT)O{&s!K@$Gk!aZ{4}J*{aRCM(UyC zm!+2Qd||fPS(#Y#!UV0vh^|ZmjY}M zQi?=wrG+Hn4G9yR=x$LB=>8nXEnzw!1sc$yItv@nCDqf-nz`$EJhqKgwG-Xj=mpc9 z+87iia!1Z}QIS@FKE(HInV4a~FsaRtF#?1jL`5fJ8D%Q->*zI<5oP6-x^$Ne%3=d_>q) zW)LD73^u07&Wwl0QHw!6n$4xd!bWyh?Xmn?6m?3{?~~r(XR)YkeVka^TZ1DlBoBq9 zV_xvx?Kc%|9TUN}*|99CUtLbixzwaNU7DSJ`}Wz92gg1xZ)mOCOz=n~&UQH?rh{CX zBlJpvv;@0~ld3cig$*f$&^dxkvdTQhIc^>I&fSSfDI}2PV3wb&^46V%t3x#k>l)^= zM6cN;SNDi0E9x!!Yyws?(F&CoXtWmliOzs1wSgAlY%M~`g7KGW4;!UhNZ&m&yMSFG z-V7bh+AoXlHL(HFYf3Lh8W}2L#2MR0FU*p|j@$SeHT}3cd-~4&<@-}>=buAA-mSa^ z&(q}UWTYRD$og?L4WIEmNd&l$;j_AER3K0juaJ){iYlV&Iw!*)LB!baoCu61ykZ_6 z2qezGAe_+7r@948J6Xt#^rVIGAi{}=Ng$td#3YuM!b{eH4xgkMy5^FLv6E~Y$q{dx zHoC?2Z|_T84PPku)m4@BMbd!(ukI#f)V5bj%H>ypK9eWISb47$`;ao$QZlHhB{r( z1h#F&q79Wzx_qj830uC}Zt5Sn@y_Vn_=MsAJAa&#@Sy|ye}3;zb^j+_|1tmE`Lmr@ zU@iSQ<@)@}yT-;^&F0o6WSNbf;aj?{t}+KpE` zB%Kq_K1Bf0_mRXSKo%h#k&BV^_^u9{GUmD#7Sqae&VvoP?tF3G`NnhShctJ}A*fzV znqPZ#z#%s_CAQl(dsNO4*6kKg-y}gdyD}L}ow+glp!1kyy?6DP;cu$8@`?EPNd7>I zEfzmV6XqNo9gHDq9G~EwDtRL_i%SD-OHmAfZgK#Q3%Q40FdKXux<3xTEZmft1Wda@ zG9lbXNIhd@y(AmboNw`(Kg{VZB(IxMuC3Ayl8G>?L715HXpe{nNk)L#iHHV$d!*(H z5N8|QRthk!ceth1Y@!2ikFU%u_~YdVM?Whac#dfsZ*X2-ihLeB{zBHC$Ky$yj>47D z$JMclle3hzsWpyRnNq!TmTW%@x`83UGuEyP(H$@6z*bomBkpHa$yw- zELC(ltcC63Ckf~%5eC5`WJIkr!M&g#z95R=A}sQ6)ri}T-=L1`Xi8I#!F&^Ch%rHK z8)X=s@@+!Goj3Evg>u3lJ?Wo^$Q5ezA|{jxLUn@{CYNvBn8>HEJvi{9^y1*^w4DRb z*XzX-&{oCwln4P?>q6lW@6tD}|{(J#9ImmjczRJg@6eYgat8Ynh2;G7l+;&K^FC$Oz3r!rmE7kwkq(*^QpbNgZz<) zjv_YS1z&KvJGBBR*X+oECaI3waoZ?~UJk;3pAj|HX)IUXT72`~y|IfAUjGbn(emnt zkj74~s_bKst{Jvq+iauf%~5uv6-r|8MP zb2;*`H@mqG#LEuQXFfzKU8Kqu&iZfgRh+Fe`WOI)MTwxar4dbmA41_(`z5T zAY=0K>T{+*Ri{#l^mM3XadyacsK7KUihqb3MYN7(nM=!5<1tjE_QNGM5fAtxg%7SW)DnAUvZVh#J?2 zZjg+c;gxtqKl67@&x<~dyt{6hcxBmUY4pOyx6p#dWK ze>pNC`fqgV+Fu@>(>L@ld^!JQRsi5Sb1x4OYv2w5J`*osp{&xr<>B!04Fc|nfwB91 zXv3mh4u(w+YRJv|2-$@Zv!%lEO2CO=fxFply`+TW2b)zS+EHIN4MMjHdAT zNg6)xt-EeEJNIDNr2&;WasN>GI5N8_`!kJqo)Y zB(lsGu+zfng$QglWYGnx%-i5*OphR{EVN1}aIjaQ;9>fy2+kYNL0+J+z$NUx$Z$ho zht%S%mBKfl(6O!5Vp0s{D^o}VK{e5j+h3U}jowcOYu|Pp{rCVt@begiD%@yO#`<~) z!&Lov5t$MLK-j*Y2B3@2m6&zyIumSrrYR`c5BEJfz(f&tt3Gk{HbXpcbUTW6n|2@` zUW1ND{XpjX6{CxT|94fA7M4B+ccAPVr3_Sb(Qp)hMZ-~=%QrqDW!uQtd=&$3=zT&d z_%`IF26mW9&6eMK>vrzJbDx*VKfKy!Js$G+EEIgw`7XLnm=lPmvdU!_MBnnf@yRI| zbs5>RY@b{8^1crMnnBvj)>$Un2EaE`qeo$n`lOCn9a7)3@21S9NN?1+ZWO|SKdhs++Pb` zX49`!^49QG1vWk25S}ptd=6@WEyQ!m16GTn%J3D?5!cnB@jaMuXR5O{n*%6fh}*If=A>|6Ffh$}k{I-+^-1G_nX@-WCo<^=2VcSxyti6p;!<72 z_NY^<;!?AGnpt-n@IB46XYYav1G(jdx_Na75X#kNyH;lrG2*QdPc3Wii4<&;q_R}T zMGnC|4Wc34hZ3rjb0q}B)ZANkx=Sr6x7N^-ElO>&@I(hNce?=)bb-Ku6E__Y5ETdq*A~=|B9-hf9Y$4}IUE zGY9|a!PSG^2Y&3p=>A{ZAM8K0???7c?fo}%ZyxpzE7<{o<~~t^=Jv+4*kAH#(Aa8HCQia;$M=fsKc?!&o!kHLGUU@_Y0R)8@%o z5?5jpFe-S%8&&%krKJSOJVr#iDWDQ=BW!#moDAqD9Q<$h_MAY|cU|5vF~M$6vYJHF zoH~%wbDEyUnWL$-1OjqV%(zkb<^y#zI~XS+Cdjhf8x5g>bb{#$L*ZzJe|)F=M56J& zOyy9uMES*DlmBltnl#J@G1YV#qJX;jh9tluk!qg4Owjk zmB6BfkFsv>(TO)eRSKuh9IxVnM{GA+JlPPVvm8T%=T41;_xio2ufTOUHUO`?z>@*u znly+o?GQq{8bNYVF3&CW-@Dp<;)@NhFhe#PzbCXx@`nLC1VvkqnAAozqiJX$e8&Hw zsnn*&uFu?@rn6ZSwWw|5w2&Z0?HVtK5};?168!w_?h`i~p6v#{!ivWjd)(V_x~A)z zyFkS?9CY8fZ>WpfI5YRw|NDF0C*EtgGO+nnn!*g%4LkBF>A92HkOX7~=mShQ8CQ4a zTtB(-Xc@loZug1X4OhR;kq!nd$l1}3V`)Sm%jse&pFQVYF5_l^vuj}H7#s*OSnkPx z^;Y+Zw;HdF4pG&?a>&fPqLhP(Vo>NX3}=ki4@8t6+dwdP-T&h|up}=uUYO(rHo`d~ z=rs9>Kv8ckDM|kfu3G{>IAt@|-|#L`*2J?< zpU5}dZc;b}>aec)UhoUeZ4*pG2fc#=$}o^J3aIOb!vkY!Rqj!zWPgkfnf`EyX8mZmGv_GKor)Cge{QO1u{)d(Rv0n=xBr?f zeh6=I^P?Mw=M0}428J08&-sII>ppR(@##SX=ed;Yb)A)g$M+o=?vJ=^=G2WfztCi+ zEsu`W2SXYB;cWMb`G(6+NP*ovZ8n=GC!>4`G%G;N9F|71f`_br#Wzf=28Qa=+C%4t zF-XxH{IjX<6Jw3Hpx#P8IYe-|k+Z_5Cd&63uvO?yKl6l6htK?5O>N~w)XA?iMD4)= z{mSzTX58v$5e_nv@$5Oi<2Kx{H`VzGNB)5g!rYo(5CGgQXOx}fA(fMq!dP{XB2i*l z9)zrtJ3i`gVfgcdh%-8>{^dK}C#D+~<1)Q(*)jk(W~a(QP#+wCeNB`LU5YmvO{uvc zLy;%&zZNhY-A%7-FuEuk_GW@U9+&)LLzzS1W?@w!$dWZ?J(qB|teYH6-l`wxpg&x? zUtZ`wQEa>;eSI?BSb}+(s99Q0Y$~opM;NPuc5>t^K?&5jXzbuRf&Yso-mSCoPNBC| zAUnidaFz8rDS=$$Icc8_^qxj7saR)77OVW10!N&p{ly)i?15S(#+6jhgm3mIH@Z(; z+Vp;|CxV%Zy#uF)LUUVeSS(G)qpLsK1;5nXuwTYW!KOkpg=qtbV=#TWi)A4i6=IO? zl)r*12>oV23Fm0=s}dg&x5oG3v;vQIDmuacIMaRNYYp#ni5cf*RtsTaJVwr%XsRt> zDcKK9u|^Scr0J*7an`lbV&N;uV+a@T_b+#!7;3y3lfy!IkG#4$yw3)eX2}L|iz2i7 z8%@pXCGzK?e(6F5Y6Q|)V*voCM`t#f-a^B*wGZVrh7b76ox=&nbxBG*^A$7>a~ozW-m*Jn|pb^ z>T#M)9i_mM)-b|+AdSDk|Jc-gkJ2NkUMA{ciwaS}PJ*H#IwKRJ%A0OO!c{;h zGf#zlrtwvXQ1NM_ef$p<|G&KJy^fy0)br|*zk1}IBfAg(mBYh_e(6x^;D3Mc-2>k^ z@aGPk*#9&8Z|(cX`@VnQz~290&!6u3p*^>||5^8Ucc0w-i@V?L`qQrO?mE5e7k0hZ z`FouocJAr;^NRbsdODJ|5ttk!#O?5UY(0sVVT8Z*XE5{i&un`3M>~i7qn=lpV-)J) znjSo9{i(q?;e6KhC4YXyDDnxj&yfMa6QRk<)h0QULad?-;G>X&K-TJ2jfZxBjPKLG z*?p{T&h4Y7PMsaB54t#=&3gO&r~3O}T4K4Lt1Z_>5}sL&H6}A)r)PmPZ~Sf2>dJt5 zs;y|NyNT*^0$Ioa*dSyV3pRO;=(w;Oo{UfBZkKPO@f?p&RR4h2`9$q?#&h>Ub0~w4 zM?)D7o=OfRgL|e5d9Zf8fR0u26F61nB}*I|k^~HZLz9}_$9BERNMPkRJtE|~1>F7Z z-N%Y`H_R{`wcw2d4#1s`;&N9)u=Nj~s{Ut;30ra(4ZOlh%RYBjkeKA`BYt!s$XdZ!yJ+Aj*Us zqW>^p9D$}c<0wxX3{(6-`j+^P-W1Byny#cwIg&L3VQ}xVx$KUQgcBgjzp|{RaD!kc zP3x=8>p#pOpD2M{P_<`)90dNfDjlRWsWPI?8D1_&TG-B1rL06iY3~XFb?GDt>c+!} zu(B>S{|oPTpIB}Pm2}@MxdBsNbLZ7Lu7g_x&$+CZ$WjskLl^xJ=Tf5=Pa<-#R%-f3 zmCureY0RhVW&|B6<2mD1REixTBU zsnlVi`~-^yHw}xH)|DGh0^)ghD!cIRchcP_%FPzyvWC*xfI3oQ6Tw0HO*SH3Q1&Ct9 zX2Z`sMX4!Dj{n~uu-aYC-e`P|MUur6ip^Q2BKOiZQ9g(rGCI-A(DuTt)XXYTmZh+* zQNQ(+2_@qa#r1#xtKBEwZFWOkkVQE#_DagG&L&bKB;jxTj}&<^2+L-5c~6op#PrpC z#6O?zJ~7ek;;uS5jY{Yp2#(ov<)T1XZ!}YW5=}!Dc-3%Gyrm_Y zJE-_+g`9@h1;nc93?bYwMer-%$&&9BkuTxVa@5H#KBXeE_5bhc>Nv9J&?gS;-ut_I zKC}BJ{<-t#-<}nC@PhB!ey$riR<(!Y3F2qx?^MaPt)>C6!76#y9h@Zrsg^GQ1pre( zcI7zI9fq7x&2^AhqjJ|)sG_|}nW`gjs3``f%%iNl`oh#)l;q^<#ny@{z_{q;ngoRc z=N1Oo1^4D^wSok>tS$!C4^sr7#;ciwDsyl(Qy{G9V1wrw_s%TX>_6@I56)2D;gX-sl7N@TA;q_6GdBC4Vn;aoFE>x?vaVa z@V(@v8}F~(>y>W2RJm1|sD~3ysc}R8X>oxRtvf{{Ds@&`UX^Pxpav82_gaI1OR%_B z;*8SI@))@_m@7&dDrg`(b3oE$`-d)|9(l^+bOT1$vAlRlFNsQJl^1s9MCk{WGSKfC z(Hn|!0LPSjv;crbezsqIk2*?tdkNwT>nFeo-d=sds4J_30^HW#Di))DCFeE-=`G{N6_v2!>@s|huvBSl{bcR} zel?v~{D6vRvuJ`EKG6wkFcExeF#I`n)!#*5t^7X zfiS?{{4ynK;v*S$M?sQQLAe*hPV+G2@&pn3(N;EH(vDs)n($E3R+%MZ%q4=qK%kX?L1cTw^u=~M1xFVBPY!DBMryyKK3mZ^(?4Y#a zyU7B$7vrqAx8k$vTZv&bEz&*Ss<~psGt%41NZc9asm<~$c~(=0Dv?C`<~y@@&y*I{ zmcD@DFRs2;Ia+_@kGG#rpTYlPT|+iwfaKDhbx9t>%3&3hZHRt6 zl9V$MA`nnNIbwGWdn7J#8t@@GG4yLtBc{@fm?%PD!f8T}iOStAG$F88^8;5YK;VYp zEBY2%1F1Ju$tLoS6$rMIynQDy*dM|Qh(r#4v);|SvM@NhNbBv|@~epa&g!^{ycz*K zat`k8DO>n4M}>${wz|}42J~*qo1i|e88NtU#UzX`ob<{!fXmF8wN^Tzni~`$l)4ZiooONVA-1?$&Uw6Q6G-hkVR}(w zZ_*wf%Q0>zvELbsX6andjZV50X_L)O$q}p$_2vw6)>U9FEd_VVdO0bG$baDafw~HD!vxy;6+5L5;+F z^Q1S8yQAVOSk4qm&M8P6JdLXa9oJ_Q*+j2*p+M2%B4lVJS7$K2i+RW8?q5q_{oFx_ z?uBO1@wvuAL9i%yF=Z!dOzfL?nV&v!aIN%7;rjYd_o-mxzmEfJ&jPoR8e_s?Mz}}r z*%@odv-dSi+12H%H|VfHV<%6;ba86H1*w(yD_0J5ocLn7@?zsk&k%Ms0mB=i75Urw z1&fwKL~kf1vVKKxXk?h>l-&>lM<%?@6;SZMScK1~VC>%!48PAp^38GhTfOaSU**k% z9VgykOd3S+8x?C)xV|{%7rcogbT%aD<^@ZW2P+rH*iO&|87u2f)leG&1YN<$AxhO} zqQ)hun%L7wY>0J9Q7!3XltytELr~JNVF4$%*2^{*jDE{fSwsj5U!zt75h^xWvRJRd zmP>ZMGQpB1Dn*ghZCtWPPgxR?RE;;i5cAYZIM>6h4m*uZPu_Az5dw-8E-$I;un1e3 ze$uTdp&|4Xxp~so&{NRAVuQY{%whE$=1Mv&6%8i!37D;S-SWbUPx=+IDE07I1dH3~ zdU$;6;i2(ZGv=BvX|0E6t0jGKbWo0YA6gjTimAER0=CJTEA4u7NLr}h8Am#kFGqdX z`gbBTPg_c9fv!w7)w34Xagzf)Mr)WlpE9K3z!f?chM<%f$5j#!LoIoS2PYy(rT>AK zr;0w<3=U@^XSS2-k$r z#5r0E4HPTbM$@`kavC#`FJ8)0vyOpO{*~@H|vee z)4of9Bsyd%0+~0Jc$Os?DOt#JWjv1Rz!n5aDNp(W5F*z#?>nSfX3`@>EllE7Gp2!W z6QcCc#e@pT!m8aXjdIDGXd`Q*2Bdh^(No=&(8eKhbcxUqwPX4x1QPr}3uSS{O?2N7 zk=ZM2YAkd|(JP}d^n&IyMDzB(Rkm1rS!u_J6`yE*sY1@Tv388sUhI~n)kI5%E>8^2 z-uvLq2Osa3mb|cfv~siFdf2H<4v-D)My-Vbb(pXFXh64r_;@P9L(26K+j8$;&h#HWTj*_|IJNg@6k*qGS7>nU{j^L{_U>B8Uc_NVMEed$BJ?nhX*+4EX0;Vcu8};=Vf>pAoJ&ObzffFYZf3YSD zwO0vyPga0~10aUb9=ISVV>Fh#N;9M5A`3xn8p-RZsy8=e@qk~n&;uOb5J~HZVWF+@ z?*`=4O7rmA6krDgq^)caz%+HK`djK9s~DjJ!SppFXokXZ?eg~zvAd_F>P|>!R zl}HAvMe?glgyf*uB5pqgSZz7?!+p35Q7;Rn0`;=^^}Q^84((LuEYNvZ*UYj(_6=e5 zYQFN0QWlh8D&@v7NrT}y^`5Yqc$8u)eiH`P0?d$#T~%_iWe`E}UalF46p6X$a4R^% zOx@j5uv&WuOjMMF-|u|HHdj+K836vzW#<*Ag40wrDq@JbY5)JSya>xCESeFr-aZEY=mh<%)Tg zw^_04m9wG{+_1X`0D|v~)g^vBkrUMPkO5ox5lnwa zXZ5q$Hdm_z2F}ear~_Ec`NM&;Ep70ouTkuZnt zq8rsfhQ{PM8L@@rGD6mU(idql6exnX#t4o|E3s;?4%z&uaDR#})pFxS&QZ-#{{iuF z&9m$qkeGuN}R(uXT+)?Xv zg`HAN9zKo<8AXmGk)4H&u3AikkTzMxSf}fjtN2P~npM128PJ^gjr#mZ-KjTl$d|15 zp0Ev=k;`$j{cJ8XQ$k1qx%5Mo_Fuj`dp=iWS1LsT+*dSl9Pr8ox4m-wzK)o%ht|3G z8_t(vUjx?EK<;2W9M*Kph1yfOK_Wc3Epo+;>(ovYC=Kw`u{0pidPbdZQ@4`=nloOo zO|$2&=)7BznfnUCjrRn-(Ln12;~`lhb)hy{iMCAO>eb3ysLXAcL{Bs>#iNnAu+MJm zprC?FS(t>6%TpPxuec(XNge&5#TF?ISOYbJ!pN9cIp2r_$^{B47QB))2eux}X>9lp z1oy}wit7b%nVkBfDC1-_b@W(1vdzWKGZzfb0;P~rHZ8!IX*7P*ENkO>3dL*%5jxpH>wsdvAU;3BooP1ZveFp_MQ+H#t|*cgrlW||K4L*Eew1jWXe zjq`VG^|HX-CfQaKL|jcumu#!^hCyuF=b&O#BE;lF-p_Ta_+e{tXU?0s*~;O<}V`ng^AI={8!RSW;gAJN)!ub{^uEFc+4cit)8j`EX;D{#mbe&g+aO6 z@Ak6%JU$)Bcww1r_%Y^{_@U&P?lJSOjN1_P7`E+&xkaR$ zkXsb1yqG%Jwh>c@t!$WQjOtTO7BoiHvE_mut|VE|vC37QM2^O(0O$e>7u;J&&r%a=r<6Y2mv+ykP*AqfhPa1R zV@(z$#x=I(g3MHAS&;WuyDG0YF38j)5*gAls}U7!WMj1!a!1{R#=+t?TZlmfl5jUo z@a^SODY;z9Bu-+eUq$%~R4i1UmH!D(5%FU9%2fmu98JnzYjGJ-Gb$zvxU|sJpQiMvvM(1Ia_k1Lhof@>t&3X~8BZED|+Sj<; zOv#7^s3b2#rv)^Fzo_tt7nEIUcUO`UVmpfVhpJ$e_D7jU{8v1Bn9A9#$c*FFZqQ_@ zVoX3=E|p*Tc9!aP<#i2I+PGAY#+K#mn#R*xJ=6-Db~s44sJYB0bYjYny`hks#!}7G zQvW^CpG#1ltGO$teg>LsQ@fa_EP#ppR4gllGw7SpNuUgmqxP~>p^+0L#_hG`g6*r^ z!7sh}^~!MLdfj*gR@3opb_I3MnPpc+3g#gi%V3&K!6-~+0seGFkrq9&pgC9cA8B(n zs3nr3QKEt+k!t}kg&9$5B2iBXeZ?dgo2^A1!|GORvG#?^w!%=8&*l^0a#dgS zmcD@zWfKKI-E3_lddn@>X1wxM)@Eq+ENnoHMzE24kZ_ENlatbp6^`0)S|Fo5Qh0C5 zT3AB?fjoyTb0UC_5=vYXqP*k{o%Oz;oZ=i*|ERQbJ)FmcX0MSww~L|3X{#(*k~W<5 zM#@OFr(Pp%m1@cjlx?$Lkv|wODu+w6DKdAsZ(i@^4qn+yaVj=0WSmrS%Z0pMc~=IZ zt1>A-&&J~f7v*v32PSgfc^nqT+@Xf*s0yv67CVf}lw)I`y5u*@s)u#)Td-&aTKU@m zMT`{Fm5(p3Wco}w^FKn1ugwzWpA@2!6pQ@QwwRh6H5v23OO_&PR{TbZLO*# zs`hwVDo4Id7)vpbS88)nRLQ{_Crgw8d;qLKnM7mgAf_9sBWfhNTfMjO|8@RRN7qkv zU4Xy;t3Cf=&l^X6!^hbbeRIzv}ppI*Oftvgf~!AOoEv9rG`~vg?{A z`344ErKkcu;7`QADb8>9^k1r2=$-H}}VW|2*)Ym%VZMisoIrrb9=P zg8(uPRN}CyT;m?8J&$TaVP#ndz|EZOni0&J(8q9`hwcaTDahn4d~9xQ4P+`S`;wtL?IAE!OVH%>c)E8UR*|R zUXSZd<8GN$@2>%O&`;cK=^$Nry1g_A#m(AgCAq6UEg|=Ei%gLcMq-81w|>1CQ=?;* zecbrYRyV$!PLVxSnO5&EA0V}YXD*m4$JLx{u3G|?L9jIjGAc1!Kua{4p9#KHEYE&< z@JoIA5AX~Kpg4A7PqP0*9%OQ>2N}COF*86LF&kgyOr8K#6C!aOj=Uj>4q~&ippl$t zGJ7nHj!)__oOT*)ONt+WHVK6K`|tXvE8Kr{tNV{~UgqGA=C$U^#XJ==x~J!f?R79kM5pP(FWU`TyqPP~;m&al}27};{KH?Zj#*KeNfKDJz=7@Ogl z+!XR?q_`UkuCqrkW0+UNV;Sw0G7v$N$rhyPtj1?};LpTa%w|*s*BAq>oNLc=uG7VG zhFlw-k>|=R4ekr%Yjw1HpZHD$;MYC6$Tn1eSOMV=h@Pc(%o#=ReI{+%p2nTe&4Yzg z7c0sT;`~qAoGzuHAhefMiiEFkH0=F2*!6QoZvTAk_5wmZaif9Vb931JK!JN6t-t4! z53HTc15)37%Ihl|C<3QRAP^d4^GV^iWLEHEWYFf>(Cl0EgmSQ48xhB8gF7k|8)n-wdz1cQ_d08~`rd#Wr$kym|4I&*oPe>wz({ zioIX$KDJnQFH;z*2f5h{CftG?`K9DVMe8AmufeidRja2@?g@EEY)hFZfhhRg1ZRy1lCsw1b#X2Io zH^{{<`^4=gjm9E&)BbPmJ~mWaAHir{+L#UH=HuAbXL!~oO+KHB9}UWi5&Cjq0Zn&} zG35KJYqlC(2B zUIKugsZrP$6ftmBS3<6$)W89Qo7n}G=0ghL10V9feYN*}>RpSb-YvTN;v$_?a`4eu z=*##&U@;fUvyHD9Yy3YFKkX+Uxr?Td?)5hBE$Q39WNwjD1n_ZHBqCk7uRDCz?SeV}SBaXG=|0X;JVgK|hcb!b<~L6IP4KnJY#iMi7QU zv!rn0tQmoU&AIwuB8W8fS zy{udeem-k6vq;C~d^a+SfG3kquDBd$Z1_oRX4Y2esMVFq5=;9vFgs0P0hj1fc#Nqy zu7sANl}ugCiUuRHdF z9HsHfESla;OUm3j7X*quq|N<2N9Z6ZnTlo_aYQ4|vR|EWBn*Kcc4KbD6Et5`^eklS z-EvVUDp~Ywh4zvrb&OjazUG9qH5>FIwMF*cRGADVQy;1|;3@Ss(laUmnPxX74QD{# zyK6VZmCLA`O!Fq9oFw*=LIvJdezDCZ6?wKQM0C@O*N(S zkg28)z*irW#;2NYxtL>>42wAiLazz#r^OJZi%LtlMl-aH#yK)_w(bs-?ttj9*w8$f zqC8Y0u?7t02hR?8AxKEsV6YpUQ<%mqJ=}J_$T@a2P@FRSL#+^6PFnCW2}SSnC^G}N zp<(^nTwkzmZX}fLa&-u5wGBepWEmqz=aI12YrU04ZS?0>uT=7l8{Jjgu~m;fF8Cq_ zl2MjeKelv~5w){C?9#&u;_6egL@ns1Co`meN31RTTwDdR@BqhFq(QO<+P0Qf<@8_F z@Dvf^!F!c1QjnYD)YlS=PKE@W@<8N&gni_9Ws4I`UkR-WJgM2X4X$<5L+de1Z4+Fk zpPsYwvFozOhtb1oe6k95hPVk&PueqVe6p)s4b%lo8f#k{pS-2ozgBr4wTE+t5Tl_M zY850BHP%b2z-{B$V#Pnsk!F45Nkxn(tPKr4f+VPUNvu7E@{;~b^;4@&Nt83txK>c^ z8VAb6`DvCK32GCxsn(RHauM4J#XZP)HDo!0+Aw6faj^)qgi{SiS8lmjU#g^8tR#!o zqyzF|`jN-OO)Z?fa8d{ah6*5Dn2r*OBnd@Q&Zu>`l=uTi@}ZSL-9rKfBN(wYE$1@K zQb4iCZcI#1-Ka6mw7G1qsZE~uulxs_>2&}fT#Wn=y7#@cPQ zerpFR3o;J3EBjZQ0Oon&k6_Hgd4ok<1;GMe3g*}-5EtM+qW%_66G0=Da90zIrWxU* z-DT#m+|_pXcbe5qahMq#q>2$y&y1f27?J`wGsotqM@^k*gd3peXhh2cBUuK0BMD$# z42EM6u{6@KkrCAK!%nq$j?Lv0AIVg&PE_X6<_{}=<=w`uf9-KcB417528^x%g0S!z z*OiVTd0p?TGAg8<`M&b-m#B?m3tXDL2A@ zpW9X9+uQ8V+WCishZNz~(jUAsdAJT&FjeL}BN~g>?r({qi}2W(Cak{YTX>c=N-z@n zNdqR(o{iIIIXx#+i(ATB&{cAAsHx|^wDe_nHz-cRTX8byI75STo-1bsR@wQY_a%6; z3c=j#pN}+Pd3pIH88hkxCsCt5e0L+9=qsKY7(OFf_|yJC|LL?a5M zht4$l{7Zd_FMp|T>C4e0Jgc{D|H`69zrX;M1@g*l&Qrwf7_oL374Uq3T^>^^29!W~(2)HV(m~4*GUIGAy9@ zooLU>FXX&iEHEKOuABu{&Z;D`3iiULWib_4o+nC16oR|ZcpJ(>l}R$vBE)FBr&J(d z`3~|Yh-iEpQm%iwqJ#C$gy}QoE}AT4Y(n{#3;8XTdo1L;bO$zRV#c+*@CYN8+)lRK zqS6N8e0YO|gOtyz+D@96M3>_NwakVA(%gDS=p1=lXdgL7IP6HXiQqcg#Kh2#f}JG8 z84iRJPLOF($~b_Yw5H`^(70O86Mp2>&39Io7`+VN*u~sJEi@1QjqY)aE)z8W_DPel54eT%quaeFvCt???0P`7q+aiei!aF@>=K z_gm`gROPND@B%pC13+ur=CZQi!)GRR zFus2QrvcsRnK^^>%*_0;XLcb3Y!ubfI7>Io$h$|)k)N%USiO8@q*-PO^1#Y>WCo4r;yy?nh<8Gc*Mv$Cg`abuz z(jDzHB-ZeY(3uPzUwj+4gJ$7?lQ&tMfY?hrO0$oyx{sr45X*wYl#z1FiB@9>30uh; zil~}T&}mV*>Y@%n;?Rq;%34Ncf_9e@lURENcI%uK4c!~C<@E+y)ao0!p1T-^kMrD6 z1&@{;I8iCEpm)d)G@+rs3iDZ>lSBnoYOR_*M3!%0xZnDQ?Et3WiOh+BuAsQVMD=CW0@q6_+;s>s6t77=@sj@B`}OjaY^$$ zAmEk2QFM34fwmyj;)UrtM~be1XJfIXGsecM8GP23X;?CT!hzY0G85!+p3HHLg#D2u zXOG;LKJEQEPGstG>WYm531ym>6oiKrC`EIWq-b~p;tdke@I2Wxh59-U z{YMj@9=a{@r5@gu_zphEZxEmKHS`d8<;a4Cb>EP}3mc_53xo_|!V`Z5OufRUME3_w3$gS&g2){Gq59ixwho|$%lkLG=;5w zaf6%*eXu3Q+ed(hCPPPK;fkut#vV!60mQM+dyH%h!JC;Xy(fJz7@?2A+>VU2mbgBT zB>vQ1EAr|dZU5;J+LFtmhXhu~E6=YDZIFw>&LK;u?j&f7DH(NEya&>ue?f(998Z}P zGfOBUFhJ8``QP(phbVgXyKv;RKiFy)>hnk@Pvw=`ln1wOOCFaW77uhBUwm+TgE(%$ zGN=@({7{;oSbLlfnG!Xf84vSi!q^4S$16~PJ(F5e%*7fS8u3S3EWCXlExc^`Y+8dI zX@+;HjU`;ua4c2mAL8qoX)MptNjFcl-Rs_u$}`84VxegBhgo*x`VI4-n7=GJZ$K3` z#8Y&S5F%+{B*X?R7^cPfxaJ@Wa!=m3J)385)Q&7y?>{6=JYMNudwGK}?NJDLF?CF^ zB|e79hqiXfR$EADk}&^36<7chz#49}NNMFG*ivWw{uYuQ8R7FtvQP3RZSExQ+Ro~) z?Rf}-`grBJHGjkEU&`b$YD!h;48;fHXe1j3+nr&rRQ3k>s8(dFR8-wADidjT9a8!T zWH7w3QXAWSBO`ns$>7P~r%g$4?{*}i_Ya~Rn zVOhx5`aR$=*1c^cLR^2(47Lt_^m(M;v2F*;2?R^v4z2InwKA!mr#FMPz~)Wq<-lc8 zSQL@IN(MS~>>W>fQ<*Y{gtgnM+X|Q%GhamZU@cP65rm){G4{P$q;DHjpp2)BiYcja zbz^JGGiH=DIwuN#xRDEi+=L&3^|k|tu#3|tF;A0a+i{fMznwj{*0r;0V7PpTxnK9RY#{o%DRSY$4ls~KlI)7OU%8sRzy5fVI-#fFzIJfPSktIg;7{; zH*=u}ZA)b14?o1!%RYE(gT%z;h&7btnlWMGY`=e>fS|ZJUPh#d1$=dL~M~af|9omubgI6DZT@pU}aB-uA1=ECr z!#a>`FemlZ$W4r3KXg$RO0X(ocQq@Kq@1mV%&fM-vyf>F_s57i|8#u+Hhvz-bQBp9 z9nq{q*~8J-x=A1oSj|!9)xD1L93J-nIKY@aqn$3%giE)3$g#S)D0&Smzq1mNy>-RG=wC{@*g+-AJe5S9 zZ2-8h)uIK>C7jKoS&~IoJQLj0u`E7aO;|(Um#sYr!yp*emU}TCH0TZ6l6%*~`*>I% zHsqYo!!(Pr2%U3F0)&ACNCoGr;D@M;&{kmdG^9Lus&(>3pGQ)Di|YTPXI*ZZ8c|JD z7}g0pjW2H{p1_#4WewCNY`tG|uKio5NpI){+Z}@kW4D-OtQ^rE zvmMbD&6P{hyh2Z$(S~9%wgdTJnuU2e_SBa^&8l`}Nl;BZ75bUChgz3!C2&%(3i8$2 zq;$36!V(+PpUFu@q(#1r^lUZ|)lNB%L8Zn|Q7CPICL_TVSIf~P_ z^)=B$M-j)f(UBV%$VyHMU?0SQ%>_elD=nZ#cq=?)@w18eNk@KM4^O_!!*|CX9w;Tw z5gkN`s8O_BNLo)~gu*Uz4()O%<=Dc!mf+gY#jZWZvB*(EI$aQ`6xryx3bexoRkO6I3~qvLh?yV5oRouG{U6*dUk$0*ldW9@e#VeB?XzLVQri9I^xr69IT-u3x(<}NG}d8Qyx zVS^qGU_6d5LkAfX!z~4vC!We$b%jW}cBqr^;rxzD&n25OM9hqol5zd|%(rp-xSczn zU&xB%JPRWnY7l0yP8PV_CNsVYSTaV-& zUv_lt8rvJY@RZog?SP>sr^i$&zD#wL6*02Bs&^%SN)kd>hvGNbz~y}->yB4X7fpESM}H4^@n&O|6V1CE!ONkK{o;8MC6K6(vkn7b*C9Q}0cRs}%#f!G1!f?o<44fP z8ZGQo!y?4q`kytf=w<6?Tu7*=3Rp(k4kOwn93QKh4J%@E)B#J%>Zh=Zu8F?>+FhT& z)_pu1Tb6c{qV<>C^=~eAAAhgma_=YZ-xmc3kSPzoS|hillA!~d6Wx75&-hKSt zhRX<J&gaX=Eji#?5?9!*t(ssLemA$FCX29aOFPsc?MKu|#XsL({_f{3I50Lwg6 zFnBf_1~wky&z{eoAH0ucI39a-xsiHFYWiW1%p4=rA!;z|Z+RMC zM@_G;Up!$Gd)<(c&?R9&ipcfj`uo|hbE}EitsGu=DLqeUCTysVG5C&hffxxD!5Sc) z6Ud@;PIzo9647bPT471%k*=No>KO9!g?SD$wjzBHP%hNJXxGts?tZMv-E~hDjJ?F; zqJ``4v+G|}I=IpG_>F><9NB6Rrr;PH1CoG!WWL~8!xJK2rTrtT-f^#rCQr-@H1&NX zjTlbA>uUTD-WLE}lJE*(*<>gUo>(&erh6N`nWvP@iM{B*YSbK8YsAA6(}{|aOT4Y# zLAX--o$-gEBau^P$ZH*x&=ym8F9{iEkhx@dDAeDo>r;(ybO8(Of&s*5Go*^T2Iyzq z6HK!|Mw4iNEnC+ULFnL$Zv*=ZRWFIvA$I=y9l} zm)KC;FLsX?t^VJ!*m30Sp}8{{iqNz`~SqN~nd;TuQyG9R;(HugaWKT{Z3$dzI*=mCH5fNA@94?GeX<#J2z= zj$<&2?96`9palJ_!;xUMW8-QbspdAKXKzd9!TMgw{Cb1TFFxyYvgH{@ z9q}RnH`phkKXFbp?Gq$;ln>EJ6kKv8rYvX$PhOq(^eBRM&)$}x%j&m;Mc-?L!Qyp!d?SlJSw{gak~t@d=wT)99P0a`PF3Zy5! z8CPVtT7`{O2z$N|WQZzN3L*&&jp9#b9%)>U3Xra^Sil%@j)`2=eCw#>({5dWmugG! zpI`5m;Pk@FnId382-b$2VvilJW+A-C$higYl509?zmXx0Rcdl&Mp3^m0gM$GG z@xkE<#-U)3DcW)C;tMBrgvI|9c}KW1`LSm%s#Bm@TA|eK^O~p{kPjhYy1Hd%nXd~X ziFRf@1UcFql#*E-V_?XHvUw5&?Amh5v67U{s6WDfNe6Fra1IOQ&sq|W}cU|+K6)@~K^_ULDp*U6lvWpu+qdoIsqG(4k*S>ANi|TIq z!PhqE<&8W$_xnUV8At1`*!^ zL6>%u6Qj;CsPlMTCF4ZHVy9rnviw%I3$b^aT;oBs!BJ;F^2~kueqpcf#j#iQ_Sk(M z$$bJUfV3t5EJu~T?;g^%CcwIH=Qq4>v;wLnt5+5SXU&0+apD+|N_39-H9rnLXDK4t zu^r@R&{R{fX{+bcvy4?7Ex<SZv}X~bVj}Y4+w2Gy!rlcpGW(H*}?c1Y;=)c0O9aP z=P-W>!zGDu4a3AsVm9enbY@LM!yJw&oGsw6r%%vDz=QzuXFBw$^4G^F-5p@&hAc~SyniCan+Qm)lJ=zk|(e)mL^lpQYRLy&Yy4f=q z%;__&jgLN$1Tz}Zq6j)d!@0>P$lYIIfSYsd1u3JtOYX#x8o5Kf7!EuFDuEmkDrG?m z{~Xfl=%uj3uiHd4zAq3eiI_}#NAkdFJ!e~j@z#&XvwCsu*2e9#*__<=5G~JIaw9{h zTUsXjJd&L2{8bP{0bR5iWt%%|kesj-nPCP7<#~YC6swo$5Hsm&!T-Gx2w`I+ z3^xMNezj@g?1|cv(I?jr%fdDA5I;t__*skN^hoQ2!ak40aXGk0)so#*t*?by4y8)0 z%*}Got0tZ-AP{Xe#mpS*<=h-|FDQnJdo0L-bIrSnL$Z)`gE^R9Tkb5zmq%|)F0ZX0 z(%$J=D{t64(;CU6a1*5>eIqnLhI!LWWl&g8t9~v)s7V8%AXF-N%9G*MAR;6Mi)-`D z#dP{?>&%rtkHj=AEE2`F0W8F+#iIF*pmyhD;FL1HO$1#nc83pk>nBjc3O)Cd=V zEi0FxXiEtbH}w(fP$M^tNNd{^mL9MzVGXYzl(0tE-rXRqCZ(}apchYJtnD+9Vywb< z`t>u9fcf=*A&s#|F zX}@oKqSV8+BTB3PcYe@u_>T|0ci@Nj{p6ls@BZelalWzh=h>{l+SK}Iw0EvFG=o25 zAaUz`Q=9JF2KFJAEEpLY^6$4u7TD+2eZ$#{2E8_bj-W8lxTJZK`LIHS-NYxu#(yhnTh?EV@-#%z ztR_&L66vlC$}fh*05-mA5h$VNxB;e6KHYAoWD*d^_5^HC-jK&@DA!{ex9Bm*KLYmrNeY$Kvx zH?hu=EeT!h(80`AsqP0n9sOe<$-bFWRMjbE)4X{SwJgNIj$|;_x zvXR=Aqrn_D*H92w7_o9~ZpslodYc0NlmvYI!NdjuKSdDhvli%Sm`Pfu*!6iNP_&Po zmq=*?!u2P-w4%7E$rP#5@FQ+|rQpk|F#|wGYbCb}bU7udfK!nqMIvg;WOVM1p%?j+ z-UQS*+6)w!p}TNp<&dHXiBdlnOSm?&&L^cj+Z3?9YFh$+ef_8eJh7JCAmC>UI)BzO zAL-wM#7&<^GQTcBy!tw!A;8KiKS2tTRDK?)kv1~Wdk#!2w9OlJ$qdO{2@bPaF3F+O zQhLA)4Pg?e)u`8|0PVTk65!GGPm(2j??LAV`6vVn`7_QmPB8I!nQjWGdvr54miMSg?foH~Io|j12Z+P7I_+N$LWw zC11Na(WYSRG20Q$gFWk?(9U}P;j0@Yvk_tbSTOcyEw&L->Me;y^?4+=No(fC+Qpi- z*dbuK96J>|h|HjQFRqf~F#ASa273u6FGmORJR2q=DAPuAH0xl8Xu2eo=z7RFKNGf1 zwbyWZZ3M0`ECeAT`Ud?-Y7{Q}(XyuHq8KjVXn?zc+62kSa#lurJXubp@`X0NB)xe% zn&9I9AMH5&hll(F_xAnb-mBd|*Y)Q*fArY|zdKib;uTnXd;Rm;y4?*dv`^CD*Luf} zR7b4N=S0{SHY8Hmhu&B~BGFap2najD?e=1MiY%`IPAHft#odv5R^REhVuE#&o7Ua~Er*%P%Tw9)m?N@&L) zZ1hkrJw3vK)^d9CI@R#m*1@BF9?5A&7C;Rc9P}=BpDkw=HJ51vnF7`?3iF8kK#LJD z0){apiCX8?jpCxl0PIW_2Ug$>%jy{3RT(zY1-KC_ZMF0b*yFY%#0Lk~Uz8A!J{;I! zBsa$X5^B5M<I z=oZ3iIP{O)x~O}BgbcV%e%Qe{qa?t64f~s;62Jgj&GR%%q(T9{B@Me2rbyGF7gmw1 z%b*LoqwgD~m`+sh=^dH&`t4|g;{Uy!>m7%G_TV4y|JA*Jx%+#%zMp^Y{P{>%;KAYb z*R&_QA8rJcKM8$Y>%I6CJd}ac8*3M_6LDCdM|%-tLPg$RCY&Z_`hVLyx7fI@>kRXv z5k*pz;u=QkB(6tQ6vwv4njvRMj$=m|Q?f2LC0V4T_!1{WaU_mKawuM8)2WkEB2QWb zekjlu1qu}HgVCaWGW=4seM#E{=miG)kf3jE^4g{d+MpK%Nc#P2pF_@>GegcHXA~f? z9}-E2Jp1gu*Is*F{`D`>=He`IWX$=qCPr2i$FV!T0uUOef7~_Ez$n#ksbQ5!=pT)_ zRMk}QAksHYp>NTgiHOXSQKUxj9EAeKl#`JMlQVTSfD--6U0x(ryh*yJzfi12x3X0- zepq{;5}>}Zp?LeWtLs)?43+H<6*Cr~m56gJ?=5+sl|VeOQQIa7Yhm#Y8qpP z#X`>mvA9I@25qtE6iH?Yc^xpzQ}7({7fuw$M^!VZ5XY7m&-SWU1ZDNQdlv|dSbmBf zv*v2zCUn7fQ|55kQ9Z{{FfEAROF;(rnCi~W%Du}M3S!4Yc+9g`#zmqwPml4Y>Pn(B zjkE3R4|tDh+aHZI4`~?@W7~H>H(Xm!5ndBL4|X3u^QszG9|ly1JrX=IND@m=>#+KK z<@l6%0}8oUGf{H7vp@Lt-xO)3NH`qRpYX>siq;VJEB?971J; zb;bp)q3n_t99k~NCLODuXi$k{6U9c+1&AdSPk5h^P#oQ;O@n^;{tIVPg7mTuM7PBH zOo&hg$Mbxly2Ki1>QeF)QcZ@TXm%+vQ7{1_yvGn(UUh=qcCi@EZtET8^gIxYNtPFm z3m^*SpKF>r{d1gu)#68|sRIr#6uWE`_&D_`X>lXctTS1Iku8=u_wKEV(~%1jQKz`3 z6kwjCWYtdm*7|NOVxVVXffxRhdULT1o{EqG93$_C`(aV35?B~AGgGx zAZWzRu@d-s`UqiEpaGU>Q%Bm421YJL;4c!OKBzaXE}sZHf25s>sSo}L{>@IK)q2gq z7{ZOF=(E*ztG^3*VNCl8}`^~Y5(}kGkh1MdDklefAecDvY`ja(2-~=m3 z%zU{*EJauHbzlapt6hPLsXM`-Z3txtaAK4f7Ai0yEGTNP)0Z484B6t26*6s?5V{=J z^uh~Eh@J-$B9*fFwVZLkXS;3$K3VhSiyO?=(gRKl{p7~iYj(6nG<$8|&1{5rDZHC{zPXz+{{4U~ z$~qv}*bEaoDp%V#N7WLLhr_cG<{V6ka}ZfyP!DyKbQQs%XybViH2v~2xKLFfD7S=O z%ya>nE?3@?GZWl9LWubChg$+HD6wx89fWYXuBZ7|c9?58$-Y}-zXqDo~O<&Is zIv&~gUT5!Hy+;oG^?@5b{{iR!6NmnB-+PB%OniIbr-_AuKO0yZIC}6;4yOA*?7zOh z(DOilSKn_Z`ucMF|G4LCz5mkvsY5^QxOe!SuHWnUZr7>KlieFJUFXif?YMlbD*$Bz z*C;~7UK;oLtT{exgB-bvdfU1nd_eRrXV97vRZqG56mNV3cE;LQPKeIZZVBZDd95oI zEl#OFH~NclY@?jFh`~p{>eLeCFf_2CeJg{)pc5KttovtO&x;SdcBrexSvWBrMTGVq z-9A&sr4#{}XtYrY7z3)4*Uf;DS%D->Z;&>*gTZKE$bTO- zoR(839P-QfYoSFbOe9k90oN|U6}1+lJr-10xZYa}NK%M)<^iHN=+v% zK6IQLHZ;)Zdw$TlF%liCjjC^wZ(D!accEzG?k@ zinBMx&pYEv-OZt8mrx2~Gd7&JI({J>xJ*HY7te9UxNhipWhNi9b-l4_==(F#Wy6Hz zKvJu%#S<`&=*jiPGLQ~cG&*b`B-QzCsw9Vk{mYN0oNYPN6PCZ zZW@Xj%~^*U;+^VGGbaCaW_8vk4<(T9n+p?HE`+do-A_gteu3vI2~@LgXl=D}kan@f zn-f1fsD0$ZIDO6J#@EF2;oCwJdV<^dV89Oki^%UEZHY0yL+hUsurj+cbp3Iea3?q^ zRam6vzz$zc9DOGw4&wwSg8MmDn#Uqk=^0nrVhR8dp7df-0U5=Rpj#uKf<4?M5BJ?L~wT?Rs@Ci2%;f{tN&Hn*%u#N3K=(oL~DDV(C+>U3zB!7GWQUk!cz zE}IUA-ug??C74(6rzf$#np^eFpGi3y%BDb(DmPQ9Kpv! zU)|c!MQAd|zQSmSL!<4PDj+n#!6?ak3!UuHP@j(W8rVAzWN3taQ4P|r635WE55LVW zVdGvXTInCHW0wM(r43bFp`kw>ZJ8Aeji)C)60>b{=5&SHG?6 zzrM~MjfY-$Np(h4>Mkb7C_8i1Txxu7s4Kgr9T`u`U0|X>+KRK2UAO3@fMZr!v31`D z4T?kK|L8TwZ(GNo<=edd7q8=w%m06+W8m-lzt{Ug&)+1z+5JxES7LhbAN_d46KlfEO1u~6bkmY@%)u%6Z8w8ySUzgKm#)krODZ6@_qBSWTV_e1cWazt6DNawZ)&ks5rQW0t zSsm$!4+veAVhg0?2~42*VNSH53O*eE;#7&>DFulg>Hg|=p%&Z4%PVrZEWeE?JP55RU)^F=nDr(D zD%JJ1k;Fzc5NyADuTMB{d0?T?Kv-A+J?4IEdU5u9{Za3@_WQ`j#hU$|tp5Bx#l+ga z=Yzww{4$|iB&X-Wo?kA(b+8DUf=t95lyzLwh6t=PTiF$ydBJ`a_~Tw6FynJ~+*n+p z5akBjY@UU(dX@pw8J;Pl9vH%c_yT?Ti4r133Y)6_%;gW{3mLS;X&uO1uk%*3H2 zMSD@?Y%fVFdi<)BkJ9>zoColtfPE^6I=hzyUMyy^e<#Jp{V=sSTfRQzJ)tds$g3q? zZbeAB?K_C8j)7F36^bFV^i)3+` z)C=Knh5KV_3awKdu`&O@)u3n!6*7?!F?ZIF2xu8l1!f;$a2iTi<+ zp<3ON>(`~^r_dVbAFBXun5ced8Rhl)n=_z;&rxSu9S-hCabB4Di>`AFeALPLeY*`h zt&k>11%pP-tjeNWizkG(ka2`?W1lxH$?E?`Eq~q~Q#tJxsce3DTd&8S2O@n3$ypTCH9dXtT-VdMwm=&chDt z@8?6+N80eDW)=DT(2JFon={te)uh>iJbpk&aLAMbFai>iHdDowAikJge8pmkc!0bw z2`Z;TK9+gNO+>Hll8sc%WA}M;KQIJd$Ym z&O7fjY(LN_D8)=NB~JM6D(w+48G%4tt2+b5lLK3&IC60BwwIMYIcVUV=1Xi8VY0Z;>k<5t^6V`O-Hz`dRaqER4uYr#oNg5riMSeM_x*+g(7L%7Qo*x_?8Qrv_z*1`ONQ9#@Yz5>a zJ0PtK8*8I=MOA_CuM5Xip-%&ECc?ak*12gd(Bc}FN(=9=0lLbbL7wRTw7$E#{Ofuu82ufG4Zv z(C;ZA&y~VQWYfp=H%X!*{$jqLUtR@tu}E6dXFQCl7_>&QWMtMGlw$a#SE*49EkbX! zU25Ry-;Qi0r{{sxpzP|EY+45amR~`U2sWj>T%nZ;ot)KPA%s&=h&KQ+NuxNo2lgNJ zLs!>DYs0_>1IC04?K-D_vz{hdT3iH~6V%;c_bQYk);)3w7%E#}82c(iA-Z(Wl=O{Q zGv#oZb>pzCD)e|$@};jcf%_Ja zB;gIo1t@Q%7@S#=pFKBz`f=ZeeT$97x#w0AofJw$8;JnO@mJN0?jEaDA+@L; zah2;wT#k@?4Fsq_fSb@tULjRxDFj#F!)qD2&cHZk*w{*YQGjfw5fUn5DVg2^^~yTN zD0-tp^P>5fu9q!c90`;EKhW`R$H4#e|6l*1zEAWX==sV1A0_@_-*>w|=z6zvjYs$I zzqJMLRDVaRyw<;#I5s{Iz-)F`65#*#@#NLY>OH5mc%i{sw7IIH$yFLnr}CrOR;<$X>~L-{{wn>7SI{brHCUxK)_pX&LbTJ( z59eC3LQ_NO(UI6Iw6^Gd0X;I;V1eRRrZhQgLa+I3OJ-fnkLGi+XMOjGcT#e3uEDh1 zTRPL^BIPoun2{EU#`NlNc4QZar^oxe7U`h|i`0tZo+g)K7;QVkJ+$YWXZlVqn~i@; z?)G}ma!S@lDs^w$wp1VOxHK(TjP%gRE*9gE_l)F%Aavc;XkkUvj%(ly6KcBigDp`f zuZ+gq7s;^M3KeS2@_a+&zH*1fmAyj;r%Hy*4BWt_6MOd+)0 zW<qDs@sNQ1s#Zlucj9YUZJpK4sE`npONXxkDS27BEkZMp{~igI{{S!m7l&4`EQ7kfmj~Su};4){PS#PnB zG&$!C9(N|+qB#%8cdOQJc$b*-l?IzR2I-Ln+m*YiR2GDwv%Ehf6I{v`25oYt)L7&P ziRA6sQ7Dx&nceK}E8azB`fP)lo+-IIos6cjX2}2$=}eQe%jB6|OCoSHMDpUhL2DPh z3(W3VgV{~0^_SS(F#(&LQ67^%-x8C)%)SrDKaCUKI5RrYU`FTWlNZ^e#pHP~fuK&B z)t=B0=Z9K0tzCF$U-MpMT5Ct@JMULiT7`3h!B&l8lvUwVC75CrfJ_#xmD#}3GDj{& zS~ozqlH=HMoA-VYr-rk;3G%(;y`U9%wEhYd%o@Tnc1?&whfp69egd*eIIIc+r zg4HbAr6WUMOrz>?u(Lqyc>$*H5ary(J#))D-`A0Pr~dpe6=?<|kQFd4RZJ+8A8>Dw zGUFe>70#~C(b5cj%BHm3kLkWWIFgNj#oYa6@0=nuBMnY6Ov&PLlBFFG{o=Px(d_bu zQ8H9$P6@DE1v>K;x+kjI$EE6!2s(aYJqZ;74O7tWi=(ll^!rI9Y^jk}T))$|v)RmO zd?oj5-dUZ6N9xPM1ST?MN!W?7UHDcNC_w8|XAnXh4mvF>0f`9$`7t?syi=h+u%ev_ z23P2Pkd4xGhBlG0=YO}uJEQp@t3Us9qN0I=(^ZZJR+cL&pH{n)sZvRwFX%oR6~t{(}0nW_ML0jsYge(~25IoTXGW{6h}s zHYdb9HmOQz95P5f3LuyQaRbW3n74Ko3wKDU5X|RW67OCe%#Oyl1nxfIotBWCsV^iG zCi+qz#T9_9%$c_1#4b~S7MZ=MgK$RzdMd&@+!A7>1$snbiP~}lf`h!t@g`Al%wGU8 za-5pcBYT#b>6LsgmyLaL`ta+Y==h_K!|xq_`q1|d&F=qJXZp}52L7q@uM*{fm%9J3 z=bK%>b@2O%_q%?4@GTGmzp?)je)ymJhr0^M1O6Z(15HwbwR#m zCp<&~TUF5abW)7Tcgm$VL+^ckt#5y7?d9m33x&c2C*)2E?3+dX>>pkvs$&q3vnx}I z2yy|zCO9#9dVE~sbHi1vsK(e(NlUlNv;29U=n!U`VPq@QMb}q5O8fPtGl|rdI$r{3 zpKvfm*N&UIYFT|;g$pHYfjV<~?U_XCWb{3k{i-`D4To771FFtHh8E=2i-}Yz`pXy2 zojsnsP?#v_+@N-6X^26POXeqx$PkmEK2n@BXw$+3X|GJNzP-^Gx|YtgteyfCdMk-N z$2yfkrxIi3U#v$9f0CKwn9tY+$Kb8lYv_|-ekqYEM@KjdqU`*&iAl_*3jAS!evI#6 z))C->ng?D)L9dv=i;E{iZ~WrRi4?4EfkXy)l?e!EO);pc$aU!EvBpg{Xgp}{ur)|$ zXMB>x1>sb}%W+m*>gsuF!$>;PTprIY6qsaK;fZ8H27deuaI z^-(9d6#BKx^_M+f=qB{Wm+BwZW&$Ij_gwm1BK2%^Z5o9gLce?QL?U%8`n%Omedy;e zWD}_q(VvgF%%NWzpXUG{iT+mWJkro`FU~RCqtW47=Kh67d*RhY3O2afgT7l|EA;O3 ztIVVh3us&Xn$XDSW*PaB=(JkpHiQN{+hCe)fg6NIes-;!dH%e~dfP#kTV`JOhwpf; zi=lqbw7hGbe8ZVK;@VCFJ|sz}>t7Fhp{!nDPIYv1D?QiH6wZ8&0SnPZY@e4H8vgki zhQBY|$`uB>pA5%eXFP}sYp;klf-*x3TR59YU5gIi6mli>qNnT2cs$ro=#8I?lK8^W zbJa0(nHRkzavs0WBE~Y^`QSpga(-e^a=59C#+}ARcP%~T)ey5$32OC7T|bHP>Q;4} zw#Wq;V$9okEwro0=spsq^=x4~p3qRAc?Igh?hvu!H5|oc;an0*6UUS7GdcS(=)aFv z;G-4zXazo6fflR)h?g%DfNq3%sT0%mt%#jqvqKh`e^kd!31`G{`*c zChFSc%m=|s47DVuuso8@$DjG#$GtBp>GE=eso$TlaFdIbM@&=%x940#>5WV#9X|mB zNa!`hq!CD{&_v+6$(g1}HKki~AV%})`1#1SSG`x6>B$CC(J-{$BR~DH#=iQH`5YK-y4JJOnj`x-A?aio%7xXQ{G#8qS-~D@G#Q~ zWmnQ7>_Vy+yf3otjew2u5-3g18MfhkrX_$l=A4dSp1byzca1r(HrV&wrFfd1JD|>V zD^Mlc@ANKEB@5owZT34}5~|rbQxunOjcQ}gy8uAWdRLfpvB7>fpC)T^zA$^{2U}8p MJ~IS!aBNln|1WInPyhe` literal 0 HcmV?d00001 diff --git a/rss_service.py b/rss_service.py index c781031..02c9364 100644 --- a/rss_service.py +++ b/rss_service.py @@ -3,10 +3,25 @@ import feedparser +def extract_image_url(entry) -> str: + if "media_content" in entry and entry.media_content: + return entry.media_content[0].get("url", "") + + if "media_thumbnail" in entry and entry.media_thumbnail: + return entry.media_thumbnail[0].get("url", "") + + if "links" in entry: + for link in entry.links: + if link.get("type", "").startswith("image/"): + return link.get("href", "") + + return "" + + def fetch_feed(university_name: str, feed_url: str) -> list[dict]: parsed_feed = feedparser.parse(feed_url) - fetched_at = datetime.now(timezone.utc).isoformat() + articles = [] for entry in parsed_feed.entries: @@ -23,6 +38,7 @@ def fetch_feed(university_name: str, feed_url: str) -> list[dict]: "link": link, "published": entry.get("published", "Unknown date"), "fetched_at": fetched_at, + "image_url": extract_image_url(entry), } ) diff --git a/settings.py b/settings.py index 323acfb..ea36c5a 100644 --- a/settings.py +++ b/settings.py @@ -1,7 +1,5 @@ from pathlib import Path -from PyQt6.QtCore import QStandardPaths - APP_NAME = "UniNews" @@ -10,21 +8,7 @@ DATA_DIR = BASE_DIR / "data" FEEDS_FILE = DATA_DIR / "feeds.json" +APP_DATA_DIR = BASE_DIR / "local_data" +APP_DATA_DIR.mkdir(parents=True, exist_ok=True) -def get_app_data_dir() -> Path: - app_data_location = QStandardPaths.writableLocation( - QStandardPaths.StandardLocation.AppLocalDataLocation - ) - - if app_data_location: - app_data_dir = Path(app_data_location) / APP_NAME - else: - app_data_dir = Path.home() / ".uninews" - - app_data_dir.mkdir(parents=True, exist_ok=True) - - return app_data_dir - - -APP_DATA_DIR = get_app_data_dir() -DATABASE_PATH = APP_DATA_DIR / "uninews.db" \ No newline at end of file +DATABASE_PATH = APP_DATA_DIR / "uninews.db" diff --git a/test.py b/test.py new file mode 100644 index 0000000..7edb5f8 --- /dev/null +++ b/test.py @@ -0,0 +1,15 @@ +from notifications import should_notify, send_article_notification + +article = { + "university": "Wageningen University", + "title": "New Scholarship Deadline Announced", + "summary": "Applications close on June 15." +} + +keywords = ["scholarship", "deadline"] + +print("Should notify:", should_notify(article, keywords)) + +send_article_notification(article) + +print("Notification sent.") \ No newline at end of file diff --git a/ui/__pycache__/main_window.cpython-310.pyc b/ui/__pycache__/main_window.cpython-310.pyc index 273557381fc794740056e0d0cf0605b5b329b3c6..a81c36327f77ba3b191e900eb457fbe23127315f 100644 GIT binary patch literal 16607 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 delta 4684 zcmZu#U2Gf25xzSf$-CpPNb1)TeX>Mbv?JM)EL)Oox$=+lQ(JK&J5H`}m<#tLQ9Ax; z?_^5?Rl2Z?rYMTmSrmN;0!T$4^H4W0%|lzbNYTEeO&&v}Xi)SaMjA9J`p`ebo!O&g zSuu%Q&hF06&d$wzGs{1JOT1}B%aMpCz-R80u7$p#JJC+@-8+Y_=%PRb;d%>q1G(5ocWwvC|cAA;!k-z}Lj~CxE0!wwpnudZ9V-_N`!5j1Ob~C^YwFq}o|b zeMRhG6?JdmI1Y@mAF0pO3>8#6yG)%jr4kGqA+IwtZ&oZLaj9G=;SruPOQgY-#>IQF z>BFWA8yy-$H3_+3u7cWz2IP!OEjK?;kHK74rv1ngnzu}1(Oll7#E3wLy{xsScAa4i zIH%ZmwR!f3h$=qM{;OqcuCU-=2yBY@78b;X;D!RfYNM}hgm4slM1XhZn>Ip|-rKlP zaNPT0)F4b1_yPNS6FD1Z=qVVYqtF=5V1^egoHOm2JT2Sy#WJxB&7*6)6ZJZmEPAQ1 zV%bJh!L^F#%FD0X)>*rdU$h9fOFdk!SfyNJBYu85fg3&Ug^k0+0z3{Xsfc0t#^IZg z*1E5h3e(mN=W3xu*ry{2_D&?t-s)_TRF$dGN&m>y@u@THZgZM_7Cr30F%QZxv$>7E z-?Eo|-W6c4#A5RK$*Ez^av_}w44xkv8$QF9;$7145EIiWcBD1J^h^``I1^`IruzcN zjvhZciX4a8ubboS{bYoFn%Ey49vvAwHhhBJ>uuZKr-N;z60B2?`Ufj3`Vjl#Kr`$b zW1k;r0kM8|xGl(j5I@8In2fQL`wj($&L5o|8RELUpNz7ny8Hb@L&KxPY%rAurc>_nhbgjJ^^TCQ4*C0YaKiMrF>(+0}c;o-u_2+ zMik?0>d^TCjKjJ-Td=LV-=Y-4{?YKOYj)Wrxmn91&IbZ}=g(hF7J zhE`Q@+ocKI3g94!y{p1fMi$)gYDE;*RZ&=ryAcwu`rm*QR8`*)-jr`~u9pBd*41yS zOBwI?hIj*2j=~<0aW+p&>lzmut7_iaV2j`;u;k*96Pg;iAyM@;X3-+?ZCYjn{av*@ zm~H!mKuS?q8}-@r+`4DjYm3~aL&UQ2>t(eT@aV!^G; zC9`P#hnf8yH98Ff*5YT&%Qn$VWmlg?G}qUpalM`_nk#_e`FV?M!JKpdpK!v<^du^p zq?7Pd_d_}~DLoCep@DMeOC~@iF@iu}wpLtv5oTyNvWDj@cUu&mwkFUfoCts)TO~*q zj(OeM=BTUZSMpZHEtE@je}zJhqCLpnY?WrqG{4OVC=bV?`*1EsK@Q}ehWVU&Y95lB zZROx_g;~ITl;&hZUZy(Y=MiA2Wt)JM(xPK@m8=`vt8=i=BP-dD2lmx)BYFv&3)sAX z&7-f0C(XaYxPvJ{6T?ziRHT3umI7bOeg%4AQI;f8k~A2Pikc|hj7w2T7nPgRgQySY zB}LSLKQ8VLdCJml|lbteM$I#%fq3qPKB)K;MI4*#QsE9v1)tf*!qFb|H-~@aW`RN_9JJ zLtldEzOpV=r45xx#0MbY zo-db5kTSr}2I_vd;M$gonsk+3fuZ{! zL9^x?7#LWSG;KKuJzf9H&7bJH7p&gJu2?87yKq0+B^A&34UxTeYWfP!c;G=qs3RnV4B}gqXaXW! zA~HrG#vJr~n00yl`KKY4@P33zY;N8)WMa}q^Iwo)H61=I96iWB>ud{64IUqwJmHN! z7u~}?iSA<;d)q?1EN+Z1K|%OoOAC9ir!_o0d~|SRWXFOh)BD&jqha=^y>0BxOl+%) zKGmhNOYJJ_&ZOA2XpH?konevvy}%NO61bU-QutjLQzk{!q?r9E0#Drs$VV~Msg4cC2tC9) zPq#huP^$qPzXFY8H$)u={%WAt3wetdO-5q5FaX%gd2$V8K#=ha2^F6}@m~ic z@<{1Jw=sdkH{boCX^(p(o}#XtESAZ#ZB2lY1*ZcVMTi5?9udQlEx5&dtSqH`{|E6K Bo?!q0 diff --git a/ui/main_window.py b/ui/main_window.py index f787c46..c83a007 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,8 +12,11 @@ QMessageBox, QPushButton, QVBoxLayout, - QWidget, + QWidget, QScrollArea, ) +from PyQt6.QtGui import QDesktopServices, QPixmap +from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest +from PyQt6.QtCore import QByteArray from database import Database from settings import FEEDS_FILE @@ -41,7 +44,7 @@ def __init__(self): self.app_settings = load_app_settings() self.setup_ui() - self.apply_styles() + self.apply_theme() self.load_cached_articles() if self.app_settings.get("refresh_on_startup", True): @@ -104,84 +107,289 @@ def create_header(self): self.root_layout.addWidget(header_card) def create_article_list(self): - self.article_list = QListWidget() - self.article_list.setObjectName("ArticleList") - self.article_list.setSpacing(12) - self.article_list.setVerticalScrollMode(QListWidget.ScrollMode.ScrollPerPixel) - self.article_list.itemDoubleClicked.connect(self.open_article) + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setObjectName("ArticleScrollArea") + self.scroll_area.setStyleSheet(""" + QScrollArea { + background-color: #080A14; + border: none; + } + + QScrollArea > QWidget > QWidget { + background-color: #080A14; + } + """) - self.root_layout.addWidget(self.article_list) + self.article_container = QWidget() + self.article_container.setStyleSheet("background-color: #080A14;") + self.article_container.setObjectName("ArticleContainer") + self.article_layout = QVBoxLayout(self.article_container) + self.article_layout.setContentsMargins(0, 0, 0, 0) + self.article_layout.setSpacing(18) + self.article_layout.addStretch() - def apply_styles(self): + self.scroll_area.setWidget(self.article_container) + self.root_layout.addWidget(self.scroll_area) + + def apply_light_theme(self): self.setStyleSheet( """ QMainWindow { - background-color: #F5F7FB; + background-color: #F4F0FA; + } + + QWidget { + font-family: "Segoe UI", "Inter", "Arial"; + color: #1F2937; } #HeaderCard { - background-color: white; - border-radius: 18px; - border: 1px solid #E5EAF3; + background-color: #FFFFFF; + border-radius: 24px; + border: 1px solid #E5D9F2; } #TitleLabel { - color: #1E293B; - font-size: 32px; - font-weight: 800; + color: #171124; + font-size: 34px; + font-weight: 900; + letter-spacing: -1px; } #SubtitleLabel { - color: #64748B; + color: #7E3AF2; font-size: 14px; + font-weight: 600; + } + + QPushButton { + background-color: #FFFFFF; + color: #2D1B45; + border: 1px solid #D8C7EF; + padding: 10px 18px; + border-radius: 11px; + font-size: 13px; + font-weight: 700; + } + + QPushButton:hover { + background-color: #F3E8FF; + border: 1px solid #A855F7; + } + + QPushButton:pressed { + background-color: #E9D5FF; } #RefreshButton { - background-color: #385963; - color: white; + background-color: #A855F7; + color: #FFFFFF; border: none; - padding: 11px 20px; - border-radius: 10px; - font-size: 14px; - font-weight: 600; } #RefreshButton:hover { - background-color: #2E4A52; + background-color: #9333EA; + } + + #RefreshButton:disabled { + background-color: #C4B5FD; + color: #FFFFFF; + } + + QScrollArea { + background-color: #F4F0FA; + border: none; + } + + QScrollArea > QWidget > QWidget { + background-color: #F4F0FA; + } + + #ArticleCard { + background-color: #FFFFFF; + border: 1px solid #E6DDF3; + border-radius: 24px; + } + + #ArticleCard:hover { + background-color: #FBF7FF; + border: 1px solid #A855F7; + } + + #ArticleImage { + background-color: #F3E8FF; + border: 1px solid #E9D5FF; + border-radius: 18px; + color: #7E22CE; + font-size: 24px; + font-weight: 900; } - #RefreshButton:pressed { - background-color: #22383F; + #ArticleTitle { + color: #171124; + font-size: 17px; + font-weight: 850; + } + + #ArticleMeta { + color: #7E22CE; + font-size: 12px; + font-weight: 750; + } + + #ArticleSummary { + color: #4B5563; + font-size: 13px; + } + + #OpenArticleLabel { + color: #A21CAF; + font-size: 12px; + font-weight: 850; + } + + #EmptyLabel { + color: #6B7280; + font-size: 16px; + padding: 60px; + } + + QScrollBar:vertical { + background: transparent; + width: 10px; + margin: 4px; + } + + QScrollBar::handle:vertical { + background: #D8B4FE; + border-radius: 5px; + } + + QScrollBar::handle:vertical:hover { + background: #A855F7; + } + + QScrollBar::add-line:vertical, + QScrollBar::sub-line:vertical { + height: 0px; + } + """ + ) + + if hasattr(self, "article_container"): + self.article_container.setStyleSheet("background-color: #F4F0FA;") + + def apply_dark_theme(self): + self.setStyleSheet( + """ + QMainWindow { + background-color: #080A14; + } + + QWidget { + font-family: "Segoe UI", "Inter", "Arial"; + color: #F8FAFC; + } + + #HeaderCard { + background-color: #111827; + border-radius: 24px; + border: 1px solid #2A3144; + } + + #TitleLabel { + color: #FFFFFF; + font-size: 34px; + font-weight: 900; + letter-spacing: -1px; + } + + #SubtitleLabel { + color: #D8B4FE; + font-size: 14px; + font-weight: 500; + } + + QPushButton { + background-color: #1F2937; + color: #F8FAFC; + border: 1px solid #374151; + padding: 10px 18px; + border-radius: 11px; + font-size: 13px; + font-weight: 700; + } + + QPushButton:hover { + background-color: #312E81; + border: 1px solid #C084FC; + } + + QPushButton:pressed { + background-color: #581C87; + } + + #RefreshButton { + background-color: #C084FC; + color: #080A14; + border: none; + } + + #RefreshButton:hover { + background-color: #E879F9; } #RefreshButton:disabled { - background-color: #94A3B8; + background-color: #475569; + color: #CBD5E1; } - #ArticleList { + #ArticleScrollArea { background-color: transparent; border: none; - outline: none; } - #ArticleList::item { - background-color: white; - color: #1E293B; - border: 1px solid #E5EAF3; - border-radius: 16px; - padding: 18px; - margin: 2px; + #ArticleCard { + background-color: #111827; + border: 1px solid #2A3144; + border-radius: 22px; + } + + #ArticleCard:hover { + background-color: #161F33; + border: 1px solid #C084FC; + } + + #ArticleTitle { + color: #FFFFFF; + font-size: 17px; + font-weight: 800; + line-height: 1.3; + } + + #ArticleMeta { + color: #C084FC; + font-size: 12px; + font-weight: 700; } - #ArticleList::item:hover { - background-color: #F8FAFC; - border: 1px solid #CBD5E1; + #ArticleSummary { + color: #CBD5E1; + font-size: 13px; + line-height: 1.5; } - #ArticleList::item:selected { - background-color: #EAF2F4; - border: 1px solid #385963; - color: #1E293B; + #OpenArticleLabel { + color: #F0ABFC; + font-size: 12px; + font-weight: 800; + } + + #EmptyLabel { + color: #94A3B8; + font-size: 16px; + padding: 60px; } QScrollBar:vertical { @@ -191,12 +399,12 @@ def apply_styles(self): } QScrollBar::handle:vertical { - background: #CBD5E1; + background: #374151; border-radius: 5px; } QScrollBar::handle:vertical:hover { - background: #94A3B8; + background: #C084FC; } QScrollBar::add-line:vertical, @@ -226,46 +434,34 @@ def load_feeds(self) -> list[dict]: return [] def refresh_news(self): - self.article_list.clear() + self.clear_article_cards() self.refresh_button.setEnabled(False) self.refresh_button.setText("Refreshing...") feeds = self.load_feeds() - - old_links = { - article.get("link") - for article in self.database.get_articles(limit=5000) - } - - new_articles_for_notification = [] + print("Feeds loaded:", feeds) for feed in feeds: university_name = feed.get("name", "Unknown university") feed_url = feed.get("url", "") - if not feed_url: - continue + print("Fetching:", university_name, feed_url) try: articles = fetch_feed(university_name, feed_url) - - for article in articles: - if article.get("link") not in old_links: - new_articles_for_notification.append(article) + print("Fetched articles:", len(articles)) self.database.save_articles(articles) + print("Saved articles. DB now has:", len(self.database.get_articles(limit=5000))) except Exception as error: print(f"Could not fetch {university_name}: {error}") - max_cached_articles = self.app_settings.get("max_cached_articles", 500) - self.database.enforce_article_limit(max_cached_articles) - self.articles = self.database.get_articles() - self.render_articles() + print("Articles loaded from DB:", len(self.articles)) - self.handle_notifications(new_articles_for_notification) + self.render_articles() self.refresh_button.setEnabled(True) self.refresh_button.setText("Refresh news") @@ -280,39 +476,94 @@ def open_article(self, item: QListWidgetItem): if url: QDesktopServices.openUrl(QUrl(url)) + def clear_article_cards(self): + while self.article_layout.count(): + item = self.article_layout.takeAt(0) + + widget = item.widget() + + if widget is not None: + widget.deleteLater() + def render_articles(self): - self.article_list.clear() + self.clear_article_cards() if not self.articles: - empty_item = QListWidgetItem( - "No articles found.\nCheck your RSS feeds or internet connection." - ) - empty_item.setFlags(Qt.ItemFlag.NoItemFlags) - self.article_list.addItem(empty_item) + empty_label = QLabel("No articles found.\nTry refreshing your feeds.") + empty_label.setObjectName("EmptyLabel") + empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter) + + self.article_layout.addWidget(empty_label) + self.article_layout.addStretch() return for article in self.articles: - title = article.get("title", "Untitled") - university = article.get("university", "Unknown university") - published = article.get("published", "Unknown date") - summary = article.get("summary", "") + card = self.create_article_card(article) + self.article_layout.addWidget(card) - clean_summary = self.clean_text(summary) + self.article_layout.addStretch() - if len(clean_summary) > 220: - clean_summary = clean_summary[:220].strip() + "..." + def create_article_card(self, article: dict) -> QFrame: + card = QFrame() + card.setObjectName("ArticleCard") + card.setCursor(Qt.CursorShape.PointingHandCursor) - item_text = ( - f"{title}\n\n" - f"{university} • {published}\n\n" - f"{clean_summary}" - ) + card_layout = QHBoxLayout(card) + card_layout.setContentsMargins(18, 18, 18, 18) + card_layout.setSpacing(18) + + image_box = QLabel() + image_box.setObjectName("ArticleImage") + image_box.setFixedSize(132, 92) + image_box.setAlignment(Qt.AlignmentFlag.AlignCenter) + + university = article.get("university", "UniNews") + initials = "".join(word[0] for word in university.split()[:2]).upper() - item = QListWidgetItem(item_text) - item.setData(ARTICLE_LINK_ROLE, article.get("link", "")) - item.setToolTip("Double-click to open article") + image_box.setText(initials) - self.article_list.addItem(item) + content_layout = QVBoxLayout() + content_layout.setSpacing(8) + + title = QLabel(article.get("title", "Untitled")) + title.setObjectName("ArticleTitle") + title.setWordWrap(True) + + published = article.get("published", "Unknown date") + + meta = QLabel(f"{university} • {published}") + meta.setObjectName("ArticleMeta") + meta.setWordWrap(True) + + summary_text = self.clean_text(article.get("summary", "")) + + if len(summary_text) > 230: + summary_text = summary_text[:230].strip() + "..." + + summary = QLabel(summary_text) + summary.setObjectName("ArticleSummary") + summary.setWordWrap(True) + + open_label = QLabel("Open article →") + open_label.setObjectName("OpenArticleLabel") + + content_layout.addWidget(title) + content_layout.addWidget(meta) + content_layout.addWidget(summary) + content_layout.addWidget(open_label) + + card_layout.addWidget(image_box) + card_layout.addLayout(content_layout) + + link = article.get("link", "") + + def open_link(event): + if link: + QDesktopServices.openUrl(QUrl(link)) + + card.mousePressEvent = open_link + + return card def clean_text(self, text: str) -> str: clean = text.replace("\n", " ").replace("\r", " ").strip() @@ -356,60 +607,9 @@ def apply_theme(self) -> None: theme = self.app_settings.get("theme", "light") if theme == "dark": - self.setStyleSheet( - """ - QMainWindow { - background-color: #0F172A; - } - - #HeaderCard { - background-color: #1E293B; - border-radius: 18px; - border: 1px solid #334155; - } - - #TitleLabel { - color: #F8FAFC; - font-size: 32px; - font-weight: 800; - } - - #SubtitleLabel { - color: #CBD5E1; - font-size: 14px; - } - - #RefreshButton { - background-color: #385963; - color: white; - border: none; - padding: 11px 20px; - border-radius: 10px; - font-size: 14px; - font-weight: 600; - } - - #ArticleList { - background-color: transparent; - border: none; - outline: none; - } - - #ArticleList::item { - background-color: #1E293B; - color: #F8FAFC; - border: 1px solid #334155; - border-radius: 16px; - padding: 18px; - margin: 2px; - } - - #ArticleList::item:selected { - background-color: #334155; - border: 1px solid #385963; - } - """ - ) + self.apply_dark_theme() else: - self.apply_styles() + self.apply_light_theme() + + From b5ab823ce0577feae5250ba5945c82ca456ccb62 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Fri, 5 Jun 2026 16:54:00 +0200 Subject: [PATCH 3/7] Drawer, Scraper and UomScraper --- .github/workflows/build.yml | 106 +++++++++ __pycache__/database.cpython-310.pyc | Bin 4996 -> 6411 bytes __pycache__/rss_service.cpython-310.pyc | Bin 1149 -> 1169 bytes data/feeds.json | 20 +- database.py | 84 +++++-- local_data/settings.json | 2 +- local_data/uninews.db | Bin 135168 -> 122880 bytes requirements.txt | Bin 412 -> 736 bytes rss_service.py | 3 +- scrapers/__init__.py | 0 scrapers/__pycache__/__init__.cpython-310.pyc | Bin 0 -> 166 bytes .../__pycache__/base_scraper.cpython-310.pyc | Bin 0 -> 609 bytes .../scraper_service.cpython-310.pyc | Bin 0 -> 551 bytes .../__pycache__/uom_scraper.cpython-310.pyc | Bin 0 -> 3747 bytes scrapers/base_scraper.py | 9 + scrapers/scraper_service.py | 16 ++ scrapers/uom_scraper.py | 154 +++++++++++++ ui/__pycache__/main_window.cpython-310.pyc | Bin 16607 -> 21624 bytes ui/main_window.py | 216 +++++++++++++++++- 19 files changed, 564 insertions(+), 46 deletions(-) create mode 100644 .github/workflows/build.yml create mode 100644 scrapers/__init__.py create mode 100644 scrapers/__pycache__/__init__.cpython-310.pyc create mode 100644 scrapers/__pycache__/base_scraper.cpython-310.pyc create mode 100644 scrapers/__pycache__/scraper_service.cpython-310.pyc create mode 100644 scrapers/__pycache__/uom_scraper.cpython-310.pyc create mode 100644 scrapers/base_scraper.py create mode 100644 scrapers/scraper_service.py create mode 100644 scrapers/uom_scraper.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..5eec734 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,106 @@ +name: UniNews CI/CD Pipeline + +on: + push: + branches: ["main", "geo"] + tags: ["v*"] + pull_request: + branches: ["main"] + +jobs: + validate-and-package: + runs-on: ubuntu-24.04 + + permissions: + contents: write + + steps: + - name: Checkout Source Code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Python Dependencies + run: | + python -m pip install --upgrade pip + pip install ruff setuptools wheel + pip install -r requirements.txt + + - name: Run Ruff Static Analysis + run: | + ruff check . + + - name: Install Native Build Tools + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + debhelper \ + dh-python \ + python3-all \ + python3-setuptools \ + python3-pyqt6 \ + python3-bs4 \ + python3-requests \ + python3-feedparser + + - name: Scaffold Debian Control Files + run: | + mkdir -p debian + mkdir -p debian/source + + echo "3.0 (native)" > debian/source/format + + cat << 'EOF' > debian/rules + #!/usr/bin/make -f + %: + dh $@ --with python3 --buildsystem=pybuild + EOF + chmod +x debian/rules + + cat << 'EOF' > debian/control + Source: uninews + Section: python + Priority: optional + Maintainer: George Apostolidis + Build-Depends: debhelper-compat (= 13), dh-python, python3-all, python3-setuptools + Standards-Version: 4.6.2 + Homepage: https://github.com/open-source-uom/UniNews + + Package: python3-uninews + Architecture: all + Depends: ${python3:Depends}, ${misc:Depends}, python3-pyqt6, python3-bs4, python3-requests, python3-feedparser + Description: University RSS and news reader desktop app + UniNews is a PyQt6 desktop application that aggregates university + RSS feeds and selected university news pages into one clean interface. + EOF + + DEB_DATE=$(date -R) + DEB_AUTHOR=$(git log -1 --pretty=format:'%an <%ae>') + + printf "uninews (0.1.0) noble; urgency=low\n\n * Auto-generated release.\n\n -- %s %s\n" "$DEB_AUTHOR" "$DEB_DATE" > debian/changelog + + - name: Build Debian Packages + run: | + dpkg-buildpackage -us -uc -b + dpkg-buildpackage -us -uc -S + + mkdir -p output_package + mv ../uninews_* output_package/ + + - name: Upload Package Artifacts + uses: actions/upload-artifact@v4 + with: + name: uninews-packages + path: output_package/* + + - name: Publish .deb to GitHub Releases + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: output_package/*.deb \ No newline at end of file diff --git a/__pycache__/database.cpython-310.pyc b/__pycache__/database.cpython-310.pyc index 9d714c4dfa647aa974d8cfb344b7f2074d03d516..4c2f58c5ed5af7a18554c91da9545d404069dda4 100644 GIT binary patch delta 2338 zcmahKU2GIZcxLwJ_O`cs*SkW0XkmM$#e-5Pf|Xx|_97&B7z$}^vyS;CMXJivwOX^1maHin{U3K znQy+oS=jd09{5PC%*^Zlx&I)z!5vH7HWw!;F~sF+~+k={c{2%9iuOdb)5f2AJMO<)hh?_A|gfh}nl zFA++pQYBU5;VKQ(F^vZ6nBKseXsAvz8Z=X2Q(BeOZI^`Y!pcNAmEO(g1KAbxz7O2t z6>X@szAl+(^Oy34xx7_3|6}b&?@PkAP5?4i!vyOjiuE;v+?OAkCZ7qOH{u|akXRTd z*28PTK0M8_zOC{HMyRIbvX`^Pi~`J+PvnwVd$+#}2*qV(8kJQSvzUu1xg^1?P-zI2 z6<1wCF81O;}Y` z_Jgj_Kx1RMo(5z|a>0-)DQm8_Bv(;6NFgW-$PJF5>mt?kRN98y>eiZ%|E9NcBcgGw zDdNpLWS#Wu97kF--_QPn{~U;U{67O5yjj^5YZr2)mH6m;e$nty5BVC^X;25*=>r6+1^b>5bx|fRS%NNAmY{n`kdjh8CE+d$ z>+(^g$!PLqa`=oja$@w%iPZ2JzFqEwl-e9Wb$TRu+B$Y_ZRQ*u9X@C^cHm!x1}acs zs2wem*9qi2W?qg&z$CUq01xv(vsmr~@SQ*g0F-w6t=O>#rtRXYJF>Pja41*Eq;t*@ zK(bCTQ@A{Hvg0Q&@3e`YQ6_~u`OkfQ;RaY)s$NlNWc zHN?9QUkrAf5*@An6hKN%VW@o3oP~QmXRo^W?S_M%;NE&W)%oY)z5`>&lc$qw{R@@3 zPPq{SzfdjVe@_5hAFqf`EOPq14$=3*z~G%&7Y8xhMS zeqRKVYg7r{z&qj-=1fFns#2*laJWy5rZ~dSyMm2mp^`22LJx4rz2exv!6&Ze#1#<&Z}P>A<~P6ho45P+y?J|Yd9dW( za2$)^7=&}1+x~s`>(knN6Ib#JLPQ!ANQ1$%z#fSs`r`dI*(Su|SEQR|t?xUVk3oy6~!lu_}o zRbx!OlY7H*P&%)3YO8N^a~HuFv5XEGNXQ*B^O)tqI@AisnypzI7bW{&!ABTdM-H8O zv!aS-M}1=s)!F=tx|9E*wyHx-AqXm6MlEL1|0nxP?75CQXI*tpqD#o*B5v`#8d-~U zT|KduzI>vEGQzl!M9sK$eLamnPsc{L^GPRd2|PF&c_ap|{p1EnfcC`(=^Gi1gA9xH zF~?m1KS%l^GlnFiBZ^6|cnMXZRWLHPO~81-`w~59CNE?%lN)^BQGh&t^gtn-Y{)Zw z*@*RXS#Bssq>t4Db3ih4joj52m#_DpTIy+GTG_Uz7VI*X=Z?BkDyh&ZoUy8hqvx%5 zyVdu%I=7-Y>7<<~=>^r*`danok#u{f>o}B#s^^|E6y}$xhwgjx4YM7kt?SKhH_RWD zPgg5m)Uc$Xs;b55d_%Tw#>tJiq1KA?m6m>d20=*S+RVf|5s0>_?iGKf->cWX%e1DH zw`eS*(~moky<60O0qM~dy@VX!e8CUtPwG?u?N>fPp=TcS>j_Qt-K+Vv|1Iss^v)^OZ+v zCLRK#9(8g0)clI=*+Tsp%$4IBo`ft8aI1&BUXM>{!B>BrSa~%>8tX#W%O{_I4yj2dsb%|TO0!Hd{a@`$#`Yv<|1iOJrD6ST OEnC9pwyS)3Vz#_${GTEEO2mqlMECm1n delta 206 zcmbQp`Imz?pO=@50SGRs$!BqG!*n ex0s7dii!k5ES8+i;*wh|DVfP7Maq-oS&RTY%PhD6 diff --git a/data/feeds.json b/data/feeds.json index 9ef12bc..e9955b2 100644 --- a/data/feeds.json +++ b/data/feeds.json @@ -1,22 +1,12 @@ [ { "name": "MIT News", + "type": "rss", "url": "https://news.mit.edu/rss/feed" }, { - "name": "Harvard Gazette", - "url": "https://news.harvard.edu/gazette/feed/" - }, - { - "name": "Nature News", - "url": "https://www.nature.com/nature.rss" -}, -{ - "name": "Science Magazine", - "url": "https://www.science.org/action/showFeed?type=etoc&feed=rss&jc=science" -}, -{ - "name": "NIH News", - "url": "https://www.nih.gov/news-events/news-releases/feed" -} + "name": "University of Macedonia", + "type": "scraper", + "scraper": "uom_news" + } ] \ No newline at end of file diff --git a/database.py b/database.py index 5d73e7f..ae94b9f 100644 --- a/database.py +++ b/database.py @@ -62,11 +62,18 @@ def create_tables(self) -> None: except sqlite3.OperationalError: pass + try: + self.connection.execute("ALTER TABLE articles ADD COLUMN source TEXT;") + self.connection.commit() + except sqlite3.OperationalError: + pass + def save_article(self, article: dict) -> None: query = """ INSERT \ OR IGNORE INTO articles ( university, + source, title, summary, link, @@ -74,13 +81,14 @@ def save_article(self, article: dict) -> None: fetched_at, image_url ) - VALUES (?, ?, ?, ?, ?, ?, ?); \ + VALUES (?, ?, ?, ?, ?, ?, ?, ?); \ """ self.connection.execute( query, ( article.get("university", "Unknown university"), + article.get("source", article.get("university", "Unknown source")), article.get("title", "Untitled"), article.get("summary", ""), article.get("link", ""), @@ -97,28 +105,38 @@ def save_articles(self, articles: list[dict]) -> None: self.save_article(article) def get_articles( - self, - university: Optional[str] = None, - search_text: Optional[str] = None, - limit: int = 200, + self, + university=None, + source=None, + search_text=None, + limit: int = 500, ) -> list[dict]: query = """ - SELECT - id, - university, - title, - summary, - link, - published, - fetched_at, - image_url - FROM articles - WHERE 1 = 1 - """ + SELECT + id, + university, + source, + title, + summary, + link, + published, + fetched_at, + image_url + FROM articles + WHERE 1 = 1 \ + """ params = [] - if university and university != "All Universities": + if university and university != "All": + query += " AND university = ?" + params.append(university) + + if source and source != "All": + query += " AND source = ?" + params.append(source) + + if university and university != "All": query += " AND university = ?" params.append(university) @@ -130,7 +148,6 @@ def get_articles( OR university LIKE ? ) """ - search_pattern = f"%{search_text}%" params.extend([search_pattern, search_pattern, search_pattern]) @@ -173,5 +190,32 @@ def load_cached_articles(self): except Exception as error: print(f"Could not load cached articles: {error}") self.articles = [] + self.selected_university = "All" + self.search_text = "" + + self.render_articles() + + def get_universities(self) -> list[str]: + query = """ + SELECT DISTINCT university + FROM articles + ORDER BY university ASC; + """ + + cursor = self.connection.execute(query) + rows = cursor.fetchall() + + return [row["university"] for row in rows] + + def get_sources_for_university(self, university: str) -> list[str]: + cursor = self.connection.execute( + """ + SELECT DISTINCT source + FROM articles + WHERE university = ? + ORDER BY source ASC; + """, + (university,), + ) - self.render_articles() \ No newline at end of file + return [row["source"] for row in cursor.fetchall() if row["source"]] \ No newline at end of file diff --git a/local_data/settings.json b/local_data/settings.json index cde3c07..7c4e9c0 100644 --- a/local_data/settings.json +++ b/local_data/settings.json @@ -1,7 +1,7 @@ { "refresh_on_startup": true, "refresh_interval_minutes": 0, - "notifications_enabled": false, + "notifications_enabled": true, "notification_keywords": [ "deadline", "scholarship", diff --git a/local_data/uninews.db b/local_data/uninews.db index d18b69d0c0bc1f725db1d07809e61940e41b45fe..1b9d093fc2d056238e1f8c5165854d1503926358 100644 GIT binary patch literal 122880 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 MJ~IS!aBNln|1WInPyhe` diff --git a/requirements.txt b/requirements.txt index b9935732b73e23bc9b9c08cb09e4592416cf8416..ec9d5df9fa0c67491d120375e0d4d4972796dcf0 100644 GIT binary patch delta 331 zcmYLEO%B086#ft!iG?@-TMMFpYU2RH#tEbtG%2Mm!NwVyZXM1ATmj#_YGm@}<;{G5 z?@@VH@1OSq3LZ{K;A71mAV$c3fQ=E4jUEiSE!0uNlKAL{_61?04-*689G0GP+?Ki# z)fCZP?5P@I$G3n^H&GciC}5GKOII+Nd) z(9O&!O=d;*m>vpeR)HFdjZT)}%I$nwY%nL{!WYF!b3*C=y`3yBb=oH;4 list[dict]: articles.append( { "university": university_name, + "source": university_name, "title": entry.get("title", "Untitled"), "summary": entry.get("summary", ""), - "link": link, + "link": entry.get("link", ""), "published": entry.get("published", "Unknown date"), "fetched_at": fetched_at, "image_url": extract_image_url(entry), diff --git a/scrapers/__init__.py b/scrapers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scrapers/__pycache__/__init__.cpython-310.pyc b/scrapers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86fe48a552feb4f0b908a149c88512018a35145d GIT binary patch literal 166 zcmd1j<>g`kf;l&ovOx4>5P=LBfgA@QE@lA|DGb33nv8xc8Hzx{2;!HgvsFxJacWU< zOnPd5VnK{cesXDUYF-JDpIMSoniOA@T999yS(0CrnOYnZnwRO9T3#GeoLrPx0MZm6 gpP83g5+AQuPRv-R)LZS$z_7zV*MM>{*5wdMJE5c*Qa4WtYMDNCd4 zBIZLW?`l=F7283D!12Q2KuT3r#w(wj%1LQ?EQum_J_wy>-o_vOf3G&%i(I@wbl1l) mAVbR7XWFpyu?So(GaDyu6dU>@mC&EGGoHuGHdj3)$NUxS9gjl* literal 0 HcmV?d00001 diff --git a/scrapers/__pycache__/scraper_service.cpython-310.pyc b/scrapers/__pycache__/scraper_service.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d3388b81963767ae14d85a6a45514821b66ac48b GIT binary patch literal 551 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 list[dict]: + pass \ No newline at end of file diff --git a/scrapers/scraper_service.py b/scrapers/scraper_service.py new file mode 100644 index 0000000..b071fe9 --- /dev/null +++ b/scrapers/scraper_service.py @@ -0,0 +1,16 @@ +from scrapers.uom_scraper import UomNewsScraper + + +SCRAPER_REGISTRY = { + "uom_news": UomNewsScraper, +} + + +def run_scraper(scraper_id: str) -> list[dict]: + scraper_class = SCRAPER_REGISTRY.get(scraper_id) + + if scraper_class is None: + raise ValueError(f"Unknown scraper: {scraper_id}") + + scraper = scraper_class() + return scraper.scrape() \ No newline at end of file diff --git a/scrapers/uom_scraper.py b/scrapers/uom_scraper.py new file mode 100644 index 0000000..d42b0e8 --- /dev/null +++ b/scrapers/uom_scraper.py @@ -0,0 +1,154 @@ +from datetime import datetime, timezone +from urllib.parse import urljoin + +import requests +from bs4 import BeautifulSoup + +from scrapers.base_scraper import BaseScraper + + +class UomNewsScraper(BaseScraper): + name = "University of Macedonia" + + def __init__(self): + self.base_url = "https://www.uom.gr" + + self.sources = [ + { + "label": "UoM Main News", + "url": "https://www.uom.gr/nea", + "pages": 2, + }, + { + "label": "UoM Economics", + "url": "https://www.uom.gr/eco/ta-teleytaia-nea-toy-tmhmatos", + "pages": 1, + }, + { + "label": "UoM Balkan, Slavic & Oriental Studies", + "url": "https://www.uom.gr/bso/ta-nea-mas", + "pages": 1, + }, + { + "label": "UoM Applied Informatics", + "url": "https://www.uom.gr/dai/ta-nea-toy-tmhmatos", + "pages": 1, + }, + { + "label": "UoM Business Administration", + "url": "https://www.uom.gr/ba/enhmerosh", + "pages": 1, + }, + { + "label": "UoM Accounting & Finance", + "url": "https://www.uom.gr/fin/enhmerosh", + "pages": 1, + }, + { + "label": "UoM International & European Studies", + "url": "https://www.uom.gr/ies/ta-teleytaia-nea-toy-tmhmatos", + "pages": 1, + }, + { + "label": "UoM Music Science & Art", + "url": "https://www.uom.gr/msa/enhmerosh", + "pages": 1, + }, + { + "label": "UoM Educational & Social Policy", + "url": "https://www.uom.gr/esp/ta-nea-mas", + "pages": 1, + }, + ] + + def scrape(self) -> list[dict]: + articles = [] + seen_links = set() + + for source in self.sources: + source_articles = self.scrape_source(source) + + for article in source_articles: + link = article.get("link") + + if link and link not in seen_links: + seen_links.add(link) + articles.append(article) + + return articles + + def scrape_source(self, source: dict) -> list[dict]: + articles = [] + + for page_number in range(1, source.get("pages", 1) + 1): + page_url = self.build_page_url(source["url"], page_number) + + try: + html = self.fetch_page(page_url) + articles.extend(self.parse_page(html, source["label"])) + except Exception as error: + print(f"Could not scrape {page_url}: {error}") + + return articles + + def build_page_url(self, base_url: str, page_number: int) -> str: + if page_number == 1: + return base_url + + separator = "&" if "?" in base_url else "?" + return f"{base_url}{separator}pn={page_number}" + + def fetch_page(self, url: str) -> str: + response = requests.get( + url, + timeout=15, + headers={ + "User-Agent": "UniNews/0.1 (+https://github.com/open-source-uom/UniNews)" + }, + ) + + response.raise_for_status() + return response.text + + def parse_page(self, html: str, source_label: str) -> list[dict]: + soup = BeautifulSoup(html, "html.parser") + results = [] + + news_items = soup.select("article.post-news.post-news-wide") + + for item in news_items: + title_link = item.select_one("h6 a") + + if not title_link: + continue + + title = title_link.get_text(strip=True) + link = urljoin(self.base_url, title_link.get("href", "")) + + image = item.select_one("img") + image_url = "" + + if image and image.get("src"): + image_url = urljoin(self.base_url, image.get("src")) + + date_spans = item.select(".post-news-meta span.small") + published = ( + date_spans[0].get_text(strip=True) + if date_spans + else "Unknown date" + ) + + results.append( + { + "university": self.name, + "source": source_label, + "title": title, + "summary": source_label, + "link": link, + "published": published, + "fetched_at": datetime.now(timezone.utc).isoformat(), + "image_url": image_url, + } + ) + + return results \ No newline at end of file diff --git a/ui/__pycache__/main_window.cpython-310.pyc b/ui/__pycache__/main_window.cpython-310.pyc index a81c36327f77ba3b191e900eb457fbe23127315f..fc44c9ae7ad059b7f254e04c140c7de2c6822bcd 100644 GIT binary patch delta 7098 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 delta 3715 zcmZuzU2Gf25xzbC9myj_@lO(ENt7)|p=`^}pA)#UWXZp>8Cw2H9ABL3F2~ zPI07AQCvB2TeN|lOr4!+mq*c^| zpqkW@;y-Ams%b4<&1f0%AF@a_t7QeRSRK_)tyAz}tE-yRa)OUo-PLW{Ho-@&o@%ew zD|prFtLC-5;A3E?U+Wiq+}d8m4rl`~(5CG?F3|*Szb?^sLtXW04^eVXDkS;i zWROJpCGtA|l)N8Y|HLOrm0RuU5bnnDB)%>`yh4@hk`|<48i77Uqf~`np)n(DM7pGD znZ{|`b-xyEEug71NmHN^qiG`!(hSXlw2gMqP8dzlE}DbBopu`$u#ueh(`~c|G*YzJ zNW(}PM*3(TMl!UYZomGdq-E)j)?ze92SB`&J_O$)NNB`pVg z+s(i7706%sPXDPDS6!7FQaRR;n(;M>0N-r8+b3H6T&&qn12niMvoVk+anwyUq*Vg* z(hX@YJ(p>Qpii!mE%USAnXhlmr_woyKe)0#RPsX4*k;MN>X?cb5uyPSA7VkH56O4W_PlB1r=&gx^$({ECv{zf)3s z_Q42ye3LwZTmi@^G$#&3Au35iQ)Gqkua&_&e@l#$#FMD^6p(e;p~QN5UvlEem+INp z*77!NUW?uRn^?O{ z7%u;7^7vCbP&9yK50dRjb^;N4MSZch;Cd05a?WLjTbX4CKvwXxNBBr;cly4~9R%4V zZ=|k}CNIZ#@b z;YZLX|FmftUcg|i#_r22FJjenTAnrx>Z}nS7<)F3&=ZAr7@E_`&9MVyivMzKa3$}C z8oq{qRbCFb0UCCdMqrh!%6J>+!p%q{fcXuQ2uNULE{aI?Q_|A8AYJM9ORl;&Po(7_ zk(Lsm8Eg1%OElgH-j;suUxnEhq>FGvmV>VbuXMNitK@3Sesp9_KnP506AQ#P7T-g-h}`4vob;X(@aRlM(Lx0sh&c^icOX{Uw9u;li60%W#JCM<()i z?P?x^c)pRa5!J!n|ycB5b4AM``D_3+z9d?*MJ>K=$MbPj9 zb!OTw+YLO+;(*UGZ1xavTFikZH)cAXiudW}c&2L*)!LY0Q)APxQwYI@`Q!Mm^9;r8 zIVAVp9lU+NClIP(U;r2Zj1)NnO$C5ZWf}N5^xH^W&J*P|`5V=TP^HQVUY>aJIfNjH zmeeHTcxAvWA7U2b)Ne=$ZY+AubR@!u3UUv+G`m3XAv%KSqw;@E4DwsiJl}D+pi&qq z1Ux_0U7cSzT-bwY$qXdlN3wNGo?5FL_QnzYo$z-LzeS3C;>ggzy?OKBoyVSsv5(1} zn@3_Kfqcth%xA!ZcaJ_6Eg~y!{R#fxqut~Y9y*rKY+aq<20tNuwf z2Z7NC&3TW%ee6FgZnzPEBu+KLki@=KvK(ndnuMlB9!8q!jf`w&8l)MAVyvvmYh*d< zW*gB}LbLOJ_D&-T&y?)za+lQXSd(Z+hXnF4%9quK3f4M>HFd+98>U?#i#1|RAE7zk zes=lO12@<-AQX_|;iuPnzK@rGQ^HDsuby(KL=-pb>{kj88h zwx+3fM<%QKjIn+lhSo0u@gfVh`I5mL(_Lh}Fu*SHKb)A@g(p~$J$ZPa=D%74BU_Ku z3y;hgw()YEjfQz)vKaCLuIX9^Uz&WB|8{b_y#8u zZketZSg6+lA79|3r?w|wL~+1znOSdT%W+;ml_|9`k$#vI>VOlH?P`BL9V&;pZ6lMOW(|o2xDany7>S8Q0G?R7(cj1N?E0-&l?l=|WB0Qza-*WX))DgMY16&@OgW*>ss4YE8R2fim z(u$IiR+1qlrS!-BN>B+WlgUxVpG`y}NrmCO!XP`(zdF0LqP0yeo^kgNpK(iu3&ERb z?I33Opkq*DrB;ba1E(qQL=#VPmcidBbDXjRu`nx!D5P3)!5rSuDc?;g%-2?2MY?w3 z%-FpYcmZ;NZDtdjL2dN!TN@Tp5)h+|RBLp>GDhKUNKPJzA|;@fK28#9LR3^ySCJzL J;+NCD{{wj!Xn+6! diff --git a/ui/main_window.py b/ui/main_window.py index c83a007..f813cb5 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -12,7 +12,7 @@ QMessageBox, QPushButton, QVBoxLayout, - QWidget, QScrollArea, + QWidget, QScrollArea, QLineEdit, ) from PyQt6.QtGui import QDesktopServices, QPixmap from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest @@ -25,6 +25,7 @@ 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 ARTICLE_LINK_ROLE = 1000 @@ -41,6 +42,10 @@ def __init__(self): 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() @@ -66,10 +71,100 @@ def setup_ui(self): self.root_layout.setSpacing(20) self.create_header() + + self.content_layout = QHBoxLayout() + self.content_layout.setSpacing(20) + + self.create_sidebar() self.create_article_list() + self.content_layout.addWidget(self.sidebar) + self.content_layout.addWidget(self.scroll_area, stretch=1) + + self.root_layout.addLayout(self.content_layout) + self.setCentralWidget(self.root) + def update_source_list(self): + self.source_list.clear() + + all_item = QListWidgetItem("All") + all_item.setData(Qt.ItemDataRole.UserRole, {"university": "All", "source": "All"}) + self.source_list.addItem(all_item) + + universities = self.database.get_universities() + + for university in universities: + university_item = QListWidgetItem(university) + university_item.setData( + Qt.ItemDataRole.UserRole, + {"university": university, "source": "All"}, + ) + self.source_list.addItem(university_item) + + sources = self.database.get_sources_for_university(university) + + for source in sources: + if source == university: + continue + + source_item = QListWidgetItem(f" └ {source}") + source_item.setData( + Qt.ItemDataRole.UserRole, + {"university": university, "source": source}, + ) + self.source_list.addItem(source_item) + + self.source_list.setCurrentRow(0) + + def on_source_selected(self, item: QListWidgetItem): + data = item.data(Qt.ItemDataRole.UserRole) + + self.selected_university = data.get("university", "All") + self.selected_source = data.get("source", "All") + + self.load_filtered_articles() + + def on_search_changed(self, text: str): + self.search_text = text.strip() + self.load_filtered_articles() + + def load_filtered_articles(self): + self.articles = self.database.get_articles( + university=self.selected_university, + source=self.selected_source, + search_text=self.search_text, + limit=500, + ) + + self.render_articles() + + def create_sidebar(self): + self.sidebar = QFrame() + self.sidebar.setObjectName("Sidebar") + self.sidebar.setFixedWidth(230) + + sidebar_layout = QVBoxLayout(self.sidebar) + sidebar_layout.setContentsMargins(18, 18, 18, 18) + sidebar_layout.setSpacing(14) + + title = QLabel("Sources") + title.setObjectName("SidebarTitle") + + self.search_input = QLineEdit() + self.search_input.setObjectName("SearchInput") + self.search_input.setPlaceholderText("Search articles...") + self.search_input.textChanged.connect(self.on_search_changed) + + self.source_list = QListWidget() + self.source_list.setObjectName("SourceList") + self.source_list.itemClicked.connect(self.on_source_selected) + + sidebar_layout.addWidget(title) + sidebar_layout.addWidget(self.search_input) + sidebar_layout.addWidget(self.source_list) + sidebar_layout.addStretch() + def create_header(self): header_card = QFrame() header_card.setObjectName("HeaderCard") @@ -130,7 +225,6 @@ def create_article_list(self): self.article_layout.addStretch() self.scroll_area.setWidget(self.article_container) - self.root_layout.addWidget(self.scroll_area) def apply_light_theme(self): self.setStyleSheet( @@ -274,6 +368,54 @@ 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; + border: 1px solid #E9D5FF; + border-radius: 10px; + 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; + font-weight: 800; + } """ ) @@ -283,6 +425,53 @@ def apply_light_theme(self): def apply_dark_theme(self): self.setStyleSheet( """ + #Sidebar { + background-color: #0B1220; + border: 1px solid #1E293B; + border-radius: 22px; + } + + #SidebarTitle { + color: #FFFFFF; + font-size: 18px; + font-weight: 900; + } + + #SearchInput { + background-color: #111827; + color: #F8FAFC; + border: 1px solid #334155; + border-radius: 10px; + 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; + font-weight: 800; + } QMainWindow { background-color: #080A14; } @@ -449,11 +638,17 @@ def refresh_news(self): print("Fetching:", university_name, feed_url) try: - articles = fetch_feed(university_name, feed_url) + source_type = feed.get("type", "rss") + + if source_type == "scraper": + scraper_id = feed.get("scraper") + articles = run_scraper(scraper_id) + else: + articles = fetch_feed(university_name, feed_url) + print("Fetched articles:", len(articles)) self.database.save_articles(articles) - print("Saved articles. DB now has:", len(self.database.get_articles(limit=5000))) except Exception as error: print(f"Could not fetch {university_name}: {error}") @@ -463,12 +658,12 @@ def refresh_news(self): self.render_articles() - self.refresh_button.setEnabled(True) - self.refresh_button.setText("Refresh news") + self.update_source_list() + self.load_filtered_articles() def load_cached_articles(self): - self.articles = self.database.get_articles() - self.render_articles() + self.update_source_list() + self.load_filtered_articles() def open_article(self, item: QListWidgetItem): url = item.data(ARTICLE_LINK_ROLE) @@ -531,7 +726,10 @@ def create_article_card(self, article: dict) -> QFrame: published = article.get("published", "Unknown date") - meta = QLabel(f"{university} • {published}") + source = article.get("source", university) + published = article.get("published", "Unknown date") + + meta = QLabel(f"{university} • {source} • {published}") meta.setObjectName("ArticleMeta") meta.setWordWrap(True) From 7ec6fa2741ec58bec99eac3520d621784d5a6645 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Fri, 5 Jun 2026 16:57:21 +0200 Subject: [PATCH 4/7] Fixed problems regarding CI/CD --- .github/workflows/build.yml | 2 +- app_settings.py | 1 - database.py | 1 - ui/main_window.py | 3 --- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5eec734..6c76f58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: - name: Run Ruff Static Analysis run: | - ruff check . + ruff check . --fix - name: Install Native Build Tools run: | diff --git a/app_settings.py b/app_settings.py index d38355e..fbb624b 100644 --- a/app_settings.py +++ b/app_settings.py @@ -1,5 +1,4 @@ import json -from pathlib import Path from settings import APP_DATA_DIR diff --git a/database.py b/database.py index ae94b9f..186ce50 100644 --- a/database.py +++ b/database.py @@ -1,6 +1,5 @@ import sqlite3 from pathlib import Path -from typing import Optional import settings diff --git a/ui/main_window.py b/ui/main_window.py index f813cb5..9415db1 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -1,7 +1,6 @@ import json from PyQt6.QtCore import Qt, QUrl -from PyQt6.QtGui import QDesktopServices from PyQt6.QtWidgets import ( QFrame, QHBoxLayout, @@ -15,8 +14,6 @@ QWidget, QScrollArea, QLineEdit, ) from PyQt6.QtGui import QDesktopServices, QPixmap -from PyQt6.QtNetwork import QNetworkAccessManager, QNetworkRequest -from PyQt6.QtCore import QByteArray from database import Database from settings import FEEDS_FILE From 8d69b18b5f153e0ef8901b046765368eb2bb4f47 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Fri, 5 Jun 2026 17:01:29 +0200 Subject: [PATCH 5/7] Add setup.py for Debian package build --- setup.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 setup.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..7b52ccd --- /dev/null +++ b/setup.py @@ -0,0 +1,33 @@ +from setuptools import setup + + +setup( + name="uninews", + version="0.1.0", + py_modules=[ + "app_settings", + "database", + "main", + "notifications", + "rss_service", + "scraper_service", + "settings", + ], + packages=[ + "ui", + "scrapers", + ], + include_package_data=True, + install_requires=[ + "PyQt6", + "feedparser", + "requests", + "beautifulsoup4", + "plyer", + ], + entry_points={ + "console_scripts": [ + "uninews=main:main", + ], + }, +) \ No newline at end of file From d4366a7acaeea3b24f79877cde7e2b7fea51bfa7 Mon Sep 17 00:00:00 2001 From: George - David Apostolidis <66482153+GeorgeApos@users.noreply.github.com> Date: Fri, 5 Jun 2026 17:08:27 +0200 Subject: [PATCH 6/7] Enhance README with project details and instructions Added detailed project description, features, installation instructions, and contribution guidelines. --- README.md | 184 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 183 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 171c8b9..60a9dc1 100644 --- a/README.md +++ b/README.md @@ -1 +1,183 @@ -# UniNews \ No newline at end of file +# UniNews + +UniNews is a small desktop application that brings together news and announcements from universities into a single place. + +The project started as a simple way to avoid visiting multiple university websites every day just to check for updates. Instead of opening several tabs, RSS feeds, and department pages, UniNews collects everything into one application and presents it in a clean interface. + +The application is written in Python using PyQt6 and currently supports both RSS feeds and custom website scrapers. News articles are stored locally so that previously fetched content remains available even when a source is temporarily unavailable. + +## Features + +- Read news from multiple universities in one place +- Support for RSS feeds +- Support for custom HTML scrapers +- Local article caching using SQLite +- Search articles by keyword +- Filter articles by university +- Filter articles by specific source or department +- Light and dark themes +- Desktop notifications for new articles +- Configurable refresh intervals +- Local settings storage + +## Project Structure + +```text +UniNews/ +├── scrapers/ +├── ui/ +├── data/ +├── local_data/ +├── app_settings.py +├── database.py +├── main.py +├── notifications.py +├── rss_service.py +├── scraper_service.py +├── settings.py +└── requirements.txt +``` + +## Installation + +Clone the repository: + +```bash +git clone https://github.com/open-source-uom/UniNews.git +cd UniNews +``` + +Create a virtual environment: + +```bash +python -m venv .venv +``` + +Activate it: + +### Windows + +```powershell +.venv\Scripts\activate +``` + +### Linux + +```bash +source .venv/bin/activate +``` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +## Running the Application + +```bash +python main.py +``` + +## Adding a New RSS Feed + +Open `data/feeds.json` and add a new entry: + +```json +{ + "name": "MIT News", + "type": "rss", + "url": "https://news.mit.edu/rss/feed" +} +``` + +## Adding a New Scraper + +Create a new file inside the `scrapers` folder. + +Example: + +```python +from scrapers.base_scraper import BaseScraper + +class ExampleScraper(BaseScraper): + + def scrape(self): + return [] +``` + +Register it in `scraper_service.py`: + +```python +SCRAPER_REGISTRY = { + "example": ExampleScraper, +} +``` + +Then add it to `feeds.json`: + +```json +{ + "name": "Example University", + "type": "scraper", + "scraper": "example" +} +``` + +## Data Storage + +UniNews stores its local data inside: + +```text +local_data/ +``` + +This includes: + +- Cached articles +- Application settings +- SQLite database + +Deleting the database file will force the application to rebuild its cache the next time it runs. + +## Development + +Lint the code: + +```bash +ruff check . +``` + +Automatically fix common issues: + +```bash +ruff check . --fix +``` + +## Roadmap + +Planned improvements include: + +- More university sources +- Better image handling +- Pagination +- Source management from the UI +- Export and backup options +- Packaging for Linux distributions +- Automatic update checks + +## Contributing + +Contributions are welcome. + +The easiest way to contribute is by adding support for additional universities through RSS feeds or custom scrapers. + +Before opening a pull request: + +1. Make sure the application runs correctly. +2. Run the linter. +3. Keep changes focused and easy to review. + +## License + +This project is released under the GNU General Public License v3.0. From 153a3e8cc56ed7a4b6c5afab62c81e322e5b9eac Mon Sep 17 00:00:00 2001 From: George - David Apostolidis Date: Fri, 5 Jun 2026 17:23:36 +0200 Subject: [PATCH 7/7] Implemented Consistent Themes --- README.md | 184 +++++++++++++++++- __pycache__/app_settings.cpython-310.pyc | Bin 1246 -> 1210 bytes __pycache__/database.cpython-310.pyc | Bin 6411 -> 6372 bytes local_data/uninews.db | Bin 122880 -> 122880 bytes ui/__pycache__/main_window.cpython-310.pyc | Bin 21624 -> 25140 bytes .../settings_dialog.cpython-310.pyc | Bin 3626 -> 3667 bytes ui/main_window.py | 171 ++++++++++++++-- ui/settings_dialog.py | 1 + 8 files changed, 344 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 171c8b9..48752e7 100644 --- a/README.md +++ b/README.md @@ -1 +1,183 @@ -# UniNews \ No newline at end of file +# UniNews + +UniNews is a small desktop application that brings together news and announcements from universities into a single place. + +The project started as a simple way to avoid visiting multiple university websites every day just to check for updates. Instead of opening several tabs, RSS feeds, and department pages, UniNews collects everything into one application and presents it in a clean interface. + +The application is written in Python using PyQt6 and currently supports both RSS feeds and custom website scrapers. News articles are stored locally so that previously fetched content remains available even when a source is temporarily unavailable. + +## Features + +- Read news from multiple universities in one place +- Support for RSS feeds +- Support for custom HTML scrapers +- Local article caching using SQLite +- Search articles by keyword +- Filter articles by university +- Filter articles by specific source or department +- Light and dark themes +- Desktop notifications for new articles +- Configurable refresh intervals +- Local settings storage + +## Project Structure + +```text +UniNews/ +├── scrapers/ +├── ui/ +├── data/ +├── local_data/ +├── app_settings.py +├── database.py +├── main.py +├── notifications.py +├── rss_service.py +├── scraper_service.py +├── settings.py +└── requirements.txt +``` + +## Installation + +Clone the repository: + +```bash +git clone https://github.com/open-source-uom/UniNews.git +cd UniNews +``` + +Create a virtual environment: + +```bash +python -m venv .venv +``` + +Activate it: + +### Windows + +```powershell +.venv\Scripts\activate +``` + +### Linux + +```bash +source .venv/bin/activate +``` + +Install dependencies: + +```bash +pip install -r requirements.txt +``` + +## Running the Application + +```bash +python main.py +``` + +## Adding a New RSS Feed + +Open `data/feeds.json` and add a new entry: + +```json +{ + "name": "MIT News", + "type": "rss", + "url": "https://news.mit.edu/rss/feed" +} +``` + +## Adding a New Scraper + +Create a new file inside the `scrapers` folder. + +Example: + +```python +from scrapers.base_scraper import BaseScraper + +class ExampleScraper(BaseScraper): + + def scrape(self): + return [] +``` + +Register it in `scraper_service.py`: + +```python +SCRAPER_REGISTRY = { + "example": ExampleScraper, +} +``` + +Then add it to `feeds.json`: + +```json +{ + "name": "Example University", + "type": "scraper", + "scraper": "example" +} +``` + +## Data Storage + +UniNews stores its local data inside: + +```text +local_data/ +``` + +This includes: + +- Cached articles +- Application settings +- SQLite database + +Deleting the database file will force the application to rebuild its cache the next time it runs. + +## Development + +Lint the code: + +```bash +ruff check . +``` + +Automatically fix common issues: + +```bash +ruff check . --fix +``` + +## Roadmap + +Planned improvements include: + +- More university sources +- Better image handling +- Pagination +- Source management from the UI +- Export and backup options +- Packaging for Linux distributions +- Automatic update checks + +## Contributing + +Contributions are welcome. + +The easiest way to contribute is by adding support for additional universities through RSS feeds or custom scrapers. + +Before opening a pull request: + +1. Make sure the application runs correctly. +2. Run the linter. +3. Keep changes focused and easy to review. + +## License + +This project is released under the MIT License. \ No newline at end of file diff --git a/__pycache__/app_settings.cpython-310.pyc b/__pycache__/app_settings.cpython-310.pyc index ec2c91ef909860522b080053cdce5aa9c8bd683e..e1d214d1c47b81b52e3a301be3a61313093c22c4 100644 GIT binary patch delta 371 zcmYk0u};H442I9n<(h_G8(Kin(xnnenGqY}5m=BIA_Ss1MS-TRuI*eQ@dD750r3W` zJP1Pu7N))e3)mSDC;2=3x9|6TR^O|3Y;CCc+<*0M`;WHUKjVTF!B8b~;}CL85LAlf zbyVV!X4St@WL|+P6j1fqB$~q1A0Vw!?LjAoZ0b`;Fwj(|1}hbUQx!)m*>v*ua){7b ze&9K@mIvOR>9XqD%M0&mQIpg%R+S5Z>8rH$QD^Y_$j5DhMJ0A3+2`5Iji1TM(pmQX5Td*`_z47hgb&H&4ES zSC2kM4jzTRf)|~MATG=|-+VhW-@LgWu0QsDPtZI5>|WnJ_}%${3KR&bh&am>G8su= zZlzYHA_XE=sM;c`V1-nDCD48WbkIfbSw|YHu|1SnL;qey4%TBEs|Z7dD;SXCBAkhE zb;+)$Arz|D&qtJNhrQm#(d*NmgwEn!p2ON=%i7VK)Mna?6YFvAGp!RW6#`8x|kJ4mdBoCCEEN1L%ngryMT;`^{m-qeiDBakoaQ3q?&eHe*sG9OAj;KLRI+j!t NBxnl4ue#T2{s0c8PxAl( diff --git a/__pycache__/database.cpython-310.pyc b/__pycache__/database.cpython-310.pyc index 4c2f58c5ed5af7a18554c91da9545d404069dda4..2579ca999246fc7694a171b7f6e0f60704d8250e 100644 GIT binary patch delta 1150 zcmZ`&&rcIU6z=S9?WVT0Q0fweSU|<)hl+xNB8iHKmtyo_(o0=-2fAeF*0)mv-i#z3 zJQy-J5!);D6z~Efi?5hcB;h-uJ$5-?h@(2%TEx+wCfUUV6XwmJm%0 zDFt1V`OOHV#B*hq=(Qh;rp+W|E{M?;YF9Z84%yz)lFI?*3hCJj1&h00*VJlG`A zA`c+dfHX!)3GKCk*c)XEyFo@mpe|4j?J_deI_U^zqYtYTR+}0G3Ex_#MGLrVGde9Y z@y8?Gdzd{k2BbIhl6!gneCW+JVQ7oih?zRs{0NDe~XB^5X zm!Hj8gS;WGr0);*%4$=Aa5GVxc|JX;0gTDA&Dy8*GMTuDjO{s(qgD=~$!3<8=RL3+ zgDx^xuKJy*v~6|vVST3rQx9~<;?|rcRYx(~BOYW1ZV$^aNC7Q76dHD=NxTn1l;Z7Z z)@x^;WjJ`B>Y^TqUQ&8HfJ*QDeVT7?ioMm>&}3+0p67uHc+Znp%BLIV`w< zFoLkZMUcjwRLkx%1>fZ^^*@Ub*+F8|er8)qFK)Na_@DfDku|2eugkifxrUZQy**Om zk#U@S6EBRh%=XsWBqeWZ`FtKuia*9#+KO8EM8HJAiydfq8{q_os$1suL0`gSTq;~f zxE7wuGPr?%rx13P!zg@xsyv8^)sEr27@5)f+l^J^(`W7+D9DQ;;PF8n0)1l1bE=F^ Q5?SHv67)!d=t`UV56e9EIsgCw delta 1197 zcmZ`(O-vI(6yDjNb}22bEv4AVp;VCtL_ttQK%*ddX-zz6dLhg1NLRPrIy>d(=HJ1@ z6PYWC7bIe0qUll3dNMJ_(an6X$KAlm_?k?b|o+d*7S)X7Ez%^%b1cRw=}6vN`Y6oU&Um- zC4m=q^%cCR6KWYYo@amB9**@kF|*tzKyT$mntASW?M>)~bQX$(KA4t4cSKJdK2wm0 zv)alHP1%-9`F1NaTkL7a!o+^*>K#mtqHfz-s9t(EXdmU^wYyj+MsbniHA+r`JHyoI zK+nOBcwxTzz5TfNSU$SBpWbf@=>l#GD`8?AQ{D*MZjiTGHg;?1IFHsu;?GJkY{$ii zB_J1g*sI!hYz1Y`17o-Ilc1FWXcA=8@icnPZpXhK>xZQtAgG>d>QrCSxxhK_X4yhw z@cJk>y(qzD6O%?|ydF3Vo`T_MG^Yk{r|szGqM;i_Y&IzGYVQ-%sNZj=U&sgh$>Z9Ic`gbTkVRS2UnQpLFR E4=0!a<^TWy diff --git a/local_data/uninews.db b/local_data/uninews.db index 1b9d093fc2d056238e1f8c5165854d1503926358..42704b4814e0bb17315cb55c7650e98d5244b020 100644 GIT binary patch delta 30 lcmZoTz}|3xeS#Dd%cO}iPC#;F!aRE>mI+M`%N!UN005t<3MBvl delta 30 kcmZoTz}|3xeS#DdquN9nCm^{oVV*q`qe_#*G6%*50FBuQ*Z=?k diff --git a/ui/__pycache__/main_window.cpython-310.pyc b/ui/__pycache__/main_window.cpython-310.pyc index fc44c9ae7ad059b7f254e04c140c7de2c6822bcd..3339a1563df6ddb7f8dbe1f79443378a6b1ce22b 100644 GIT binary patch delta 6287 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 F{|ozO4gmlF delta 4388 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 delta 361 zcmcaCvr2|HpO=@50SNq`$!2ZZ$jivgxM4Ftb1Y*-miPjR8ip+Cg^Y|0H7pBc7BYZo zS)iC0R1CzH0<#$x$ks4HS#mYZP!>cF;{v%F7LZ5^LokD;^yCXH9~h%1A7ss7(v+WU z&NdmyInB0eatnJMqt9e!4*Sh69CH{MRVF{=lwwq!{D)JXO&#dIB8|z4T*7RcAg0!2 zD=rI0v&qF=DvZIKXK>{+G8Rqd;PJ{d0?8VK2on%t3L?xvggKDV+$z-}Hr hNgI$dDL5ewV(9{j{K-tbwT!xx8+fgOj@-jL9RQ?5RjdF2 diff --git a/ui/main_window.py b/ui/main_window.py index 9415db1..dcb9a1c 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -51,6 +51,8 @@ def __init__(self): if self.app_settings.get("refresh_on_startup", True): self.refresh_news() + else: + QTimer.singleShot(1500, self.refresh_news) self.refresh_timer = QTimer(self) self.refresh_timer.timeout.connect(self.refresh_news) @@ -58,8 +60,158 @@ def __init__(self): self.apply_theme() self.apply_refresh_timer() - if self.app_settings.get("refresh_on_startup", True): - self.refresh_news() + def get_theme_stylesheet(self, theme: str) -> str: + if theme == "dark": + bg = "#080A14" + panel = "#111827" + panel_2 = "#0B1220" + border = "#2A3144" + text = "#F8FAFC" + muted = "#CBD5E1" + accent = "#C084FC" + accent_hover = "#E879F9" + input_bg = "#111827" + card = "#111827" + card_hover = "#161F33" + image_bg = "#1F2937" + else: + bg = "#F4F0FA" + panel = "#FFFFFF" + panel_2 = "#FFFFFF" + border = "#E6DDF3" + text = "#171124" + muted = "#4B5563" + accent = "#A855F7" + accent_hover = "#9333EA" + input_bg = "#F9F5FF" + card = "#FFFFFF" + card_hover = "#FBF7FF" + image_bg = "#F3E8FF" + + return f""" + QMainWindow, QDialog {{ + background-color: {bg}; + }} + + QWidget {{ + font-family: "Segoe UI", "Inter", "Arial"; + color: {text}; + }} + + #HeaderCard, #Sidebar {{ + background-color: {panel}; + border: 1px solid {border}; + border-radius: 24px; + }} + + #TitleLabel {{ + color: {text}; + font-size: 34px; + font-weight: 900; + }} + + #SubtitleLabel, #ArticleMeta, #OpenArticleLabel {{ + color: {accent}; + font-weight: 700; + }} + + QPushButton {{ + background-color: {panel_2}; + color: {text}; + border: 1px solid {border}; + padding: 10px 18px; + border-radius: 11px; + font-size: 13px; + font-weight: 700; + }} + + QPushButton:hover {{ + background-color: {card_hover}; + border: 1px solid {accent}; + }} + + #RefreshButton, #SaveButton {{ + background-color: {accent}; + color: white; + border: none; + }} + + #RefreshButton:hover, #SaveButton:hover {{ + background-color: {accent_hover}; + }} + + QLineEdit, QComboBox, QSpinBox {{ + background-color: {input_bg}; + color: {text}; + border: 1px solid {border}; + border-radius: 10px; + padding: 8px 10px; + font-size: 13px; + }} + + QCheckBox {{ + color: {text}; + spacing: 8px; + }} + + QScrollArea, QScrollArea > QWidget > QWidget, #ArticleContainer {{ + background-color: {bg}; + border: none; + }} + + #ArticleCard {{ + background-color: {card}; + border: 1px solid {border}; + border-radius: 24px; + }} + + #ArticleCard:hover {{ + background-color: {card_hover}; + border: 1px solid {accent}; + }} + + #ArticleImage {{ + background-color: {image_bg}; + border: 1px solid {border}; + border-radius: 18px; + color: {accent}; + font-size: 24px; + font-weight: 900; + }} + + #ArticleTitle, #SidebarTitle {{ + color: {text}; + font-weight: 900; + }} + + #ArticleSummary {{ + color: {muted}; + font-size: 13px; + }} + + #SourceList {{ + background-color: transparent; + border: none; + outline: none; + }} + + #SourceList::item {{ + color: {muted}; + padding: 10px 12px; + border-radius: 10px; + }} + + #SourceList::item:hover {{ + background-color: {card_hover}; + color: {text}; + }} + + #SourceList::item:selected {{ + background-color: {accent}; + color: white; + font-weight: 800; + }} + """ def setup_ui(self): self.root = QWidget() @@ -626,13 +778,11 @@ def refresh_news(self): self.refresh_button.setText("Refreshing...") feeds = self.load_feeds() - print("Feeds loaded:", feeds) for feed in feeds: university_name = feed.get("name", "Unknown university") feed_url = feed.get("url", "") - print("Fetching:", university_name, feed_url) try: source_type = feed.get("type", "rss") @@ -643,8 +793,6 @@ def refresh_news(self): else: articles = fetch_feed(university_name, feed_url) - print("Fetched articles:", len(articles)) - self.database.save_articles(articles) except Exception as error: @@ -766,6 +914,7 @@ 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"))) if dialog.exec(): self.app_settings = load_app_settings() @@ -800,11 +949,11 @@ def apply_refresh_timer(self) -> None: def apply_theme(self) -> None: theme = self.app_settings.get("theme", "light") + stylesheet = self.get_theme_stylesheet(theme) + self.setStyleSheet(stylesheet) - if theme == "dark": - self.apply_dark_theme() - else: - self.apply_light_theme() - + if hasattr(self, "article_container"): + bg = "#080A14" if theme == "dark" else "#F4F0FA" + self.article_container.setStyleSheet(f"background-color: {bg};") diff --git a/ui/settings_dialog.py b/ui/settings_dialog.py index d84c03e..712428b 100644 --- a/ui/settings_dialog.py +++ b/ui/settings_dialog.py @@ -69,6 +69,7 @@ def setup_ui(self): self.cancel_button.clicked.connect(self.reject) self.save_button = QPushButton("Save") + self.save_button.setObjectName("SaveButton") self.save_button.clicked.connect(self.save_settings) button_layout.addStretch()