diff --git a/README.md b/README.md index 50071e8..6321a8f 100644 --- a/README.md +++ b/README.md @@ -24,15 +24,21 @@ python src/unidesk/main.py ``` src/unidesk/ - main.py entry point - home.py main window and all navigation logic - pages.py text content for each informational page - credits.py list of contributors - links.py external links shown in the Links page + main.py entry point + home.py main window and all navigation logic + pages.py text content for each informational page + credits.py list of contributors + links.py external links shown in the Links page + academic_institutions.py known universities and their departments + academic_config.py reads/writes the shared academic profile ``` To update any page content just open `pages.py` and edit the body text for that page. To add a new contributor open `credits.py`. +## Academic profile + +The footer on the home screen has a **Configure UniOS** button. It opens a page where you pick your university and department from dropdowns, which is saved to `~/.unios/academicConfig.json`. Other UniOS apps (such as UniBackpack) read this file, so the available choices live in `academic_institutions.py` and must stay in sync with those apps. If UniBackpack adds new universities or departments, mirror them in `academic_institutions.py`. + ## Contributing Contributions are welcome. If you want to improve the UI, fix a typo, or add a new page, open a pull request on GitHub. If you find a bug, open an issue. There is no contribution too small. diff --git a/src/unidesk/academic_config.py b/src/unidesk/academic_config.py new file mode 100644 index 0000000..d4bd273 --- /dev/null +++ b/src/unidesk/academic_config.py @@ -0,0 +1,37 @@ +import json +import os + +# Where the shared UniOS academic profile is persisted. Other UniOS apps read +# this same file, so the location and key names below must not change. +CONFIG_DIR = os.path.join(os.path.expanduser("~"), ".unios") +CONFIG_PATH = os.path.join(CONFIG_DIR, "academicConfig.json") + + +def load_academic_config(): + """Return the saved academic profile as a dict. + + Always returns {"universityName": str, "departmentName": str}. A missing, + corrupt, or partial file yields empty strings instead of raising. + """ + config = {"universityName": "", "departmentName": ""} + try: + with open(CONFIG_PATH, encoding="utf-8") as f: + data = json.load(f) + except (FileNotFoundError, json.JSONDecodeError, OSError): + return config + + if isinstance(data, dict): + config["universityName"] = data.get("universityName", "") or "" + config["departmentName"] = data.get("departmentName", "") or "" + return config + + +def save_academic_config(university, department): + """Persist the academic profile to ~/.unios/academicConfig.json.""" + os.makedirs(CONFIG_DIR, exist_ok=True) + data = { + "universityName": university, + "departmentName": department, + } + with open(CONFIG_PATH, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) diff --git a/src/unidesk/academic_institutions.py b/src/unidesk/academic_institutions.py new file mode 100644 index 0000000..0bf4baa --- /dev/null +++ b/src/unidesk/academic_institutions.py @@ -0,0 +1,9 @@ +# Academic institutions known to UniOS. +# +# IMPORTANT: This list must stay in sync with UniBackpack's hardcoded values +# (UniBackpack/src/MainWindow.cpp). +UNIVERSITIES = { + "Aristotle University of Thessaloniki": ["Informatics", "Physics"], + "University of Western Macedonia": ["Informatics", "Mechanical Engineering"], + "University of Macedonia": ["Applied Informatics", "Economics"], +} diff --git a/src/unidesk/home.py b/src/unidesk/home.py index e4b052e..56ba648 100644 --- a/src/unidesk/home.py +++ b/src/unidesk/home.py @@ -2,19 +2,33 @@ import sys import tempfile +sys.path.insert(0, os.path.dirname(__file__)) + from PyQt6.QtWidgets import ( - QWidget, QVBoxLayout, QHBoxLayout, QLabel, - QFrame, QPushButton, QStackedWidget, QScrollArea, - QApplication, QMainWindow, QCheckBox + QWidget, + QVBoxLayout, + QHBoxLayout, + QLabel, + QFrame, + QPushButton, + QStackedWidget, + QScrollArea, + QApplication, + QMainWindow, + QCheckBox, + QComboBox, ) from PyQt6.QtCore import Qt, QUrl from PyQt6.QtGui import QDesktopServices from pages import PAGES from credits import CREDITS from links import LINKS +from academic_config import load_academic_config, save_academic_config +from academic_institutions import UNIVERSITIES AUTOSTART_PATH = os.path.expanduser("~/.config/autostart/unidesk.desktop") + def _is_autostart_disabled(): if not os.path.exists(AUTOSTART_PATH): return False @@ -24,16 +38,20 @@ def _is_autostart_disabled(): FOOTER_LINKS = [ {"label": "Discord", "url": "https://discord.gg/QA9AxTdppX"}, - {"label": "GitHub", "url": "https://github.com/open-source-uom"}, - {"label": "Website", "url": "https://open-source-uom.github.io/UniOS-landing-page/"}, + {"label": "GitHub", "url": "https://github.com/open-source-uom"}, + { + "label": "Website", + "url": "https://open-source-uom.github.io/UniOS-landing-page/", + }, ] -NAV_LEFT = ["Introduction", "Features", "Links", "FAQ"] +NAV_LEFT = ["Introduction", "Features", "Links", "FAQ"] NAV_RIGHT = ["Community", "Source Code", "Contribute", "Credits"] # Helpers + def _qlabel(text, size=12, color="#cdd6f4", bold=False, wrap=False): lbl = QLabel(text) lbl.setStyleSheet( @@ -111,7 +129,7 @@ def _scroll_page(on_back, title): return widget, cl -def _footer(): +def _footer(on_configure=None): footer = QWidget() footer.setFixedHeight(40) footer.setStyleSheet("background: #110d1a;") @@ -119,6 +137,16 @@ def _footer(): ft.setContentsMargins(14, 0, 14, 0) ft.addWidget(_qlabel("Open Source UoM · 2026", size=11, color="#585b70")) ft.addStretch() + + if on_configure is not None: + cfg = QPushButton("Configure UniOS") + cfg.setStyleSheet( + "background: transparent; border: none; color: #8b5897; font-size: 11px;" + ) + cfg.setCursor(Qt.CursorShape.PointingHandCursor) + cfg.clicked.connect(lambda _: on_configure()) + ft.addWidget(cfg) + for link in FOOTER_LINKS: b = QPushButton(link["label"]) b.setStyleSheet( @@ -132,6 +160,7 @@ def _footer(): # Page Builders + def build_text_page(key, on_back): data = PAGES[key] widget, cl = _scroll_page(on_back, key) @@ -158,16 +187,24 @@ def build_credits_page(on_back): fl.setSpacing(2) name = _qlabel(person["name"], size=13, color="#cdd6f4", bold=True) - name.setStyleSheet(name.styleSheet() + " background: transparent; border: none;") + name.setStyleSheet( + name.styleSheet() + " background: transparent; border: none;" + ) fl.addWidget(name) role = _qlabel(person["role"], size=11, color="#a6adc8") - role.setStyleSheet(role.styleSheet() + " background: transparent; border: none;") + role.setStyleSheet( + role.styleSheet() + " background: transparent; border: none;" + ) fl.addWidget(role) if person.get("projects"): - proj = _qlabel("Projects: " + ", ".join(person["projects"]), size=11, color="#8b5897") - proj.setStyleSheet(proj.styleSheet() + " background: transparent; border: none;") + proj = _qlabel( + "Projects: " + ", ".join(person["projects"]), size=11, color="#8b5897" + ) + proj.setStyleSheet( + proj.styleSheet() + " background: transparent; border: none;" + ) fl.addWidget(proj) cl.addWidget(frame) @@ -178,7 +215,7 @@ def build_credits_page(on_back): def build_links_page(on_back): widget, cl = _scroll_page(on_back, "Links") - + for link in LINKS: frame = QFrame() frame.setFrameShape(QFrame.Shape.StyledPanel) @@ -190,10 +227,13 @@ def build_links_page(on_back): fl.setSpacing(6) name = _qlabel(link["label"], size=13, color="#cdd6f4", bold=True) - name.setStyleSheet(name.styleSheet() + " background: transparent; border: none;") + name.setStyleSheet( + name.styleSheet() + " background: transparent; border: none;" + ) fl.addWidget(name) btn = QPushButton("Open Link") + btn.setCursor(Qt.CursorShape.PointingHandCursor) btn.setStyleSheet( "QPushButton { background-color: #89b4fa; color: #1e1e2e; font-weight: bold; " "border: none; border-radius: 4px; padding: 5px 10px; font-size: 11px; }" @@ -208,12 +248,97 @@ def build_links_page(on_back): return widget +def build_academic_config_page(on_back): + widget, cl = _scroll_page(on_back, "Configure UniOS") + + intro = _qlabel( + "Set your university and department. This profile is shared with other " + "UniOS apps, so pick the entries that match your studies.", + size=12, + color="#a6adc8", + wrap=True, + ) + cl.addWidget(intro) + + combo_style = ( + "QComboBox { background-color: #2d1f3d; color: #cdd6f4; border: 1px solid #8b5897; " + "border-radius: 4px; padding: 6px 8px; font-size: 12px; }" + "QComboBox QAbstractItemView { background-color: #2d1f3d; color: #cdd6f4; " + "selection-background-color: #3d2a52; }" + ) + + cl.addWidget(_qlabel("University", size=12, color="#cdd6f4", bold=True)) + university_combo = QComboBox() + university_combo.setStyleSheet(combo_style) + university_combo.setPlaceholderText("Select university") + university_combo.addItems(list(UNIVERSITIES.keys())) + university_combo.setCurrentIndex(-1) + cl.addWidget(university_combo) + + cl.addWidget(_qlabel("Department", size=12, color="#cdd6f4", bold=True)) + department_combo = QComboBox() + department_combo.setStyleSheet(combo_style) + department_combo.setPlaceholderText("Select department") + department_combo.setCurrentIndex(-1) + cl.addWidget(department_combo) + + status = _qlabel("", size=11, color="#a6adc8", wrap=True) + + def refresh_departments(): + university = university_combo.currentText() + department_combo.clear() + if university in UNIVERSITIES: + department_combo.addItems(UNIVERSITIES[university]) + department_combo.setCurrentIndex(-1) + + university_combo.currentIndexChanged.connect(lambda _: refresh_departments()) + + # Pre-fill from any existing config, but only if the saved values are still + # known to us (UniBackpack may have entries we haven't mirrored yet). + saved = load_academic_config() + saved_university = saved["universityName"] + saved_department = saved["departmentName"] + if saved_university in UNIVERSITIES: + university_combo.setCurrentText(saved_university) + if saved_department in UNIVERSITIES[saved_university]: + department_combo.setCurrentText(saved_department) + + save_btn = QPushButton("Save") + save_btn.setCursor(Qt.CursorShape.PointingHandCursor) + save_btn.setStyleSheet( + "QPushButton { background-color: #89b4fa; color: #1e1e2e; font-weight: bold; " + "border: none; border-radius: 4px; padding: 6px 12px; font-size: 12px; }" + "QPushButton:hover { background-color: #b4befe; }" + ) + + def on_save(): + university = university_combo.currentText() + department = department_combo.currentText() + if not university or not department: + status.setText("Please select a university and department.") + status.setStyleSheet(status.styleSheet().replace("#a6adc8", "#f38ba8")) + return + save_academic_config(university, department) + status.setText(f"Saved: {university} · {department}") + status.setStyleSheet(status.styleSheet().replace("#f38ba8", "#a6adc8")) + + save_btn.clicked.connect(lambda _: on_save()) + + cl.addSpacing(6) + cl.addWidget(save_btn) + cl.addWidget(status) + cl.addStretch() + return widget + + # Nav button + class NavButton(QPushButton): def __init__(self, label, align_right=False): super().__init__(label) self.setFixedHeight(38) + self.setCursor(Qt.CursorShape.PointingHandCursor) self.setStyleSheet(""" QPushButton {{ @@ -239,6 +364,7 @@ def __init__(self, label, align_right=False): # Main Window + class UniOSWelcome(QMainWindow): def __init__(self): super().__init__() @@ -273,7 +399,8 @@ def _build_main(self): hero_sub = _qlabel( "A custom Linux distribution for the modern Greek university", - size=12, color="#a6adc8" + size=12, + color="#a6adc8", ) hero_sub.setAlignment(Qt.AlignmentFlag.AlignCenter) hl.addWidget(hero_sub) @@ -296,8 +423,8 @@ def _build_main(self): left_col.addWidget(btn) left_col.addStretch() - center_col = QVBoxLayout() - center_col.setAlignment(Qt.AlignmentFlag.AlignCenter) + bottom_row = QHBoxLayout() + bottom_row.setContentsMargins(28, 0, 28, 14) self._autostart_cb = QCheckBox("Auto start") self._autostart_cb.setChecked(True) _svg = b"" @@ -306,7 +433,8 @@ def _build_main(self): _f.flush() _check_path = _f.name - self._autostart_cb.setStyleSheet(""" + self._autostart_cb.setStyleSheet( + """ QCheckBox { color: #a6adc8; font-size: 12px; @@ -331,12 +459,31 @@ def _build_main(self): QCheckBox::indicator:checked:hover { background-color: #9b68a7; } - """ % _check_path) + """ + % _check_path + ) self._autostart_cb.stateChanged.connect(self._toggle_autostart) - center_col.addStretch() - center_col.addWidget(self._autostart_cb, alignment=Qt.AlignmentFlag.AlignHCenter) - center_col.addStretch() + cfg_btn = QPushButton("Configure UniOS") + cfg_btn = QPushButton("Configure UniOS") + cfg_btn.setFixedHeight(28) + cfg_btn.setStyleSheet(""" + QPushButton { + background-color: #2d1f3d; + border: 1px solid #8b5897; + border-radius: 5px; + color: #cdd6f4; + font-size: 11px; + font-weight: bold; + padding: 0 12px; + } + """) + cfg_btn.setCursor(Qt.CursorShape.PointingHandCursor) + cfg_btn.clicked.connect(self._show_academic_config) + + bottom_row.addWidget(self._autostart_cb) + bottom_row.addStretch() + bottom_row.addWidget(cfg_btn) right_col = QVBoxLayout() right_col.setSpacing(10) @@ -348,19 +495,19 @@ def _build_main(self): nav_layout.addLayout(left_col) nav_layout.addLayout(right_col) - left_col.addWidget(self._autostart_cb) mp.addWidget(nav_widget, stretch=1) + mp.addLayout(bottom_row) mp.addWidget(_divider()) mp.addWidget(_footer()) - self._stack.addWidget(main_page) # index 0 def _build_subpages(self): subpages = { **{key: build_text_page(key, self._show_main) for key in PAGES}, "Credits": build_credits_page(self._show_main), - "Links": build_links_page(self._show_main), + "Links": build_links_page(self._show_main), + "Configure UniOS": build_academic_config_page(self._show_main), } for key, widget in subpages.items(): self._page_indices[key] = self._stack.addWidget(widget) @@ -368,7 +515,7 @@ def _build_subpages(self): def _toggle_autostart(self, state): if self._autostart_cb.isChecked(): if os.path.exists(AUTOSTART_PATH): - os.remove(AUTOSTART_PATH) + os.remove(AUTOSTART_PATH) else: os.makedirs(os.path.dirname(AUTOSTART_PATH), exist_ok=True) with open(AUTOSTART_PATH, "w") as f: @@ -377,13 +524,16 @@ def _toggle_autostart(self, state): "Exec=unidesk\nIcon=unios\nTerminal=false\n" "X-KDE-autostart-condition=false\nHidden=true\n" ) - + def _show_page(self, key): self._stack.setCurrentIndex(self._page_indices[key]) def _show_main(self): self._stack.setCurrentIndex(0) + def _show_academic_config(self): + self._stack.setCurrentIndex(self._page_indices["Configure UniOS"]) + def main(): app = QApplication(sys.argv) diff --git a/src/unidesk/main.py b/src/unidesk/main.py index 999d418..cc3228a 100644 --- a/src/unidesk/main.py +++ b/src/unidesk/main.py @@ -2,7 +2,7 @@ import sys from PyQt6.QtGui import QIcon from PyQt6.QtWidgets import QApplication -from home import UniOSWelcome +from .home import UniOSWelcome def main():