diff options
-rw-r--r-- | .gitignore | 162 | ||||
-rw-r--r-- | MANIFEST.in | 1 | ||||
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | kodereviewer/__init__.py | 0 | ||||
-rw-r--r-- | kodereviewer/__main__.py | 3 | ||||
-rw-r--r-- | kodereviewer/app.py | 56 | ||||
-rw-r--r-- | kodereviewer/data.py | 70 | ||||
-rw-r--r-- | kodereviewer/mdconverter.py | 30 | ||||
-rw-r--r-- | kodereviewer/network_manager.py | 58 | ||||
-rw-r--r-- | kodereviewer/project.py | 49 | ||||
-rw-r--r-- | kodereviewer/project_model.py | 92 | ||||
-rw-r--r-- | kodereviewer/pull_request_model.py | 66 | ||||
-rw-r--r-- | kodereviewer/qml/AddRepositoryPage.qml | 97 | ||||
-rw-r--r-- | kodereviewer/qml/Main.qml | 85 | ||||
-rw-r--r-- | kodereviewer/qml/ProjectListPage.qml | 130 | ||||
-rw-r--r-- | kodereviewer/qml/SettingsPage.qml | 39 | ||||
-rw-r--r-- | kodereviewer/qml/WelcomePage.qml | 117 | ||||
-rwxr-xr-x | org.deprecated.kodereviewer.desktop | 8 | ||||
-rw-r--r-- | org.deprecated.kodereviewer.svg | 314 | ||||
-rw-r--r-- | pyproject.toml | 32 |
20 files changed, 1412 insertions, 0 deletions
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 --- /dev/null +++ b/kodereviewer/__init__.py 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 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="48" + height="48" + viewBox="0 0 48 48" + version="1.1" + id="svg1" + inkscape:version="1.3.2 (091e20ef0f, 2023-11-25, custom)" + sodipodi:docname="kodereviewer.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview1" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + inkscape:document-units="px" + showguides="false" + inkscape:zoom="4" + inkscape:cx="6" + inkscape:cy="23.375" + inkscape:window-width="1920" + inkscape:window-height="1022" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="layer5"> + <sodipodi:guide + position="23.9975,-4.6411364" + orientation="1,0" + id="guide5" + inkscape:locked="false" /> + <sodipodi:guide + position="4.2472727,24" + orientation="0,1" + id="guide6" + inkscape:locked="false" + inkscape:label="" + inkscape:color="rgb(0,134,229)" /> + </sodipodi:namedview> + <defs + id="defs1"> + <linearGradient + id="swatch67"> + <stop + style="stop-color:#ffffff;stop-opacity:1;" + offset="0" + id="stop67" /> + </linearGradient> + <linearGradient + id="linearGradient58" + inkscape:collect="always" + inkscape:label="shadow"> + <stop + style="stop-color:#1a1212;stop-opacity:0.36863258;" + offset="0" + id="stop57" /> + <stop + style="stop-color:#fefefe;stop-opacity:0;" + offset="1" + id="stop58" /> + </linearGradient> + <linearGradient + id="linearGradient55" + inkscape:label="KDE"> + <stop + style="stop-color:#00cbff;stop-opacity:1;" + offset="0.00026041" + id="stop54" /> + <stop + style="stop-color:#ff2291;stop-opacity:1;" + offset="1" + id="stop55" /> + </linearGradient> + <linearGradient + id="linearGradient15" + inkscape:collect="always"> + <stop + style="stop-color:#d0d0d0;stop-opacity:1;" + offset="0" + id="stop15" /> + <stop + style="stop-color:#fafafa;stop-opacity:1;" + offset="1" + id="stop16" /> + </linearGradient> + <linearGradient + id="linearGradient2" + inkscape:label="KDE"> + <stop + style="stop-color:#00cbff;stop-opacity:1;" + offset="0" + id="stop4" /> + <stop + style="stop-color:#00cbff;stop-opacity:1;" + offset="0.99973959" + id="stop5" /> + </linearGradient> + <linearGradient + id="linearGradient2-1" + inkscape:label="KDE"> + <stop + style="stop-color:#00cbff;stop-opacity:1;" + offset="0.00026041" + id="stop3" /> + <stop + style="stop-color:#228eff;stop-opacity:1;" + offset="1" + id="stop2" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient2-1" + id="linearGradient3" + x1="4" + y1="24" + x2="44" + y2="24" + gradientUnits="userSpaceOnUse" + gradientTransform="matrix(0.975,0,0,1,0.1,0)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient15" + id="linearGradient16" + x1="19.682396" + y1="16.570776" + x2="27.682396" + y2="16.570776" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-52.477341,-19.080291)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient15" + id="linearGradient18" + x1="29.232605" + y1="25.395569" + x2="37.232605" + y2="25.395569" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-58.628174,7.8370361)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient15" + id="linearGradient20" + x1="19.814938" + y1="30.319584" + x2="27.814938" + y2="30.319584" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-59.265166,-15.163406)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient58" + id="linearGradient33" + x1="32.493103" + y1="25.552551" + x2="47.312561" + y2="26.146179" + gradientUnits="userSpaceOnUse" + gradientTransform="rotate(45,33.255293,25.325471)" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient58" + id="linearGradient35" + gradientUnits="userSpaceOnUse" + gradientTransform="translate(-4.9084664,-30.418656)" + x1="33.445984" + y1="25.811829" + x2="47.096008" + y2="25.455994" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient58" + id="linearGradient68" + gradientUnits="userSpaceOnUse" + x1="24.064537" + y1="30.420015" + x2="32.667351" + y2="39.118153" /> + </defs> + <g + inkscape:label="Fondo" + inkscape:groupmode="layer" + id="layer1" + style="display:inline"> + <g + id="g7" + transform="rotate(90,24,24)"> + <circle + style="display:inline;fill:#0374ea;fill-opacity:1;stroke-width:1.085;stroke-dasharray:none" + id="path1-6" + cx="24" + cy="24" + r="20" + inkscape:label="sombra" /> + <ellipse + style="display:inline;fill:url(#linearGradient3);stroke-width:1.07135;stroke-dasharray:none" + id="path1" + cx="23.5" + cy="24" + rx="19.5" + ry="20" + inkscape:label="fondo" /> + </g> + </g> + <g + inkscape:groupmode="layer" + id="layer5" + inkscape:label="shadow"> + <path + d="m 36.873047,37.724609 -10.363281,-10.365234 -0.002,0.002 a 4,4 0 0 1 1.169922,1.923828 4,4 0 0 1 -2.828125,4.898438 4,4 0 0 1 -3.988281,-1.177735 l -0.0098,0.0098 8.699219,8.699219 a 19.5,19.012501 0 0 0 7.322266,-3.990235 z" + style="fill:url(#linearGradient68);stroke-width:1.085;fill-rule:nonzero;fill-opacity:1" + id="path62" /> + <path + d="m 42.613281,29.125 -6.689453,-6.6875 -5.65625,5.65625 8.166016,8.166016 A 19.5,19.012501 0 0 0 42.613281,29.125 Z" + style="display:inline;opacity:1;vector-effect:none;fill:url(#linearGradient33);stroke-width:1.085;stop-color:#000000;stop-opacity:1" + id="path57" /> + <path + id="path51" + style="fill:url(#linearGradient35);stroke-width:1.085;stroke-dasharray:none" + d="M 28.643349 -9.0238979 A 4 4 0 0 1 32.326657 -6.0642693 A 4 4 0 0 1 29.49823 -1.1642403 A 4 4 0 0 1 28.190359 -1.0468495 L 28.190359 -1.0219903 L 48.189603 -1.0233713 L 48.189603 -1.5371286 L 41.267691 -1.5371286 L 41.26631 -1.566131 A 4 4 0 0 1 38.62847 -2.7124174 A 4 4 0 0 1 38.62847 -8.3692717 A 4 4 0 0 1 39.490256 -9.0238979 L 28.643349 -9.0238979 z " + transform="rotate(45)" /> + <g + id="g44-5" + style="display:inline" /> + </g> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="Git lines" + style="display:inline"> + <g + id="g43"> + <rect + style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085;stroke-dasharray:none" + id="rect13-8" + width="2" + height="12" + x="4.5583954" + y="30.228466" + transform="rotate(-45)" /> + <path + d="m 15.826172,6.8457031 c -0.629358,0.300465 -1.239339,0.6346349 -1.828125,1 L 22.402344,16.25 23.816406,14.835938 Z" + style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085" + id="path32" /> + <rect + style="display:inline;fill:#f9f9f9;fill-opacity:0.7;stroke-width:1.085;stroke-dasharray:none" + id="rect13-3" + width="2" + height="12" + x="22.973835" + y="17.202845" /> + </g> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Git" + style="display:inline"> + <g + id="g44"> + <circle + style="fill:url(#linearGradient18);stroke-width:2.06845;stroke-dasharray:none" + id="path5-5" + cx="-25.395569" + cy="33.232605" + r="4" + transform="rotate(-90)" /> + <circle + style="display:inline;fill:url(#linearGradient16);stroke-width:2.06845;stroke-dasharray:none" + id="path5" + cx="-28.794945" + cy="-2.509515" + r="4" + transform="rotate(-150)" /> + <circle + style="fill:url(#linearGradient20);stroke-width:2.06845;stroke-dasharray:none" + id="path5-2" + cx="-35.45023" + cy="15.156178" + r="4" + transform="rotate(-105)" /> + </g> + </g> + <g + id="g32" + style="display:none"> + <path + d="M 43.990234,23.623047 C 43.92087,34.340771 34.992755,42.99498 24,43 13.059009,42.991474 4.1556521,34.412714 4.0175781,23.746094 4.0111803,23.830691 4.0053208,23.915328 4,24 4,35.045684 12.954316,44 24,44 35.045684,44 44,35.045684 44,24 43.9979,23.874321 43.9947,23.748664 43.9902,23.623047 Z" + style="display:inline;fill:#1a1a1a;stroke-width:1.085" + id="path31" /> + <path + d="M 22.970703,4.0253906 C 12.403316,4.5476611 4,13.067006 4,23.5 4.00533,23.58207 4.011187,23.6641 4.017578,23.746094 4.1520846,13.151708 12.506458,4.5645588 22.970703,4.0253906 Z" + style="display:inline;fill:#1a1a1a;stroke-width:1.07135" + id="path30" /> + <path + style="display:inline;fill:#1a1a1a;stroke-width:1.07135" + d="M 24,4 C 13.059565,4.008734 4.1564669,12.806537 4.0175781,23.746094 4.1556524,34.412735 13.058987,42.991474 24,43 13.5066,43 5,34.4934 5,24 5,13.5066 13.5066,5 24,5 34.4934,5 43,13.5066 43,24 43,34.4934 34.4934,43 24,43 34.992777,42.99498 43.92087,34.340793 43.990234,23.623047 43.791369,13.073716 35.450854,4.557314 25.029297,4.0253906 24.688408,4.0079914 24.34513,4.0001072 24,4 Z" + id="path29" /> + <path + d="M 24,4 A 20,20 0 0 1 43.990234,23.623047 20,19.5 0 0 0 44,23.5 20,19.5 0 0 0 24,4 Z" + style="display:inline;fill:#1a1a1a;stroke-width:1.07135" + id="path28" /> + </g> +</svg> 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 = "." |