From 504d29accac51c537d5dcd42b129deb6f7463457 Mon Sep 17 00:00:00 2001 From: Matias Linares Date: Fri, 20 Sep 2024 20:46:28 -0300 Subject: Initial commit --- .gitignore | 162 +++++++++++++++++ MANIFEST.in | 1 + README.md | 3 + kodereviewer/__init__.py | 0 kodereviewer/__main__.py | 3 + kodereviewer/app.py | 56 ++++++ kodereviewer/data.py | 70 ++++++++ kodereviewer/mdconverter.py | 30 ++++ kodereviewer/network_manager.py | 58 ++++++ kodereviewer/project.py | 49 +++++ kodereviewer/project_model.py | 92 ++++++++++ kodereviewer/pull_request_model.py | 66 +++++++ kodereviewer/qml/AddRepositoryPage.qml | 97 ++++++++++ kodereviewer/qml/Main.qml | 85 +++++++++ kodereviewer/qml/ProjectListPage.qml | 130 ++++++++++++++ kodereviewer/qml/SettingsPage.qml | 39 ++++ kodereviewer/qml/WelcomePage.qml | 117 ++++++++++++ org.deprecated.kodereviewer.desktop | 8 + org.deprecated.kodereviewer.svg | 314 +++++++++++++++++++++++++++++++++ pyproject.toml | 32 ++++ 20 files changed, 1412 insertions(+) create mode 100644 .gitignore create mode 100644 MANIFEST.in create mode 100644 README.md create mode 100644 kodereviewer/__init__.py create mode 100644 kodereviewer/__main__.py create mode 100644 kodereviewer/app.py create mode 100644 kodereviewer/data.py create mode 100644 kodereviewer/mdconverter.py create mode 100644 kodereviewer/network_manager.py create mode 100644 kodereviewer/project.py create mode 100644 kodereviewer/project_model.py create mode 100644 kodereviewer/pull_request_model.py create mode 100644 kodereviewer/qml/AddRepositoryPage.qml create mode 100644 kodereviewer/qml/Main.qml create mode 100644 kodereviewer/qml/ProjectListPage.qml create mode 100644 kodereviewer/qml/SettingsPage.qml create mode 100644 kodereviewer/qml/WelcomePage.qml create mode 100755 org.deprecated.kodereviewer.desktop create mode 100644 org.deprecated.kodereviewer.svg create mode 100644 pyproject.toml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..efa407c --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..92295a5 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include kodereviewer/qml/*.qml diff --git a/README.md b/README.md new file mode 100644 index 0000000..20b9c16 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Kodereviewer + +A code review tool diff --git a/kodereviewer/__init__.py b/kodereviewer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/kodereviewer/__main__.py b/kodereviewer/__main__.py new file mode 100644 index 0000000..87c61d9 --- /dev/null +++ b/kodereviewer/__main__.py @@ -0,0 +1,3 @@ +from . import app + +app.main() diff --git a/kodereviewer/app.py b/kodereviewer/app.py new file mode 100644 index 0000000..a2eb5f4 --- /dev/null +++ b/kodereviewer/app.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python3 + +import os +import sys +import signal + +from KI18n import KLocalizedContext, KLocalizedString +from PySide6.QtGui import QGuiApplication +from PySide6.QtCore import QUrl, QByteArray +from PySide6.QtQml import QQmlApplicationEngine, qmlRegisterType + +from kodereviewer.mdconverter import MdConverter +from kodereviewer.project_model import ProjectModel +from kodereviewer.pull_request_model import PullRequestModel +from kodereviewer.network_manager import NetworkManager + + +def main(): + """Initializes and manages the application execution""" + app = QGuiApplication(sys.argv) + engine = QQmlApplicationEngine() + # KLocalizedString::setApplicationDomain("tutorial"); + KLocalizedString.setApplicationDomain(QByteArray(b"kodereviewer")) + + app.setOrganizationName("Deprecated") + app.setOrganizationDomain("deprecated.org") + app.setApplicationName("Kode Reviewer") + app.setDesktopFileName("kodereviewer") + + """Needed to close the app with Ctrl+C""" + signal.signal(signal.SIGINT, signal.SIG_DFL) + + """Needed to get proper KDE style outside of Plasma""" + if not os.environ.get("QT_QUICK_CONTROLS_STYLE"): + os.environ["QT_QUICK_CONTROLS_STYLE"] = "org.kde.desktop" + + qmlRegisterType(MdConverter, "org.deprecated.kodereviewer", 1, 0, "MdConverter") + qmlRegisterType(ProjectModel, "org.deprecated.kodereviewer", 1, 0, "ProjectModel") + qmlRegisterType(PullRequestModel, "org.deprecated.kodereviewer", 1, 0, "PullRequestModel") + qmlRegisterType(NetworkManager, "org.deprecated.kodereviewer", 1, 0, "NetworkManager") + + localized_context = KLocalizedContext() + engine.rootContext().setContextObject(localized_context) + base_path = os.path.abspath(os.path.dirname(__file__)) + engine.addImportPath(f"file://{base_path}/qml") + url = QUrl(f"file://{base_path}/qml/Main.qml") + engine.load(url) + + if len(engine.rootObjects()) == 0: + quit() + + app.exec() + + +if __name__ == "__main__": + main() diff --git a/kodereviewer/data.py b/kodereviewer/data.py new file mode 100644 index 0000000..7d3b73a --- /dev/null +++ b/kodereviewer/data.py @@ -0,0 +1,70 @@ +from datetime import datetime +from enum import Enum +from typing import Any, Optional + +from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug +from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory +from PySide6.QtQml import QmlElement + + +class User(QObject): + username: str + avatar_url: str + + def __init__(self, data: dict[str, Any]): + super().__init__() + self.username = data['login'] + self.avatar_url = data['avatar_url'] + + +class Label(QObject): + name: str + color: str + description: str + + def __init__(self, data: dict[str, Any]): + super().__init__() + self.name = data['name'] + self.color = data['color'] + self.description = data['description'] + + + +class State(Enum): + OPEN = 'open' + CLOSED = 'closed' + DRAFT = 'draft' + + +class PullRequest(QObject): + + number: int + title: str + state: State + url: str + body: str | None + created_at: datetime + updated_at: datetime + + user: User + assignee: User | None + labels: list[str] + + + def __init__(self, data: dict[str, Any]): + super().__init__() + self.number = data['number'] + self.state = data['state'] + self.title = data['title'] + self.url = data['html_url'] + self.body = data['body'] + self.created_at = datetime.fromisoformat(data['created_at']) + self.updated_at = datetime.fromisoformat(data['updated_at']) + + self.user = User(data['user']) + self.assignee = None + if data['assignee'] is not None: + self.assignee = User(data['assignee']) + + labels = [Label(label) for label in data['labels']] + diff --git a/kodereviewer/mdconverter.py b/kodereviewer/mdconverter.py new file mode 100644 index 0000000..1d0a653 --- /dev/null +++ b/kodereviewer/mdconverter.py @@ -0,0 +1,30 @@ +from markdown import markdown +from PySide6.QtCore import QObject, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class MdConverter(QObject): + """A simple markdown converter""" + + sourceTextChanged = Signal() + + def __init__(self, _source_text=""): + super().__init__() + self._source_text = _source_text + + @Property(str, notify=sourceTextChanged) + def sourceText(self): + return self._source_text + + @sourceText.setter + def sourceText(self, val: str): + self._source_text = val + self.sourceTextChanged.emit() + + @Slot(result=str) + def mdFormat(self): + return markdown(self._source_text) diff --git a/kodereviewer/network_manager.py b/kodereviewer/network_manager.py new file mode 100644 index 0000000..2d8b8aa --- /dev/null +++ b/kodereviewer/network_manager.py @@ -0,0 +1,58 @@ +from typing import Optional + +from PySide6.QtCore import QObject, QSettings, Signal, Slot, Property, qDebug +from PySide6.QtNetwork import QHttpHeaders, QNetworkAccessManager, QNetworkReply, QNetworkRequestFactory +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + +@QmlElement +class NetworkManager(QObject): + + _project: Optional[Project] + _manager: QNetworkAccessManager + _request_factory: QNetworkRequestFactory + + projectChanged = Signal() + + def __init__(self): + super().__init__() + self._project = None + self._manager = QNetworkAccessManager() + + self._request_factory = QNetworkRequestFactory() + settings = QSettings() + github_token: str = str(settings.value("githubToken")) + headers = QHttpHeaders() + headers.append(QHttpHeaders.WellKnownHeader.Accept, "application/vnd.github+json") + headers.append(QHttpHeaders.WellKnownHeader.Authorization, + f'Bearer {github_token}') + headers.append(QHttpHeaders.WellKnownHeader.UserAgent, "kodereviewer") + print(f'Authorization: Bearer {github_token}') + self._request_factory.setCommonHeaders(headers) + + self._manager.finished.connect(self.reply_finished) + + def project(self) -> Optional[Project]: + return self._project + + def set_project(self, project: Optional[Project]): + if project is None: + return + self._project = project + self.projectChanged.emit() + + base_url = f'https://api.github.com/repos/{self._project.owner}/{self._project.name}' + self._request_factory.setBaseUrl(base_url) + + project = Property(Project, fget=project, fset=set_project) + + def reply_finished(self, reply: QNetworkReply): + self._project.load_pull_requests(reply.readAll()) + + @Slot() + def getPullRequests(self) -> None: + self._manager.get(self._request_factory.createRequest("/pulls")) diff --git a/kodereviewer/project.py b/kodereviewer/project.py new file mode 100644 index 0000000..669ee90 --- /dev/null +++ b/kodereviewer/project.py @@ -0,0 +1,49 @@ +import json +from PySide6.QtCore import QByteArray, QObject, QUrl, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.data import PullRequest + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + +@QmlElement +class Project(QObject): + """Represents a github project""" + + _name: str + _owner: str + _url: QUrl + _pull_requests: list[PullRequest] + + pullRequestChanged = Signal() + + def __init__(self, name: str, owner: str, url: QUrl): + super().__init__() + self._name = name + self._owner = owner + self._url = url + self._pull_requests = [] + + @Property(str) + def name(self) -> str: + return self._name + + @Property(str) + def owner(self) -> str: + return self._owner + + @Property(QUrl) + def url(self) -> QUrl: + return self._url + + @Property(list) + def pullRequests(self) -> list[PullRequest]: + return self._pull_requests + + def load_pull_requests(self, response: QByteArray) -> None: + data = json.loads(response.toStdString()) + self._pull_requests = [ + PullRequest(pr) for pr in data + ] + self.pullRequestChanged.emit() diff --git a/kodereviewer/project_model.py b/kodereviewer/project_model.py new file mode 100644 index 0000000..c676157 --- /dev/null +++ b/kodereviewer/project_model.py @@ -0,0 +1,92 @@ +import json +from os import path +from typing import Any + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + + +@QmlElement +class ProjectModel(QAbstractListModel): + """Projects list!""" + + projects: list[Project] + + NameRole = Qt.ItemDataRole.UserRole + 1 + OwnerRole = NameRole + 1 + UrlRole = OwnerRole + 1 + + def __init__(self): + super().__init__() + self.projects = [] + + project_config = self._project_file() + try: + with open(project_config) as fp: + data = json.load(fp) + if isinstance(data, list): + self._load_projects(data) + except OSError as e: + pass + + def _project_file(self) -> str: + app_data = QStandardPaths.writableLocation(QStandardPaths.AppDataLocation) + project_config = path.join(app_data, 'projects.json') + return project_config + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + project = self.projects[index.row()] + + if role == Qt.ItemDataRole.DisplayRole: + return project.name + if role == self.NameRole: + return project.name + if role == self.OwnerRole: + return project.owner + if role == self.UrlRole: + return project.url + + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + return len(self.projects) + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.NameRole: QByteArray(b"name"), + self.OwnerRole: QByteArray(b"owner"), + self.UrlRole: QByteArray(b"url") + } + + @Slot(int, result=Project) + def get(self, index: int) -> Project: + return self.projects[index] + + def _load_projects(self, data: list[dict[str, Any]]) -> None: + for project in data: + self.projects.append(Project( + project['name'], + project['owner'], + project['url'] + )) + + def _save_projects(self): + project_config = self._project_file() + data = [{'name': project.name, 'owner': project.owner, 'url': project.url} for project in self.projects] + + with open(self._project_file(), 'w') as fp: + json.dump(data, fp) + + @Slot(str, str, str) + def add(self, name: str, owner: str, url: str) -> None: + self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount()) + self.projects.append(Project(name, owner, url)) + self._save_projects() + self.endInsertRows() diff --git a/kodereviewer/pull_request_model.py b/kodereviewer/pull_request_model.py new file mode 100644 index 0000000..e08bf6e --- /dev/null +++ b/kodereviewer/pull_request_model.py @@ -0,0 +1,66 @@ +import json +from os import path +from typing import Any, Optional + +from PySide6.QtCore import QAbstractListModel, QByteArray, QModelIndex, QObject, QPersistentModelIndex, QStandardPaths, QUrl, Qt, Signal, Slot, Property +from PySide6.QtQml import QmlElement + +from kodereviewer.project import Project +from kodereviewer.data import PullRequest + +QML_IMPORT_NAME = "org.deprecated.kodereviewer" +QML_IMPORT_MAJOR_VERSION = 1 + +@QmlElement +class PullRequestModel(QAbstractListModel): + + _project: Optional[Project] + + NumberRole = Qt.ItemDataRole.UserRole + 1 + TitleRole = NumberRole + 1 + + def __init__(self): + super().__init__() + + def data(self, + index: QModelIndex | QPersistentModelIndex, + role: int = Qt.ItemDataRole.DisplayRole) -> object: + if self._project is None: + return None + + pull_request = self._project.pullRequests[index.row()] + if role == self.NumberRole: + return pull_request.number + if role == self.TitleRole: + return pull_request.title + if role == Qt.ItemDataRole.DisplayRole: + return f'{pull_request.number} - {pull_request.title}' + return None + + def rowCount(self, parent: QModelIndex | QPersistentModelIndex = QModelIndex()) -> int: + if self._project is not None: + return len(self._project.pullRequests) + return 0 + + def roleNames(self) -> dict[int, QByteArray]: + return { + self.NumberRole: QByteArray(b"number"), + self.TitleRole: QByteArray(b"title"), + } + + def get_project(self) -> Optional[Project]: + return self._project + + def set_project(self, project: Optional[Project])-> None: + if project is None: + return + self._project = project + print("Connecting!") + self._project.pullRequestChanged.connect(self._reset_model) + + project = Property(Project, fget=get_project, fset=set_project) + + def _reset_model(self) -> None: + print("Reseting pull request model") + self.beginResetModel() + self.endResetModel() diff --git a/kodereviewer/qml/AddRepositoryPage.qml b/kodereviewer/qml/AddRepositoryPage.qml new file mode 100644 index 0000000..2bd26fe --- /dev/null +++ b/kodereviewer/qml/AddRepositoryPage.qml @@ -0,0 +1,97 @@ +import QtCore +import QtQuick 6.7 +import QtQuick.Controls 6 as QQC2 +import QtQuick.Layouts 6.7 + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard + +FormCard.FormCardPage { + id: root + title: "Add a new repository" + + signal accepted(string url, string name, string owner, string displayName) + + FormCard.FormHeader { + title: "Github information" + } + + FormCard.FormCard { + FormCard.FormTextFieldDelegate { + id: urlField + label: "URL" + onTextChanged: root.fillDataFromUrl(text) + } + + FormCard.FormDelegateSeparator {} + + FormCard.FormTextFieldDelegate { + id: nameField + label: "Name" + } + + FormCard.FormDelegateSeparator {} + + FormCard.FormTextFieldDelegate { + id: ownerField + label: "Owner" + } + + FormCard.FormDelegateSeparator {} + } + + FormCard.FormHeader { + title: "General information" + } + + FormCard.FormCard { + FormCard.FormTextFieldDelegate { + id: displayNameField + label: "Display name" + } + } + + + FormCard.FormHeader { + title: "Git" + } + + FormCard.FormCard { + FormCard.FormCheckDelegate { + id: cloneCheck + text: "Clone repository" + checked: false + } + + FormCard.FormDelegateSeparator {} + + FormCard.FormTextFieldDelegate { + id: cloneDirectory + label: "Clone directory" + enabled: cloneCheck.checked + } + } + + + footer: QQC2.ToolBar { + contentItem: QQC2.DialogButtonBox { + standardButtons: QQC2.Dialog.Ok | QQC2.Dialog.Cancel + onAccepted: root.accepted( + urlField.text, + nameField.text, + ownerField.text, + displayNameField.text + ) + onRejected: applicationWindow().pageStack.pop() + } + } + + function fillDataFromUrl(text) { + const s = text.split("/") + if (s.length > 2) { + nameField.text = s[s.length -1] + ownerField.text = s[s.length - 2] + displayNameField.text = s[s.length -1] + } + } +} diff --git a/kodereviewer/qml/Main.qml b/kodereviewer/qml/Main.qml new file mode 100644 index 0000000..0b41a05 --- /dev/null +++ b/kodereviewer/qml/Main.qml @@ -0,0 +1,85 @@ +pragma ComponentBehavior: Bound +import QtQuick +import QtCore +import QtQuick.Controls as Controls +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard + +import org.deprecated.kodereviewer 1.0 + +Kirigami.ApplicationWindow { + id: root + + title: qsTr("Kode Reviewer") + + minimumWidth: Kirigami.Units.gridUnit * 20 + minimumHeight: Kirigami.Units.gridUnit * 20 + width: minimumWidth + height: minimumHeight + + signal projectSelected() + + property Project project + + property NetworkManager connection: NetworkManager { + project: root.project + } + + Settings { + id: settings + property alias width: root.width + property alias height: root.height + property string githubToken: "" + } + + pageStack.initialPage: initPage + + Component { + id: initPage + WelcomePage { + onProjectSelected: project => { + root.project = project + root.projectSelected() + } + } + } + + Loader { + id: projectListPageLoader + active: false + sourceComponent: Component { + ProjectListPage { + connection: root.connection + project: root.project + } + } + } + + Loader { + id: placeHolderPageLoader + active: false + sourceComponent: Component { + Kirigami.Page { + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + title: "Select a pull request" + spacing: Kirigami.Units.largeSpacing * 2 + Kirigami.PlaceholderMessage { + anchors.centerIn: parent + icon.name: "org.deprecated.kodereviewer" + text: "Select a pull request" + } + } + } + } + + onProjectSelected: { + projectListPageLoader.active = true + placeHolderPageLoader.active = true + pageStack.replace(projectListPageLoader.item) + pageStack.push(placeHolderPageLoader.item) + } +} diff --git a/kodereviewer/qml/ProjectListPage.qml b/kodereviewer/qml/ProjectListPage.qml new file mode 100644 index 0000000..334799e --- /dev/null +++ b/kodereviewer/qml/ProjectListPage.qml @@ -0,0 +1,130 @@ +pragma ComponentBehavior: Bound +import QtQuick 2.15 // Removing version break onCurrentItemChanged +import QtQuick.Layouts +import QtQuick.Controls as QQC2 + +import org.kde.kirigamiaddons.delegates as Delegates +import org.kde.kitemmodels 1 as KItemModels +import org.kde.kirigami as Kirigami + +import org.deprecated.kodereviewer + +Kirigami.Page { + id: root + + required property NetworkManager connection + required property Project project + + + readonly property int currentWidth: _private.currentWidth + 1 + + onCurrentWidthChanged: pageStack.defaultColumnWidth = root.currentWidth + Component.onCompleted: { + pageStack.defaultColumnWidth = root.currentWidth + connection.getPullRequests() + } + + Kirigami.Theme.colorSet: Kirigami.Theme.View + Kirigami.Theme.inherit: false + + PullRequestModel { + id: pullRequestModel + project: root.project + } + + KItemModels.KSortFilterProxyModel { + id: pullRequestFilterModel + sourceModel: pullRequestModel + filterRoleName: "title" + } + + title: "Pull Requests" + + actions: [ + Kirigami.Action { + id: searchAction + icon.name: "search" + shortcut: Shortcut { + sequence: "Ctrl+F" + onActivated: { + print("Shortcut triggered") + searchAction.trigger() + } + } + + onTriggered: print("search triggered") + } + ] + + contentItem: QQC2.StackView { + id: stackView + anchors.fill: parent + + initialItem: pullRequestListView + + Component { + id: pullRequestListView + QQC2.ScrollView { + ListView { + id: view + model: pullRequestFilterModel + clip: true + delegate: Delegates.RoundedItemDelegate { + required property int number + required property string title + required property int index + highlighted: ListView.isCurrentItem + + text: `${number} - ${title}` + icon.name: "vcs-merge-request" + } + } + } + } + } + + MouseArea { + anchors.top: parent.top + anchors.bottom: parent.bottom + parent: applicationWindow().overlay.parent + + x: root.currentWidth - width / 2 + width: Kirigami.Units.smallSpacing * 2 + z: root.z + 1 + enabled: true + visible: enabled + cursorShape: Qt.SplitHCursor + + property int _lastX + + onPressed: mouse => { + _lastX = mouse.x; + } + onPositionChanged: mouse => { + if (_lastX == -1) { + return; + } + if (mouse.x > _lastX) { + // _private.currentWidth = _private.currentWidth + (_lastX + mouse.x); + _private.currentWidth = Math.min(_private.defaultWidth, _private.currentWidth + (mouse.x - _lastX)) + } else if (mouse.x < _lastX) { + const tmpWidth = _private.currentWidth - (_lastX - mouse.x); + if (tmpWidth > _private.minWidth) + _private.currentWidth = tmpWidth; + + } + } + } + + /* + * Hold the modifiable currentWidth in a private object so that only internal + * members can modify it. + */ + QtObject { + id: _private + property int currentWidth: defaultWidth + readonly property int defaultWidth: Kirigami.Units.gridUnit * 17 + readonly property int minWidth: Kirigami.Units.gridUnit * 2 + } + +} diff --git a/kodereviewer/qml/SettingsPage.qml b/kodereviewer/qml/SettingsPage.qml new file mode 100644 index 0000000..83e5c90 --- /dev/null +++ b/kodereviewer/qml/SettingsPage.qml @@ -0,0 +1,39 @@ +import QtCore +import QtQuick 6.7 +import QtQuick.Controls 6 as QQC2 +import QtQuick.Layouts 6.7 + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard + + +FormCard.FormCardPage { + id: root + + actions: [ + Kirigami.Action { + id: saveAction + text: "Save" + onTriggered: { + settings.sync() + } + } + ] + + Settings { + id: settings + property string githubToken: githubTokenField.text + } + + FormCard.FormHeader { + title: "Authorization" + } + + FormCard.FormCard { + FormCard.FormTextFieldDelegate { + id: githubTokenField + text: settings.githubToken + label: "Github Token" + } + } +} diff --git a/kodereviewer/qml/WelcomePage.qml b/kodereviewer/qml/WelcomePage.qml new file mode 100644 index 0000000..af74d01 --- /dev/null +++ b/kodereviewer/qml/WelcomePage.qml @@ -0,0 +1,117 @@ +import QtCore +import QtQuick 6.7 +import QtQuick.Controls 6 as QQC2 +import QtQuick.Layouts 6.7 + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard +import org.kde.kirigamiaddons.settings as KirigamiSettings + +import org.deprecated.kodereviewer + +FormCard.FormCardPage { + id: root + + title: "Welcome" + + property int projectCount: projectModel.rowCount() + + signal projectSelected(Project project) + + ProjectModel { + id: projectModel + + onModelReset: { + projectCount = projectModel.rowCount() + } + } + + Component { + id: addRepositoryPage + AddRepositoryPage { + onAccepted: (url, name, owner, displayName) => { + projectModel.add(displayName, owner, url) + applicationWindow().pageStack.pop() + } + } + } + KirigamiSettings.ConfigurationView { + id: configuration + + window: applicationWindow() as Kirigami.ApplicationWindow + + modules: [ + KirigamiSettings.ConfigurationModule { + moduleId: "appearance" + text: i18nc("@action:button", "General") + icon.name: "preferences-system-symbolic" + page: () => Qt.createComponent("SettingsPage.qml") + }, + KirigamiSettings.ConfigurationModule { + moduleId: "about" + text: i18nc("@action:button", "About Kode Reviewer") + icon.name: "help-about" + page: () => Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutPage") + category: i18nc("@title:group", "About") + }, + KirigamiSettings.ConfigurationModule { + moduleId: "aboutkde" + text: i18nc("@action:button", "About KDE") + icon.name: "kde" + page: () => Qt.createComponent("org.kde.kirigamiaddons.formcard", "AboutKDE") + category: i18nc("@title:group", "About") + } + ] + } + + Kirigami.Heading { + id: welcomeMessage + + text: "Welcome to Kode Reviewer" + + Layout.alignment: Qt.AlignHCenter + Layout.topMargin: Kirigami.Units.largeSpacing + } + + FormCard.FormHeader { + id: existingProjectsHeader + title: "Existing projects" + visible: root.projectCount > 0 + } + + FormCard.FormCard { + visible: existingProjectsHeader.visible + + Repeater { + id: loadedProjects + model: projectModel + delegate: FormCard.FormButtonDelegate { + required property string name + required property string url + required property int index + text: name + description: url + onClicked: root.projectSelected(projectModel.get(index)) + } + } + } + + FormCard.FormHeader { + title: "Add new project" + } + + FormCard.FormCard { + FormCard.FormButtonDelegate { + text: "Add new project" + onClicked: applicationWindow().pageStack.push(addRepositoryPage) + } + } + + FormCard.FormCard { + FormCard.FormButtonDelegate { + text: "Settings" + icon.name: 'settings-configure-symbolic' + onClicked: configuration.open() + } + } +} diff --git a/org.deprecated.kodereviewer.desktop b/org.deprecated.kodereviewer.desktop new file mode 100755 index 0000000..e4ae541 --- /dev/null +++ b/org.deprecated.kodereviewer.desktop @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Kode reviewer +Exec=kode-reviewer +Icon=kode-reviewer +Comment=A tool to make code reviews +Type=Application +Terminal=false +Categories=Development;Debugger;Qt; diff --git a/org.deprecated.kodereviewer.svg b/org.deprecated.kodereviewer.svg new file mode 100644 index 0000000..305cd1d --- /dev/null +++ b/org.deprecated.kodereviewer.svg @@ -0,0 +1,314 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..4bed84a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "org.deprecated.kodereviewer" +version = "0.1" +authors = [ + {name = "Matias Linares", email = "matias@deprecated.org" } +] +description = "Code review tool" +readme = "README.md" + +dependencies = [ + "PySide6", + "markdown" +] + +[options] +packages = "kodereviewer" +include_package_data = true + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[options.data_files] +"share/applications" = "org.deprecated.kodereviewer.desktop" +"share/icons/hicolor/scalable/apps" = "org.deprecated.kodereviewer.svg" + +[project.scripts] +kodereviewer = "kodereviewer.app:main" + +[tool.pyright] +venv = "venv" +venvPath = "." -- cgit v1.2.3-70-g09d2