import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM
import "Status.js" as Status
KCM.SimpleKCM {
id: configGeneral
property alias cfg_instanceUrl: instanceUrlField.text
property alias cfg_accessToken: accessTokenField.text
property string cfg_watched: "[]"
// Connection test
property bool isTestingConnection: false
property string connectionTestResult: ""
property bool connectionTestSuccess: false
// Server listing
property bool isLoadingRepos: false
property string loadReposError: ""
property var namespaces: []
property var repositories: []
readonly property var watched: {
try {
var parsed = JSON.parse(cfg_watched || "[]")
return Array.isArray(parsed) ? parsed : []
} catch (e) {
return []
}
}
readonly property bool canConnect: instanceUrlField.text.trim() !== "" && accessTokenField.text.trim() !== ""
GitGudAPI {
id: testAPI
baseUrl: instanceUrlField.text
accessToken: accessTokenField.text
}
function testConnection() {
if (!canConnect) {
connectionTestResult = i18n("Please enter both URL and access token")
connectionTestSuccess = false
return
}
isTestingConnection = true
connectionTestResult = ""
testAPI.testConnection(function(success, message) {
isTestingConnection = false
connectionTestResult = message
connectionTestSuccess = success
if (success) loadRepositories()
})
}
function loadRepositories() {
if (!canConnect) return
isLoadingRepos = true
loadReposError = ""
testAPI.listRepos(function(success, data) {
isLoadingRepos = false
if (!success) {
loadReposError = data
return
}
namespaces = data.namespaces || []
var repos = (data.repositories || []).slice()
repos.sort(function(a, b) { return (a.full_name || "").localeCompare(b.full_name || "") })
repositories = repos
})
}
// ── watched list editing ────────────────────────────────────────────
function saveWatched(list) {
cfg_watched = JSON.stringify(list)
}
function isWatchedRepo(fullName) {
return watched.some(function(w) { return w.kind === "repo" && w.fullName === fullName })
}
function isWatchedNamespace(handle) {
return watched.some(function(w) { return w.kind === "namespace" && w.handle === handle })
}
function addRepo(fullName) {
if (!fullName || isWatchedRepo(fullName)) return
saveWatched(watched.concat([{ kind: "repo", fullName: fullName }]))
}
function addNamespace(handle) {
if (!handle || isWatchedNamespace(handle)) return
saveWatched(watched.concat([{ kind: "namespace", handle: handle, grouped: false }]))
}
function removeAt(index) {
var list = watched.slice()
list.splice(index, 1)
saveWatched(list)
}
function setGrouped(index, grouped) {
var list = watched.slice()
list[index] = Object.assign({}, list[index], { grouped: grouped })
saveWatched(list)
}
function move(index, delta) {
var target = index + delta
if (target < 0 || target >= watched.length) return
var list = watched.slice()
var item = list.splice(index, 1)[0]
list.splice(target, 0, item)
saveWatched(list)
}
function addManual() {
var text = manualField.text.trim().replace(/^\/+|\/+$/g, "")
if (!text) return
if (text.indexOf("/") !== -1) addRepo(text)
else addNamespace(text)
manualField.text = ""
}
function repoCount(handle) {
return repositories.filter(function(r) { return r.owner === handle }).length
}
readonly property var filteredRepos: {
var q = searchField.text.trim().toLowerCase()
if (!q) return repositories
return repositories.filter(function(r) {
return (r.full_name || "").toLowerCase().indexOf(q) !== -1
|| (r.description || "").toLowerCase().indexOf(q) !== -1
})
}
Component.onCompleted: if (canConnect) Qt.callLater(loadRepositories)
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
// ── connection ──────────────────────────────────────────────────
Kirigami.FormLayout {
Layout.fillWidth: true
QQC2.TextField {
id: instanceUrlField
Kirigami.FormData.label: i18n("GitGud URL:")
placeholderText: i18n("https://git.example.com")
Layout.fillWidth: true
onTextChanged: connectionTestResult = ""
}
RowLayout {
Kirigami.FormData.label: i18n("Access Token:")
Layout.fillWidth: true
QQC2.TextField {
id: accessTokenField
Layout.fillWidth: true
placeholderText: i18n("ggp_…")
echoMode: showTokenButton.checked ? TextInput.Normal : TextInput.Password
onTextChanged: connectionTestResult = ""
}
QQC2.ToolButton {
id: showTokenButton
checkable: true
icon.name: checked ? "password-show-off" : "password-show-on"
QQC2.ToolTip.text: checked ? i18n("Hide token") : i18n("Show token")
QQC2.ToolTip.visible: hovered
}
}
RowLayout {
Kirigami.FormData.label: i18n("Connection:")
QQC2.Button {
text: i18n("Test Connection")
icon.name: "network-connect"
enabled: !isTestingConnection && canConnect
onClicked: testConnection()
}
QQC2.BusyIndicator {
visible: isTestingConnection
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
}
QQC2.Label {
visible: connectionTestResult !== ""
text: connectionTestResult
color: connectionTestSuccess ? Kirigami.Theme.positiveTextColor : Kirigami.Theme.negativeTextColor
wrapMode: Text.WordWrap
Layout.fillWidth: true
font: Kirigami.Theme.smallFont
}
QQC2.Label {
Layout.fillWidth: true
wrapMode: Text.WordWrap
font: Kirigami.Theme.smallFont
color: Kirigami.Theme.disabledTextColor
textFormat: Text.StyledText
onLinkActivated: function(link) { Qt.openUrlExternally(link) }
text: {
var base = testAPI.normalizedBase()
var where = base ? "<a href=\"" + base + "/users/settings/tokens\">" + base + "/users/settings/tokens</a>"
: i18n("Settings → Access tokens")
return i18n("Create a token at %1 and tick <b>Desktop widget</b> (repo:read, ci:read, ci:write). Without ci:write the widget is read-only.", where)
}
}
}
// ── watched ─────────────────────────────────────────────────────
Kirigami.Heading {
level: 3
text: i18n("Watching")
}
QQC2.Label {
visible: watched.length === 0
Layout.fillWidth: true
wrapMode: Text.WordWrap
opacity: 0.7
text: i18n("Nothing watched yet. Add repositories or whole users/organizations below.")
}
Repeater {
model: watched
delegate: RowLayout {
required property var modelData
required property int index
readonly property bool isNamespace: modelData.kind === "namespace"
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
source: isNamespace ? "group" : "folder-git"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: isNamespace
? i18n("%1/* (every repository)", modelData.handle)
: modelData.fullName
}
QQC2.CheckBox {
visible: isNamespace
text: i18n("One circle")
checked: !!modelData.grouped
onToggled: setGrouped(index, checked)
QQC2.ToolTip.text: i18n("Fold every repository in this namespace into a single circle showing the most urgent status")
QQC2.ToolTip.visible: hovered
}
QQC2.ToolButton {
icon.name: "go-up"
enabled: index > 0
onClicked: move(index, -1)
}
QQC2.ToolButton {
icon.name: "go-down"
enabled: index < watched.length - 1
onClicked: move(index, 1)
}
QQC2.ToolButton {
icon.name: "list-remove"
onClicked: removeAt(index)
QQC2.ToolTip.text: i18n("Stop watching")
QQC2.ToolTip.visible: hovered
}
}
}
RowLayout {
Layout.fillWidth: true
QQC2.TextField {
id: manualField
Layout.fillWidth: true
placeholderText: i18n("owner/repository, or just owner for everything they own")
onAccepted: addManual()
}
QQC2.Button {
text: i18n("Add")
icon.name: "list-add"
enabled: manualField.text.trim() !== ""
onClicked: addManual()
}
}
// ── server listing ──────────────────────────────────────────────
RowLayout {
Layout.fillWidth: true
Kirigami.Heading {
Layout.fillWidth: true
level: 3
text: i18n("Available on the server")
}
QQC2.BusyIndicator {
visible: isLoadingRepos
Layout.preferredWidth: Kirigami.Units.iconSizes.smallMedium
Layout.preferredHeight: Kirigami.Units.iconSizes.smallMedium
}
QQC2.Button {
text: repositories.length > 0 ? i18n("Reload") : i18n("List Repositories")
icon.name: "view-refresh"
enabled: canConnect && !isLoadingRepos
onClicked: loadRepositories()
}
}
QQC2.Label {
visible: loadReposError !== ""
Layout.fillWidth: true
wrapMode: Text.WordWrap
color: Kirigami.Theme.negativeTextColor
text: loadReposError
}
QQC2.Label {
visible: !canConnect
Layout.fillWidth: true
wrapMode: Text.WordWrap
opacity: 0.7
text: i18n("Enter a URL and token to browse repositories.")
}
// Namespaces: umbrella watches
QQC2.Label {
visible: namespaces.length > 0
text: i18n("Users & organizations")
font.bold: true
}
Repeater {
model: namespaces
delegate: RowLayout {
required property var modelData
Layout.fillWidth: true
spacing: Kirigami.Units.smallSpacing
Kirigami.Icon {
source: modelData.kind === "org" ? "group" : "user"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
textFormat: Text.StyledText
text: "<b>" + modelData.handle + "</b>"
+ (modelData.name && modelData.name !== modelData.handle ? " — " + modelData.name : "")
+ " <font color=\"" + Kirigami.Theme.disabledTextColor + "\">"
+ i18np("(%1 repository)", "(%1 repositories)", repoCount(modelData.handle)) + "</font>"
}
QQC2.Button {
text: isWatchedNamespace(modelData.handle) ? i18n("Watching") : i18n("Watch all")
icon.name: isWatchedNamespace(modelData.handle) ? "checkmark" : "list-add"
enabled: !isWatchedNamespace(modelData.handle)
onClicked: addNamespace(modelData.handle)
}
}
}
// Individual repositories
QQC2.Label {
visible: repositories.length > 0
text: i18n("Repositories")
font.bold: true
}
Kirigami.SearchField {
id: searchField
visible: repositories.length > 0
Layout.fillWidth: true
placeholderText: i18n("Filter repositories…")
}
QQC2.ScrollView {
visible: repositories.length > 0
Layout.fillWidth: true
Layout.preferredHeight: Kirigami.Units.gridUnit * 14
ListView {
id: repoList
clip: true
model: filteredRepos
delegate: QQC2.ItemDelegate {
id: repoDelegate
required property var modelData
readonly property bool explicit: isWatchedRepo(modelData.full_name)
readonly property bool viaNamespace: isWatchedNamespace(modelData.owner)
width: repoList.width
contentItem: RowLayout {
spacing: Kirigami.Units.smallSpacing
StatusCircle {
status: repoDelegate.modelData.ci_status || "unknown"
Layout.preferredWidth: Kirigami.Units.iconSizes.small
Layout.preferredHeight: Kirigami.Units.iconSizes.small
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
QQC2.Label {
Layout.fillWidth: true
elide: Text.ElideRight
text: repoDelegate.modelData.full_name
+ (repoDelegate.modelData.archived ? " " + i18n("(archived)") : "")
}
QQC2.Label {
visible: !!repoDelegate.modelData.description
Layout.fillWidth: true
elide: Text.ElideRight
font: Kirigami.Theme.smallFont
opacity: 0.7
text: repoDelegate.modelData.description || ""
}
}
QQC2.Label {
visible: repoDelegate.viaNamespace && !repoDelegate.explicit
font: Kirigami.Theme.smallFont
opacity: 0.7
text: i18n("via %1", repoDelegate.modelData.owner)
}
QQC2.Button {
text: repoDelegate.explicit ? i18n("Watching") : i18n("Add")
icon.name: repoDelegate.explicit ? "checkmark" : "list-add"
enabled: !repoDelegate.explicit
onClicked: addRepo(repoDelegate.modelData.full_name)
}
}
}
QQC2.Label {
anchors.centerIn: parent
visible: repoList.count === 0
opacity: 0.7
text: i18n("No matching repositories")
}
}
}
}
}
neiam
/gitgud-plasmoid
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
16.9 KiB · text
History
db86591