diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c770dad..7d10313 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,7 +24,7 @@ jobs: sudo apt-get update sudo apt-get install -y \ build-essential cmake \ - qt6-base-dev qt6-tools-dev-tools \ + qt6-base-dev qt6-tools-dev qt6-tools-dev-tools \ libqt6widgets6 \ cppcheck \ dpkg-dev diff --git a/.gitignore b/.gitignore index f433558..700c8d1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # .idea .idea/ +.vscode/ # Executable UniBackpack diff --git a/CMakeLists.txt b/CMakeLists.txt index 77ad2ee..f519f6e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(UniBackpack VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) -find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets Network LinguistTools) set(CMAKE_AUTOMOC ON) # Handles Signals/Slots set(CMAKE_AUTORCC ON) # Handles .qrc resource files @@ -38,6 +38,15 @@ target_link_libraries(UniBackpack PRIVATE Qt6::Network ) +# --- Internationalization (i18n) --- +# Compiles .ts -> .qm and embeds them under ":/i18n/" automatically. +# Run "cmake --build build --target update_translations" to populate .ts from tr() strings. +qt_add_translations(UniBackpack + TS_FILES + translations/unibackpack_en.ts + translations/unibackpack_el.ts +) + install(TARGETS UniBackpack DESTINATION bin) # CPack configuration to generate the Debian installer (.deb) diff --git a/README.md b/README.md index 3c5f391..f6924ac 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ sudo pacman -S base-devel cmake qt6-base qt6-tools polkit **For Debian / Ubuntu / Linux Mint:** ```bash -sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev-tools policykit-1 +sudo apt install build-essential cmake qt6-base-dev qt6-tools-dev qt6-tools-dev-tools policykit-1 ``` ## Project Structure diff --git a/include/Downloader.hpp b/include/Downloader.hpp index 1111292..2c3e7f7 100644 --- a/include/Downloader.hpp +++ b/include/Downloader.hpp @@ -29,6 +29,7 @@ class Downloader : public QObject { signals: void progress_updated(int percentage); void status_message(const QString &message); + void status_update(const QString &message); void download_completed(bool success); }; diff --git a/include/MainWindow.hpp b/include/MainWindow.hpp index 7e50fe1..b16c226 100644 --- a/include/MainWindow.hpp +++ b/include/MainWindow.hpp @@ -6,6 +6,9 @@ #include #include #include +#include + +void applyTranslator(const QString &locale); QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } @@ -18,6 +21,10 @@ class MainWindow : public QMainWindow public: MainWindow(QWidget *parent = nullptr); ~MainWindow(); + void retranslate(); + + protected: + void changeEvent(QEvent *event) override; private slots: void on_university_selection(const QModelIndex &index); diff --git a/src/Downloader.cpp b/src/Downloader.cpp index 4a2fe3b..2b142c6 100644 --- a/src/Downloader.cpp +++ b/src/Downloader.cpp @@ -28,11 +28,15 @@ bool Downloader::is_in_pacman_repo(const QString &package_name) { process.start("pacman", QStringList() << "-Si" << package_name); process.waitForFinished(); if (process.exitCode() == 0) { - emit status_message("Found: " + package_name); + QString msg = tr("Found: %1").arg(package_name); + emit status_message(msg); + emit status_update(msg); QCoreApplication::processEvents(); return true; } else { - emit status_message("Not in repos: " + package_name); + QString msg = tr("Not in repos: %1").arg(package_name); + emit status_message(msg); + emit status_update(msg); QCoreApplication::processEvents(); return false; } @@ -46,11 +50,15 @@ bool Downloader::is_in_apt_repo(const QString &package_name) { QString stdout_output = process.readAllStandardOutput(); if (process.exitCode() == 0 && stdout_output.contains("Package:")) { - emit status_message("Found: " + package_name); + QString msg = tr("Found: %1").arg(package_name); + emit status_message(msg); + emit status_update(msg); QCoreApplication::processEvents(); return true; } else { - emit status_message("Not in repos: " + package_name); + QString msg = tr("Not in repos: %1").arg(package_name); + emit status_message(msg); + emit status_update(msg); QCoreApplication::processEvents(); return false; } @@ -62,7 +70,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString if (standard_package_manager && package_manager == "pacman") { QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/pacman_list.txt"; - emit status_message("Checking packages for " + name_of_department + "..."); + QString msg = tr("Checking packages for %1...").arg(name_of_department); + emit status_message(msg); + emit status_update(msg); QFile file(filepath_of_list); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Critical Error: Could not open the file!" << file.errorString(); @@ -72,7 +82,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString while (!in.atEnd()) { QString package = in.readLine().trimmed(); if (!package.isEmpty() && is_in_pacman_repo(package)) { - emit status_message("Adding: " + package); + QString addMsg = tr("Adding: %1").arg(package); + emit status_message(addMsg); + emit status_update(addMsg); installable_with_standard_package_manager.append(package); } } @@ -82,7 +94,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString if (!standard_package_manager && package_manager == "pacman") { QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/pacman_list.txt"; - emit status_message("Checking packages for " + name_of_department + "..."); + QString msg = tr("Checking packages for %1...").arg(name_of_department); + emit status_message(msg); + emit status_update(msg); QFile file(filepath_of_list); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Critical Error: Could not open the file!" << file.errorString(); @@ -92,7 +106,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString while (!in.atEnd()) { QString package = in.readLine().trimmed(); if (!package.isEmpty() && !is_in_pacman_repo(package)) { - emit status_message("Adding (non-standard): " + package); + QString addMsg = tr("Adding (non-standard): %1").arg(package); + emit status_message(addMsg); + emit status_update(addMsg); installable_with_non_standard_package_manager.append(package); } } @@ -102,7 +118,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString if (standard_package_manager && package_manager == "apt") { QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/apt_list.txt"; - emit status_message("Checking packages for " + name_of_department + "..."); + QString msg = tr("Checking packages for %1...").arg(name_of_department); + emit status_message(msg); + emit status_update(msg); QFile file(filepath_of_list); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Critical Error: Could not open the file!" << file.errorString(); @@ -112,7 +130,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString while (!in.atEnd()) { QString package = in.readLine().trimmed(); if (!package.isEmpty() && is_in_apt_repo(package)) { - emit status_message("Adding: " + package); + QString addMsg = tr("Adding: %1").arg(package); + emit status_message(addMsg); + emit status_update(addMsg); installable_with_standard_package_manager.append(package); } } @@ -122,7 +142,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString if (!standard_package_manager && package_manager == "apt") { QString filepath_of_list = ":/lists/" + name_of_university + "/" + name_of_department + "/apt_list.txt"; - emit status_message("Checking packages for " + name_of_department + "..."); + QString msg = tr("Checking packages for %1...").arg(name_of_department); +emit status_message(msg); +emit status_update(msg); QFile file(filepath_of_list); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { qDebug() << "Critical Error: Could not open the file!" << file.errorString(); @@ -132,7 +154,9 @@ QStringList Downloader::read_package_list(bool standard_package_manager, QString while (!in.atEnd()) { QString package = in.readLine().trimmed(); if (!package.isEmpty() && !is_in_apt_repo(package)) { - emit status_message("Adding (non-standard): " + package); + QString addMsg = tr("Adding (non-standard): %1").arg(package); + emit status_message(addMsg); + emit status_update(addMsg);; installable_with_non_standard_package_manager.append(package); } } @@ -148,9 +172,8 @@ void Downloader::download_via_pacman(const QStringList &list_to_be_downloaded) { // there is a problem with pacman -Syu though, so the user should be alerted about that QMessageBox::warning( nullptr, - "Warning", - "It is advised to update your system before proceeding." - "\nRun sudo pacman -Syu" + tr("Warning"), + tr("It is advised to update your system before proceeding.\nRun sudo pacman -Syu") ); if (list_to_be_downloaded.isEmpty()) { diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 5c1d25e..babde77 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,18 +1,39 @@ // Author: Apostolos Chalis 2026 // Co-Author: Ioannis Michadasis - #include "ui_MainWindow.h" #include "MainWindow.hpp" #include "Downloader.hpp" +#include +#include +#include #include #include #include #include +void applyTranslator(const QString &locale) { + static QTranslator elTranslator; + static bool installed = false; + + if (installed) { + QCoreApplication::removeTranslator(&elTranslator); + installed = false; + } + + if (locale.startsWith("el")) { + if (elTranslator.isEmpty()) // if no translation is found, the original English text is shown + elTranslator.load(":/i18n/unibackpack_el.qm"); + if (!elTranslator.isEmpty()) { + QCoreApplication::installTranslator(&elTranslator); + installed = true; + } + } +} + MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { - + ui->setupUi(this); university_model = new QStandardItemModel(this); @@ -24,8 +45,9 @@ MainWindow::MainWindow(QWidget *parent) {"University of Macedonia", ":/icons/uom_logo.png"} }; - for (const auto &[name, iconPath] : universities) { - QStandardItem *item = new QStandardItem(QIcon(iconPath), name); + for (const auto &[key, iconPath] : universities) { + QStandardItem *item = new QStandardItem(QIcon(iconPath), key); + item->setData(key, Qt::UserRole); university_model->appendRow(item); } @@ -39,6 +61,33 @@ MainWindow::MainWindow(QWidget *parent) connect(ui->listView, &QListView::clicked, this, &MainWindow::on_university_selection); connect(ui->showMoreButton, &QPushButton::clicked, this, &MainWindow::toggle_output); + + // Language selector in status bar + QComboBox *langCombo = new QComboBox(this); + langCombo->addItem("English", "en_US"); + langCombo->addItem("Ελληνικά", "el_GR"); + + QSettings settings; + QString currentLang = settings.value("language", "en_US").toString(); + int idx = langCombo->findData(currentLang); + if (idx >= 0) { + langCombo->blockSignals(true); + langCombo->setCurrentIndex(idx); + langCombo->blockSignals(false); + } + + statusBar()->addPermanentWidget(langCombo); + + connect(langCombo, &QComboBox::currentIndexChanged, this, [=](int index) { + QString lang = langCombo->itemData(index).toString(); + QSettings s; + s.setValue("language", lang); + applyTranslator(lang); + QEvent ev(QEvent::LanguageChange); + QCoreApplication::sendEvent(this, &ev); + }); + + retranslate(); } MainWindow::~MainWindow() { @@ -53,10 +102,9 @@ void MainWindow::toggle_output() { void MainWindow::on_university_selection(const QModelIndex &index) { if (showing_universities) { - current_university = university_model->data(index, Qt::DisplayRole).toString(); - + current_university = university_model->data(index, Qt::UserRole).toString(); + QStringList departments; - departments << "Back to Universities"; if (current_university == "Aristotle University of Thessaloniki") { departments << "Informatics" << "Physics"; @@ -67,17 +115,24 @@ void MainWindow::on_university_selection(const QModelIndex &index) { } department_model->clear(); + + QStandardItem *backItem = new QStandardItem(tr("Back to Universities")); + backItem->setData("__back__", Qt::UserRole); + department_model->appendRow(backItem); + for (const QString &dept : departments) { - department_model->appendRow(new QStandardItem(dept)); + QStandardItem *item = new QStandardItem(tr(dept.toUtf8().constData())); + item->setData(dept, Qt::UserRole); + department_model->appendRow(item); } - + ui->listView->setModel(department_model); showing_universities = false; } else { - QString selectedDept = department_model->data(index, Qt::DisplayRole).toString(); - - if (selectedDept == "Back to Universities") { + QString selectedDept = department_model->data(index, Qt::UserRole).toString(); + + if (selectedDept == "__back__") { ui->listView->setModel(university_model); showing_universities = true; return; @@ -98,16 +153,8 @@ void MainWindow::on_university_selection(const QModelIndex &index) { ui->statusLabel->setVisible(true); ui->showMoreButton->setVisible(true); - // Connect signals connect(downloader, &Downloader::status_message, this, [=](const QString &msg) { ui->outputView->append(msg); - - if (msg.startsWith("Found:") || msg.startsWith("Adding:") || - msg.startsWith("Checking") || msg.startsWith("Not found:") || - msg.startsWith("Adding (non-standard):")) { - ui->statusLabel->setText(msg.trimmed()); - } - for (const QString &line : msg.split('\n')) { if (line.startsWith("dlstatus:") || line.startsWith("pmstatus:")) { QStringList parts = line.split(':'); @@ -123,11 +170,14 @@ void MainWindow::on_university_selection(const QModelIndex &index) { } }); + connect(downloader, &Downloader::status_update, this, [=](const QString &msg) { + ui->statusLabel->setText(msg.trimmed()); + }); + connect(downloader, &Downloader::download_completed, this, [=](bool success) { ui->listView->setEnabled(true); ui->progressBar->setMaximum(100); ui->progressBar->setValue(100); - if (success) { ui->statusLabel->setText("✓ Finished!"); ui->progressBar->setFormat("Done!"); @@ -145,10 +195,8 @@ void MainWindow::on_university_selection(const QModelIndex &index) { ui->progressBar->setValue(percent); }); - // Read packages QStringList packages_to_download = downloader->read_package_list(true, package_manager); - // Start download if (package_manager == "pacman") { downloader->download_via_pacman(packages_to_download); } else if (package_manager == "apt") { @@ -159,4 +207,36 @@ void MainWindow::on_university_selection(const QModelIndex &index) { ui->statusLabel->setVisible(true); } } +} + +void MainWindow::retranslate() { + for (int i = 0; i < university_model->rowCount(); ++i) { + QStandardItem *item = university_model->item(i); + QString key = item->data(Qt::UserRole).toString(); + QString translated = tr(key.toUtf8().constData()); + item->setText(translated.isEmpty() ? key : translated); + } + + if (!showing_universities) { + for (int i = 0; i < department_model->rowCount(); ++i) { + QStandardItem *item = department_model->item(i); + QString key = item->data(Qt::UserRole).toString(); + if (key == "__back__") { + item->setText(tr("Back to Universities")); + } else { + QString translated = tr(key.toUtf8().constData()); + item->setText(translated.isEmpty() ? key : translated); + } + } + } + + ui->showMoreButton->setText(output_visible ? tr("Hide details ▲") : tr("Show details ▼")); +} + +void MainWindow::changeEvent(QEvent *event) { + if (event->type() == QEvent::LanguageChange) { + ui->retranslateUi(this); + retranslate(); + } + QMainWindow::changeEvent(event); } \ No newline at end of file diff --git a/src/main.cpp b/src/main.cpp index 6baaae1..bbe88b3 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,24 @@ // Author: Apostolos Chalis 2026 #include #include "MainWindow.hpp" +#include +#include +#include int main(int argc, char *argv[]){ + qputenv("QT_QPA_PLATFORMTHEME", ""); QApplication uni_backpack_app(argc, argv); + uni_backpack_app.setOrganizationName("UniBackpack"); + uni_backpack_app.setApplicationName("UniBackpack"); uni_backpack_app.setWindowIcon(QIcon(":/icons/unibackpack.png")); uni_backpack_app.setDesktopFileName("UniBackpack"); + // Load saved language, default to system locale, fall back to English + QSettings settings; + QString lang = settings.value("language", QLocale::system().name()).toString(); + applyTranslator(lang); + + MainWindow main_window; main_window.show(); diff --git a/translations/unibackpack_el.ts b/translations/unibackpack_el.ts new file mode 100644 index 0000000..4e36d0c --- /dev/null +++ b/translations/unibackpack_el.ts @@ -0,0 +1,107 @@ + + + + + Downloader + + + + Found: %1 + Βρέθηκε: %1 + + + + + Not in repos: %1 + Δεν βρέθηκε στα αποθετήρια: %1 + + + + + + + Checking packages for %1... + Έλεγχος πακέτων για %1... + + + + + Adding: %1 + Προσθήκη: %1 + + + + + Adding (non-standard): %1 + Προσθήκη (μη τυπικό): %1 + + + + Warning + Προειδοποίηση + + + + It is advised to update your system before proceeding. +Run sudo pacman -Syu + Συνιστάται να ενημερώσετε το σύστημά σας πριν συνεχίσετε. +Εκτελέστε sudo pacman -Syu + + + + MainWindow + + + UniBackpack + UniBackpack + + + + + Show details ▼ + Εμφάνιση λεπτομερειών ▼ + + + + Back to Universities + Επιστροφή στην επιλογή Πανεπιστημίων + + + + Hide details ▲ + Απόκρυψη λεπτομερειών ▲ + + + Aristotle University of Thessaloniki + Αριστοτέλειο Πανεπιστήμιο Θεσσαλονίκης + + + University of Western Macedonia + Πανεπιστήμιο Δυτικής Μακεδονίας + + + University of Macedonia + Πανεπιστήμιο Μακεδονίας + + + Informatics + Τμήμα Πληροφορικής + + + Physics + Τμήμα Φυσικής + + + Mechanical Engineering + Τμήμα Μηχανολογίας + + + Applied Informatics + Τμήμα Εφαρμοσμένης Πληροφορικής + + + Economics + Τμήμα Οικονομικών + + + \ No newline at end of file diff --git a/translations/unibackpack_en.ts b/translations/unibackpack_en.ts new file mode 100644 index 0000000..9211ae1 --- /dev/null +++ b/translations/unibackpack_en.ts @@ -0,0 +1,75 @@ + + + + + Downloader + + + + Found: %1 + + + + + + Not in repos: %1 + + + + + + + + Checking packages for %1... + + + + + + Adding: %1 + + + + + + Adding (non-standard): %1 + + + + + Warning + + + + + It is advised to update your system before proceeding. +Run sudo pacman -Syu + + + + + MainWindow + + + UniBackpack + + + + + + Show details ▼ + + + + + + Back to Universities + + + + + Hide details ▲ + + + +