Formatting

This commit is contained in:
quadbyte 2025-08-14 14:58:00 -04:00
parent 3f64bb1879
commit 151e2b6aaf
7 changed files with 869 additions and 840 deletions

View file

@ -13,273 +13,270 @@ import "../../Helpers/FuzzySort.js" as Fuzzysort
import "../../Helpers/MathHelper.js" as MathHelper import "../../Helpers/MathHelper.js" as MathHelper
NLoader { NLoader {
id: appLauncher id: appLauncher
isLoaded: false isLoaded: false
// Clipboard state is persisted in Services/Clipboard.qml // Clipboard state is persisted in Services/Clipboard.qml
content: Component { content: Component {
NPanel { NPanel {
id: appLauncherPanel id: appLauncherPanel
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
// No local timer/processes; use persistent Clipboard service
// Removed local clipboard processes; handled by Clipboard service // No local timer/processes; use persistent Clipboard service
// Removed local clipboard processes; handled by Clipboard service
// Copy helpers via simple exec; avoid keeping processes alive locally
function copyImageBase64(mime, base64) {
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`])
}
// Copy helpers via simple exec; avoid keeping processes alive locally function copyText(text) {
function copyImageBase64(mime, base64) { Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`])
Quickshell.execDetached(["sh", "-lc", `printf %s ${base64} | base64 -d | wl-copy -t '${mime}'`]) }
}
function copyText(text) { function updateClipboardHistory() {
Quickshell.execDetached(["sh", "-lc", `printf %s ${text} | wl-copy -t text/plain;charset=utf-8`]) Clipboard.refresh()
} }
function selectNext() {
if (filteredEntries.length > 0) {
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1)
}
}
function selectPrev() {
if (filteredEntries.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0)
}
}
function updateClipboardHistory() { function activateSelected() {
Clipboard.refresh(); if (filteredEntries.length === 0)
} return
function selectNext() { var modelData = filteredEntries[selectedIndex]
if (filteredEntries.length > 0) { if (modelData && modelData.execute) {
selectedIndex = Math.min(selectedIndex + 1, filteredEntries.length - 1); if (modelData.isCommand) {
modelData.execute()
return
} else {
modelData.execute()
}
appLauncherPanel.hide()
}
}
property var desktopEntries: DesktopEntries.applications.values
property string searchText: ""
property int selectedIndex: 0
property var filteredEntries: {
console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
if (!desktopEntries || desktopEntries.length === 0) {
console.log("[AppLauncher] No desktop entries available")
return []
}
// Filter out entries that shouldn't be displayed
var visibleEntries = desktopEntries.filter(entry => {
if (!entry || entry.noDisplay) {
return false
}
return true
})
console.log("[AppLauncher] Visible entries:", visibleEntries.length)
var query = searchText ? searchText.toLowerCase() : ""
var results = []
// Handle special commands
if (query === ">") {
results.push({
"isCommand": true,
"name": ">calc",
"content": "Calculator - evaluate mathematical expressions",
"icon": "calculate",
"execute": function () {
searchText = ">calc "
searchInput.cursorPosition = searchText.length
}
})
results.push({
"isCommand": true,
"name": ">clip",
"content": "Clipboard history - browse and restore clipboard items",
"icon": "content_paste",
"execute": function () {
searchText = ">clip "
searchInput.cursorPosition = searchText.length
}
})
return results
}
// Handle clipboard history
if (query.startsWith(">clip")) {
if (!Clipboard.initialized) {
Clipboard.refresh()
}
const searchTerm = query.slice(5).trim()
Clipboard.history.forEach(function (clip, index) {
let searchContent = clip.type === 'image' ? clip.mimeType : clip.content || clip
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
let entry
if (clip.type === 'image') {
entry = {
"isClipboard": true,
"name": "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
"content": "Image: " + clip.mimeType,
"icon": "image",
"type": 'image',
"data": clip.data,
"execute": function () {
const base64Data = clip.data.split(',')[1]
copyImageBase64(clip.mimeType, base64Data)
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType])
}
} }
} } else {
const textContent = clip.content || clip
let displayContent = textContent
let previewContent = ""
function selectPrev() { displayContent = displayContent.replace(/\s+/g, ' ').trim()
if (filteredEntries.length > 0) {
selectedIndex = Math.max(selectedIndex - 1, 0); if (displayContent.length > 50) {
previewContent = displayContent
displayContent = displayContent.split('\n')[0].substring(0, 50) + "..."
} }
entry = {
"isClipboard": true,
"name": displayContent,
"content": previewContent || textContent,
"icon": "content_paste",
"execute": function () {
Quickshell.clipboardText = String(textContent)
copyText(String(textContent))
var preview = (textContent.length > 50) ? textContent.slice(0, 50) + "…" : textContent
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview])
}
}
}
results.push(entry)
} }
})
function activateSelected() { if (results.length === 0) {
if (filteredEntries.length === 0) return; results.push({
"isClipboard": true,
"name": "No clipboard history",
"content": "No matching clipboard entries found",
"icon": "content_paste_off"
})
}
var modelData = filteredEntries[selectedIndex]; return results
if (modelData && modelData.execute) { }
if (modelData.isCommand) {
modelData.execute(); // Handle calculator
return; if (query.startsWith(">calc")) {
} else { var expr = searchText.slice(5).trim()
modelData.execute(); if (expr && isMathExpression(expr)) {
var value = safeEval(expr)
if (value !== null && value !== undefined && value !== "") {
var formattedResult = MathHelper.MathHelper.formatResult(value)
results.push({
"isCalculator": true,
"name": `Calculator: ${expr} = ${formattedResult}`,
"result": value,
"expr": expr,
"icon": "calculate",
"execute": function () {
Quickshell.clipboardText = String(formattedResult)
clipboardTextCopyProcess.copyText(String(formattedResult))
Quickshell.execDetached(
["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`])
}
})
}
}
return results
}
// Regular app search
if (!query) {
results = results.concat(visibleEntries.sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase())
}))
} else {
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
"keys": ["name", "comment", "genericName"]
})
results = results.concat(fuzzyResults.map(function (r) {
return r.obj
}))
}
console.log("[AppLauncher] Filtered entries:", results.length)
return results
}
Component.onCompleted: {
console.log("[AppLauncher] Component completed")
console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined')
if (typeof DesktopEntries !== 'undefined') {
console.log("[AppLauncher] DesktopEntries.entries:",
DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
}
// Start clipboard refresh immediately on open
updateClipboardHistory()
}
function isMathExpression(str) {
// Allow more characters for enhanced math functions
return /^[-+*/().0-9\s\w]+$/.test(str)
}
function safeEval(expr) {
return MathHelper.MathHelper.evaluate(expr)
}
// Main content container
Rectangle {
anchors.centerIn: parent
width: Math.min(700 * scaling, parent.width * 0.75)
height: Math.min(550 * scaling, parent.height * 0.8)
radius: 32 * scaling
color: Colors.backgroundPrimary
border.color: Colors.outline
border.width: Style.borderThin * scaling
// Subtle gradient background
gradient: Gradient {
GradientStop {
position: 0.0
color: Qt.lighter(Colors.backgroundPrimary, 1.02)
}
GradientStop {
position: 1.0
color: Qt.darker(Colors.backgroundPrimary, 1.1)
}
} }
appLauncherPanel.hide();
}
}
property var desktopEntries: DesktopEntries.applications.values
property string searchText: ""
property int selectedIndex: 0
property var filteredEntries: {
console.log("[AppLauncher] Total desktop entries:", desktopEntries ? desktopEntries.length : 0)
if (!desktopEntries || desktopEntries.length === 0) {
console.log("[AppLauncher] No desktop entries available")
return []
}
// Filter out entries that shouldn't be displayed
var visibleEntries = desktopEntries.filter(entry => {
if (!entry || entry.noDisplay) {
return false
}
return true
})
console.log("[AppLauncher] Visible entries:", visibleEntries.length)
var query = searchText ? searchText.toLowerCase() : "";
var results = [];
// Handle special commands
if (query === ">") {
results.push({
isCommand: true,
name: ">calc",
content: "Calculator - evaluate mathematical expressions",
icon: "calculate",
execute: function() {
searchText = ">calc ";
searchInput.cursorPosition = searchText.length;
}
});
results.push({
isCommand: true,
name: ">clip",
content: "Clipboard history - browse and restore clipboard items",
icon: "content_paste",
execute: function() {
searchText = ">clip ";
searchInput.cursorPosition = searchText.length;
}
});
return results;
}
// Handle clipboard history
if (query.startsWith(">clip")) {
if (!Clipboard.initialized) {
Clipboard.refresh();
}
const searchTerm = query.slice(5).trim();
Clipboard.history.forEach(function(clip, index) {
let searchContent = clip.type === 'image' ?
clip.mimeType :
clip.content || clip;
if (!searchTerm || searchContent.toLowerCase().includes(searchTerm)) {
let entry;
if (clip.type === 'image') {
entry = {
isClipboard: true,
name: "Image from " + new Date(clip.timestamp).toLocaleTimeString(),
content: "Image: " + clip.mimeType,
icon: "image",
type: 'image',
data: clip.data,
execute: function() {
const base64Data = clip.data.split(',')[1];
copyImageBase64(clip.mimeType, base64Data);
Quickshell.execDetached(["notify-send", "Clipboard", "Image copied: " + clip.mimeType]);
}
};
} else {
const textContent = clip.content || clip;
let displayContent = textContent;
let previewContent = "";
displayContent = displayContent.replace(/\s+/g, ' ').trim();
if (displayContent.length > 50) {
previewContent = displayContent;
displayContent = displayContent.split('\n')[0].substring(0, 50) + "...";
}
entry = {
isClipboard: true,
name: displayContent,
content: previewContent || textContent,
icon: "content_paste",
execute: function() {
Quickshell.clipboardText = String(textContent);
copyText(String(textContent));
var preview = (textContent.length > 50) ? textContent.slice(0,50) + "…" : textContent;
Quickshell.execDetached(["notify-send", "Clipboard", "Text copied: " + preview]);
}
};
}
results.push(entry);
}
});
if (results.length === 0) {
results.push({
isClipboard: true,
name: "No clipboard history",
content: "No matching clipboard entries found",
icon: "content_paste_off"
});
}
return results;
}
// Handle calculator
if (query.startsWith(">calc")) {
var expr = searchText.slice(5).trim();
if (expr && isMathExpression(expr)) {
var value = safeEval(expr);
if (value !== null && value !== undefined && value !== "") {
var formattedResult = MathHelper.MathHelper.formatResult(value);
results.push({
isCalculator: true,
name: `Calculator: ${expr} = ${formattedResult}`,
result: value,
expr: expr,
icon: "calculate",
execute: function() {
Quickshell.clipboardText = String(formattedResult);
clipboardTextCopyProcess.copyText(String(formattedResult));
Quickshell.execDetached(["notify-send", "Calculator Result", `${expr} = ${formattedResult} (copied to clipboard)`]);
}
});
}
}
return results;
}
// Regular app search
if (!query) {
results = results.concat(visibleEntries.sort(function (a, b) {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
}));
} else {
var fuzzyResults = Fuzzysort.go(query, visibleEntries, {
keys: ["name", "comment", "genericName"]
});
results = results.concat(fuzzyResults.map(function (r) {
return r.obj;
}));
}
console.log("[AppLauncher] Filtered entries:", results.length)
return results
}
Component.onCompleted: { ColumnLayout {
console.log("[AppLauncher] Component completed") anchors.fill: parent
console.log("[AppLauncher] DesktopEntries available:", typeof DesktopEntries !== 'undefined') anchors.margins: Style.marginLarge * scaling
if (typeof DesktopEntries !== 'undefined') { spacing: Style.marginMedium * scaling
console.log("[AppLauncher] DesktopEntries.entries:", DesktopEntries.entries ? DesktopEntries.entries.length : 'undefined')
}
// Start clipboard refresh immediately on open
updateClipboardHistory();
}
function isMathExpression(str) { // Search bar
// Allow more characters for enhanced math functions Rectangle {
return /^[-+*/().0-9\s\w]+$/.test(str);
}
function safeEval(expr) {
return MathHelper.MathHelper.evaluate(expr);
}
// Main content container
Rectangle {
anchors.centerIn: parent
width: Math.min(700 * scaling, parent.width * 0.75)
height: Math.min(550 * scaling, parent.height * 0.8)
radius: 32 * scaling
color: Colors.backgroundPrimary
border.color: Colors.outline
border.width: Style.borderThin * scaling
// Subtle gradient background
gradient: Gradient {
GradientStop { position: 0.0; color: Qt.lighter(Colors.backgroundPrimary, 1.02) }
GradientStop { position: 1.0; color: Qt.darker(Colors.backgroundPrimary, 1.1) }
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Style.marginLarge * scaling
spacing: Style.marginMedium * scaling
// Search bar
Rectangle {
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 40 * scaling Layout.preferredHeight: 40 * scaling
Layout.bottomMargin: Style.marginMedium * scaling Layout.bottomMargin: Style.marginMedium * scaling
@ -289,67 +286,71 @@ NLoader {
border.width: searchInput.activeFocus ? 2 : 1 border.width: searchInput.activeFocus ? 2 : 1
Row { Row {
anchors.fill: parent anchors.fill: parent
anchors.margins: 12 * scaling anchors.margins: 12 * scaling
spacing: 10 * scaling spacing: 10 * scaling
Text { Text {
text: "search" text: "search"
font.family: "Material Symbols Outlined" font.family: "Material Symbols Outlined"
font.pointSize: 16 * scaling font.pointSize: 16 * scaling
color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary color: searchInput.activeFocus ? Colors.accentPrimary : Colors.textSecondary
}
TextField {
id: searchInput
placeholderText: "Search applications..."
color: Colors.textPrimary
placeholderTextColor: Colors.textSecondary
background: null
font.pointSize: 13 * scaling
Layout.fillWidth: true
onTextChanged: {
searchText = text
selectedIndex = 0 // Reset selection when search changes
} }
selectedTextColor: Colors.textPrimary
TextField { selectionColor: Colors.accentPrimary
id: searchInput padding: 0
placeholderText: "Search applications..." verticalAlignment: TextInput.AlignVCenter
color: Colors.textPrimary leftPadding: 0
placeholderTextColor: Colors.textSecondary rightPadding: 0
background: null topPadding: 0
font.pointSize: 13 * scaling bottomPadding: 0
Layout.fillWidth: true font.bold: true
onTextChanged: { Component.onCompleted: {
searchText = text; contentItem.cursorColor = Colors.textPrimary
selectedIndex = 0; // Reset selection when search changes contentItem.verticalAlignment = TextInput.AlignVCenter
} // Focus the search bar by default
selectedTextColor: Colors.textPrimary Qt.callLater(() => {
selectionColor: Colors.accentPrimary searchInput.forceActiveFocus()
padding: 0 })
verticalAlignment: TextInput.AlignVCenter
leftPadding: 0
rightPadding: 0
topPadding: 0
bottomPadding: 0
font.bold: true
Component.onCompleted: {
contentItem.cursorColor = Colors.textPrimary
contentItem.verticalAlignment = TextInput.AlignVCenter
// Focus the search bar by default
Qt.callLater(() => {
searchInput.forceActiveFocus()
})
}
onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary
Keys.onDownPressed: selectNext()
Keys.onUpPressed: selectPrev()
Keys.onEnterPressed: activateSelected()
Keys.onReturnPressed: activateSelected()
Keys.onEscapePressed: appLauncherPanel.hide()
} }
onActiveFocusChanged: contentItem.cursorColor = Colors.textPrimary
Keys.onDownPressed: selectNext()
Keys.onUpPressed: selectPrev()
Keys.onEnterPressed: activateSelected()
Keys.onReturnPressed: activateSelected()
Keys.onEscapePressed: appLauncherPanel.hide()
}
} }
Behavior on border.color { Behavior on border.color {
ColorAnimation { duration: 120 } ColorAnimation {
duration: 120
}
} }
Behavior on border.width { Behavior on border.width {
NumberAnimation { duration: 120 } NumberAnimation {
duration: 120
}
} }
} }
// Applications list // Applications list
ScrollView { ScrollView {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true clip: true
@ -357,163 +358,176 @@ NLoader {
ScrollBar.vertical.policy: ScrollBar.AsNeeded ScrollBar.vertical.policy: ScrollBar.AsNeeded
ListView { ListView {
id: appsList id: appsList
anchors.fill: parent anchors.fill: parent
spacing: 4 * scaling spacing: 4 * scaling
model: filteredEntries model: filteredEntries
currentIndex: selectedIndex currentIndex: selectedIndex
delegate: Rectangle { delegate: Rectangle {
width: appsList.width - Style.marginSmall * scaling width: appsList.width - Style.marginSmall * scaling
height: 56 * scaling height: 56 * scaling
radius: 16 * scaling radius: 16 * scaling
property bool isSelected: index === selectedIndex property bool isSelected: index === selectedIndex
color: (appCardArea.containsMouse || isSelected) ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundSecondary color: (appCardArea.containsMouse || isSelected) ? Qt.darker(
border.color: (appCardArea.containsMouse || isSelected) ? Colors.accentPrimary : "transparent" Colors.accentPrimary,
border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0 1.1) : Colors.backgroundSecondary
border.color: (appCardArea.containsMouse
Behavior on color { || isSelected) ? Colors.accentPrimary : "transparent"
ColorAnimation { duration: 150 } border.width: (appCardArea.containsMouse || isSelected) ? 2 : 0
}
Behavior on border.color {
ColorAnimation { duration: 150 }
}
Behavior on border.width {
NumberAnimation { duration: 150 }
}
RowLayout { Behavior on color {
anchors.fill: parent ColorAnimation {
anchors.margins: Style.marginMedium * scaling duration: 150
spacing: Style.marginMedium * scaling }
// App icon with background
Rectangle {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
radius: 14 * scaling
color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary, 1.1) : Colors.backgroundTertiary
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand) || (iconImg.status === Image.Ready && iconImg.source !== "" && iconImg.status !== Image.Error && iconImg.source !== "")
visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode
// Clipboard image display
Image {
id: clipboardImage
anchors.fill: parent
anchors.margins: 6 * scaling
visible: modelData.type === 'image'
source: modelData.data || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
}
IconImage {
id: iconImg
anchors.fill: parent
anchors.margins: 6 * scaling
asynchronous: true
source: modelData.isCalculator ? "calculate" :
modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") :
modelData.isCommand ? modelData.icon :
(modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "")
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand || parent.iconLoaded) && modelData.type !== 'image'
}
// Fallback icon container
Rectangle {
anchors.fill: parent
anchors.margins: 6 * scaling
radius: 10 * scaling
color: Colors.accentPrimary
opacity: 0.3
visible: !parent.iconLoaded
}
Text {
anchors.centerIn: parent
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard || modelData.isCommand)
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: 18 * scaling
font.weight: Font.Bold
color: Colors.accentPrimary
}
Behavior on color {
ColorAnimation { duration: 150 }
}
}
// App info
ColumnLayout {
Layout.fillWidth: true
spacing: 2 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: 14 * scaling
font.weight: Font.Bold
color: Colors.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) :
modelData.isClipboard ? modelData.content :
modelData.isCommand ? modelData.content :
(modelData.genericName || modelData.comment || "")
font.pointSize: 11 * scaling
color: (appCardArea.containsMouse || isSelected) ? Colors.textPrimary : Colors.textSecondary
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
}
MouseArea {
id: appCardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index;
activateSelected();
}
}
} }
}
}
// No results message Behavior on border.color {
NText { ColorAnimation {
duration: 150
}
}
Behavior on border.width {
NumberAnimation {
duration: 150
}
}
RowLayout {
anchors.fill: parent
anchors.margins: Style.marginMedium * scaling
spacing: Style.marginMedium * scaling
// App icon with background
Rectangle {
Layout.preferredWidth: 40 * scaling
Layout.preferredHeight: 40 * scaling
radius: 14 * scaling
color: appCardArea.containsMouse ? Qt.darker(Colors.accentPrimary,
1.1) : Colors.backgroundTertiary
property bool iconLoaded: (modelData.isCalculator || modelData.isClipboard
|| modelData.isCommand) || (iconImg.status === Image.Ready
&& iconImg.source !== ""
&& iconImg.status !== Image.Error
&& iconImg.source !== "")
visible: !searchText.startsWith(">calc") // Hide icon when in calculator mode
// Clipboard image display
Image {
id: clipboardImage
anchors.fill: parent
anchors.margins: 6 * scaling
visible: modelData.type === 'image'
source: modelData.data || ""
fillMode: Image.PreserveAspectCrop
asynchronous: true
cache: true
}
IconImage {
id: iconImg
anchors.fill: parent
anchors.margins: 6 * scaling
asynchronous: true
source: modelData.isCalculator ? "calculate" : modelData.isClipboard ? (modelData.type === 'image' ? "" : "content_paste") : modelData.isCommand ? modelData.icon : (modelData.icon ? Quickshell.iconPath(modelData.icon, "application-x-executable") : "")
visible: (modelData.isCalculator || modelData.isClipboard || modelData.isCommand
|| parent.iconLoaded) && modelData.type !== 'image'
}
// Fallback icon container
Rectangle {
anchors.fill: parent
anchors.margins: 6 * scaling
radius: 10 * scaling
color: Colors.accentPrimary
opacity: 0.3
visible: !parent.iconLoaded
}
Text {
anchors.centerIn: parent
visible: !parent.iconLoaded && !(modelData.isCalculator || modelData.isClipboard
|| modelData.isCommand)
text: modelData.name ? modelData.name.charAt(0).toUpperCase() : "?"
font.pointSize: 18 * scaling
font.weight: Font.Bold
color: Colors.accentPrimary
}
Behavior on color {
ColorAnimation {
duration: 150
}
}
}
// App info
ColumnLayout {
Layout.fillWidth: true
spacing: 2 * scaling
NText {
text: modelData.name || "Unknown"
font.pointSize: 14 * scaling
font.weight: Font.Bold
color: Colors.textPrimary
elide: Text.ElideRight
Layout.fillWidth: true
}
NText {
text: modelData.isCalculator ? (modelData.expr + " = " + modelData.result) : modelData.isClipboard ? modelData.content : modelData.isCommand ? modelData.content : (modelData.genericName || modelData.comment || "")
font.pointSize: 11 * scaling
color: (appCardArea.containsMouse
|| isSelected) ? Colors.textPrimary : Colors.textSecondary
elide: Text.ElideRight
Layout.fillWidth: true
visible: text !== ""
}
}
}
MouseArea {
id: appCardArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedIndex = index
activateSelected()
}
}
}
}
}
// No results message
NText {
text: searchText.trim() !== "" ? "No applications found" : "No applications available" text: searchText.trim() !== "" ? "No applications found" : "No applications available"
font.pointSize: Style.fontSizeLarge * scaling font.pointSize: Style.fontSizeLarge * scaling
color: Colors.textSecondary color: Colors.textSecondary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
visible: filteredEntries.length === 0 visible: filteredEntries.length === 0
} }
// Results count // Results count
NText { NText {
text: searchText.startsWith(">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length !== 1 ? 's' : ''}` : text: searchText.startsWith(
searchText.startsWith(">calc") ? `${filteredEntries.length} result${filteredEntries.length !== 1 ? 's' : ''}` : ">clip") ? `${filteredEntries.length} clipboard item${filteredEntries.length
`${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}` !== 1 ? 's' : ''}` : searchText.startsWith(
">calc") ? `${filteredEntries.length} result${filteredEntries.length
!== 1 ? 's' : ''}` : `${filteredEntries.length} application${filteredEntries.length !== 1 ? 's' : ''}`
font.pointSize: Style.fontSizeSmall * scaling font.pointSize: Style.fontSizeSmall * scaling
color: Colors.textSecondary color: Colors.textSecondary
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
Layout.fillWidth: true Layout.fillWidth: true
visible: searchText.trim() !== "" visible: searchText.trim() !== ""
}
} }
} }
} }
} }
} }
}

View file

@ -9,12 +9,12 @@ import qs.Services
import qs.Widgets import qs.Widgets
NLoader { NLoader {
isLoaded: Settings.data.general.showDock isLoaded: Settings.data.general.showDock
content: Component { content: Component {
Variants { Variants {
model: Quickshell.screens model: Quickshell.screens
Item { Item {
property var modelData property var modelData
readonly property real scaling: Scaling.scale(modelData) readonly property real scaling: Scaling.scale(modelData)
@ -38,308 +38,321 @@ NLoader {
property var contextMenuToplevel: null property var contextMenuToplevel: null
PanelWindow { PanelWindow {
id: dockWindow id: dockWindow
visible: true visible: true
screen: modelData screen: modelData
exclusionMode: ExclusionMode.Ignore
anchors.bottom: true
anchors.left: true
anchors.right: true
focusable: false
color: "transparent"
implicitHeight: 60
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: if (autoHide && !dockHovered && !anyAppHovered)
hidden = true
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
// Mouse area at screen bottom to detect entry and keep dock visible
MouseArea {
id: screenEdgeMouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 10
hoverEnabled: true
propagateComposedEvents: true
onEntered: if (autoHide && hidden)
showTimer.start()
onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered)
hideTimer.start()
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
MouseArea {
anchors.fill: parent
enabled: contextMenuVisible
onClicked: {
contextMenuVisible = false
contextMenuTarget = null
contextMenuToplevel = null
}
}
Rectangle {
id: dockContainer
width: dock.width + 40
height: 50
color: Colors.backgroundSecondary
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
topLeftRadius: 20
topRightRadius: 20
MouseArea {
id: dockMouseArea
anchors.fill: parent
hoverEnabled: true
propagateComposedEvents: true
onEntered: {
dockHovered = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
dockHovered = false
if (autoHide && !anyAppHovered && !contextMenuVisible)
hideTimer.start()
}
}
Item {
id: dock
width: runningAppsRow.width
height: parent.height - 10
anchors.centerIn: parent
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel)
return ""
let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true)
if (!icon)
icon = Quickshell.iconPath(toplevel.appId, true)
if (!icon)
icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true)
if (!icon)
icon = Quickshell.iconPath(toplevel.title, true)
return icon || Quickshell.iconPath("application-x-executable", true)
}
Row {
id: runningAppsRow
spacing: 8
height: parent.height
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle {
id: appButton
width: 36
height: 36
radius: 18
color: "transparent"
property bool isActive: ToplevelManager.activeToplevel
&& ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
Behavior on color {
ColorAnimation {
duration: 150
}
}
Image {
id: appIcon
width: 28
height: 28
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: false
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
}
Text {
anchors.centerIn: parent
visible: !appIcon.visible
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
font.pixelSize: 14
font.bold: true
color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !contextMenuVisible)
hideTimer.start()
}
onClicked: function (mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
if (mouse.button === Qt.RightButton) {
appTooltip.hide()
contextMenuTarget = appButton
contextMenuToplevel = modelData
contextMenuVisible = true
}
}
}
Rectangle {
visible: isActive
width: 20
height: 3
color: Colors.accentPrimary
radius: 1.5
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 2
}
}
}
}
}
}
// Context Menu
PanelWindow {
id: contextMenuWindow
visible: contextMenuVisible
screen: dockWindow.screen
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
anchors.bottom: true anchors.bottom: true
anchors.left: true anchors.left: true
anchors.right: true anchors.right: true
focusable: false
color: "transparent" color: "transparent"
implicitHeight: 60 focusable: false
// Timer for auto-hide delay
Timer {
id: hideTimer
interval: hideDelay
onTriggered: if (autoHide && !dockHovered && !anyAppHovered) hidden = true
}
// Timer for show delay
Timer {
id: showTimer
interval: showDelay
onTriggered: hidden = false
}
// Behavior for smooth hide/show animations
Behavior on margins.bottom {
NumberAnimation {
duration: hidden ? hideAnimationDuration : showAnimationDuration
easing.type: Easing.InOutQuad
}
}
// Mouse area at screen bottom to detect entry and keep dock visible
MouseArea {
id: screenEdgeMouseArea
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: 10
hoverEnabled: true
propagateComposedEvents: true
onEntered: if (autoHide && hidden) showTimer.start()
onExited: if (autoHide && !hidden && !dockHovered && !anyAppHovered) hideTimer.start()
}
margins.bottom: hidden ? -(fullHeight - peekHeight) : 0
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
enabled: contextMenuVisible onClicked: {
onClicked: { contextMenuVisible = false
contextMenuVisible = false contextMenuTarget = null
contextMenuTarget = null contextMenuToplevel = null
contextMenuToplevel = null hidden = true // Hide dock when context menu closes
} }
} }
Rectangle { Rectangle {
id: dockContainer id: contextMenuContainer
width: dock.width + 40 width: 80
height: 50 height: 32
color: Colors.backgroundSecondary radius: 8
anchors.horizontalCenter: parent.horizontalCenter color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary
anchors.bottom: parent.bottom border.color: Colors.outline
topLeftRadius: 20 border.width: 1
topRightRadius: 20
MouseArea { x: {
id: dockMouseArea if (!contextMenuTarget)
anchors.fill: parent return 0
hoverEnabled: true const pos = contextMenuTarget.mapToItem(null, 0, 0)
propagateComposedEvents: true let xPos = pos.x + (contextMenuTarget.width - width) / 2
return Math.max(0, Math.min(xPos, dockWindow.width - width))
}
onEntered: { y: {
dockHovered = true if (!contextMenuTarget)
if (autoHide) { return 0
showTimer.stop() const pos = contextMenuTarget.mapToItem(null, 0, 0)
hideTimer.stop() return pos.y - height + 32
hidden = false }
}
} Text {
onExited: { anchors.centerIn: parent
dockHovered = false text: "Close"
if (autoHide && !anyAppHovered && !contextMenuVisible) hideTimer.start() font.pixelSize: 14
} color: Colors.textPrimary
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenuToplevel?.close)
contextMenuToplevel.close()
contextMenuVisible = false
hidden = true
} }
}
Item { // Animation
id: dock scale: contextMenuVisible ? 1 : 0.9
width: runningAppsRow.width opacity: contextMenuVisible ? 1 : 0
height: parent.height - 10 transformOrigin: Item.Bottom
anchors.centerIn: parent
NTooltip {
id: appTooltip
visible: false
positionAbove: true
}
function getAppIcon(toplevel: Toplevel): string {
if (!toplevel) return "";
let icon = Quickshell.iconPath(toplevel.appId?.toLowerCase(), true);
if (!icon) icon = Quickshell.iconPath(toplevel.appId, true);
if (!icon) icon = Quickshell.iconPath(toplevel.title?.toLowerCase(), true);
if (!icon) icon = Quickshell.iconPath(toplevel.title, true);
return icon || Quickshell.iconPath("application-x-executable", true);
}
Row {
id: runningAppsRow
spacing: 8
height: parent.height
anchors.centerIn: parent
Repeater {
model: ToplevelManager ? ToplevelManager.toplevels : null
delegate: Rectangle {
id: appButton
width: 36
height: 36
radius: 18
color:"transparent"
property bool isActive: ToplevelManager.activeToplevel && ToplevelManager.activeToplevel === modelData
property bool hovered: appMouseArea.containsMouse
property string appId: modelData ? modelData.appId : ""
property string appTitle: modelData ? modelData.title : ""
Behavior on color { ColorAnimation { duration: 150 } }
Image {
id: appIcon
width: 28
height: 28
anchors.centerIn: parent
source: dock.getAppIcon(modelData)
visible: source.toString() !== ""
smooth: false
mipmap: false
antialiasing: false
fillMode: Image.PreserveAspectFit
}
Text {
anchors.centerIn: parent
visible: !appIcon.visible
text: appButton.appId ? appButton.appId.charAt(0).toUpperCase() : "?"
font.pixelSize: 14
font.bold: true
color: appButton.isActive ? Colors.accentPrimary : Colors.textPrimary
}
MouseArea {
id: appMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onEntered: {
anyAppHovered = true
const appName = appButton.appTitle || appButton.appId || "Unknown"
appTooltip.text = appName.length > 40 ? appName.substring(0, 37) + "..." : appName
appTooltip.target = appButton
appTooltip.isVisible = true
if (autoHide) {
showTimer.stop()
hideTimer.stop()
hidden = false
}
}
onExited: {
anyAppHovered = false
appTooltip.hide()
if (autoHide && !dockHovered && !contextMenuVisible) hideTimer.start()
}
onClicked: function(mouse) {
if (mouse.button === Qt.MiddleButton && modelData?.close) {
modelData.close()
}
if (mouse.button === Qt.LeftButton && modelData?.activate) {
modelData.activate()
}
if (mouse.button === Qt.RightButton) {
appTooltip.hide()
contextMenuTarget = appButton
contextMenuToplevel = modelData
contextMenuVisible = true
}
}
}
Rectangle {
visible: isActive
width: 20
height: 3
color: Colors.accentPrimary
radius: 1.5
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: 2
}
}
}
}
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutBack
} }
}
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
} }
}
// Context Menu
PanelWindow {
id: contextMenuWindow
visible: contextMenuVisible
screen: dockWindow.screen
exclusionMode: ExclusionMode.Ignore
anchors.bottom: true
anchors.left: true
anchors.right: true
color: "transparent"
focusable: false
MouseArea {
anchors.fill: parent
onClicked: {
contextMenuVisible = false
contextMenuTarget = null
contextMenuToplevel = null
hidden = true // Hide dock when context menu closes
}
}
Rectangle {
id: contextMenuContainer
width: 80
height: 32
radius: 8
color: closeMouseArea.containsMouse ? Colors.hover : Colors.backgroundPrimary
border.color: Colors.outline
border.width: 1
x: {
if (!contextMenuTarget) return 0
const pos = contextMenuTarget.mapToItem(null, 0, 0)
let xPos = pos.x + (contextMenuTarget.width - width) / 2
return Math.max(0, Math.min(xPos, dockWindow.width - width))
}
y: {
if (!contextMenuTarget) return 0
const pos = contextMenuTarget.mapToItem(null, 0, 0)
return pos.y - height + 32
}
Text {
anchors.centerIn: parent
text: "Close"
font.pixelSize: 14
color: Colors.textPrimary
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (contextMenuToplevel?.close) contextMenuToplevel.close()
contextMenuVisible = false
hidden = true
}
}
// Animation
scale: contextMenuVisible ? 1 : 0.9
opacity: contextMenuVisible ? 1 : 0
transformOrigin: Item.Bottom
Behavior on scale {
NumberAnimation {
duration: 150
easing.type: Easing.OutBack
}
}
Behavior on opacity {
NumberAnimation { duration: 100 }
}
}
}
} }
}
} }
}
} }
}
}

View file

@ -540,7 +540,7 @@ WlSessionLock {
Text { Text {
anchors.centerIn: parent anchors.centerIn: parent
text: lock.authenticating ? "EXECUTING..." : "EXECUTE" text: lock.authenticating ? "EXECUTING" : "EXECUTE"
color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary color: executeButtonArea.containsMouse ? Colors.onAccent : Colors.accentPrimary
font.family: "DejaVu Sans Mono" font.family: "DejaVu Sans Mono"
font.pointSize: Style.fontSizeMedium font.pointSize: Style.fontSizeMedium

View file

@ -236,7 +236,8 @@ ColumnLayout {
} }
NText { NText {
text: (modelData.contributions || 0) + " " + ((modelData.contributions || 0) === 1 ? "commit" : "commits") text: (modelData.contributions || 0) + " " + ((modelData.contributions
|| 0) === 1 ? "commit" : "commits")
font.pointSize: Style.fontSizeSmall * scaling font.pointSize: Style.fontSizeSmall * scaling
color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary color: contributorArea.containsMouse ? Colors.backgroundPrimary : Colors.textSecondary
} }

View file

@ -6,134 +6,133 @@ import Quickshell.Io
import qs.Services import qs.Services
Singleton { Singleton {
id: root id: root
property var history: [] property var history: []
property bool initialized: false property bool initialized: false
// Internal state // Internal state
property bool _enabled: true property bool _enabled: true
Timer { Timer {
interval: 1000 interval: 1000
repeat: true repeat: true
running: root._enabled running: root._enabled
onTriggered: root.refresh() onTriggered: root.refresh()
} }
// Detect current clipboard types (text/image) // Detect current clipboard types (text/image)
Process { Process {
id: typeProcess id: typeProcess
property bool isLoading: false property bool isLoading: false
property var currentTypes: [] property var currentTypes: []
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (exitCode === 0) { if (exitCode === 0) {
currentTypes = String(stdout.text).trim().split('\n').filter(t => t) currentTypes = String(stdout.text).trim().split('\n').filter(t => t)
const imageType = currentTypes.find(t => t.startsWith('image/')) const imageType = currentTypes.find(t => t.startsWith('image/'))
if (imageType) { if (imageType) {
imageProcess.mimeType = imageType imageProcess.mimeType = imageType
imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`] imageProcess.command = ["sh", "-c", `wl-paste -n -t "${imageType}" | base64 -w 0`]
imageProcess.running = true imageProcess.running = true
} else { } else {
textProcess.command = ["wl-paste", "-n", "--type", "text/plain"] textProcess.command = ["wl-paste", "-n", "--type", "text/plain"]
textProcess.running = true textProcess.running = true
}
} else {
typeProcess.isLoading = false
}
} }
} else {
stdout: StdioCollector {} typeProcess.isLoading = false
}
} }
// Read image data stdout: StdioCollector {}
Process { }
id: imageProcess
property string mimeType: ""
onExited: (exitCode, exitStatus) => { // Read image data
if (exitCode === 0) { Process {
const base64 = stdout.text.trim() id: imageProcess
if (base64) { property string mimeType: ""
const entry = {
type: 'image',
mimeType: mimeType,
data: `data:${mimeType};base64,${base64}`,
timestamp: new Date().getTime()
}
const exists = root.history.find(item => item.type === 'image' && item.data === entry.data) onExited: (exitCode, exitStatus) => {
if (!exists) { if (exitCode === 0) {
root.history = [entry, ...root.history].slice(0, 20) const base64 = stdout.text.trim()
} if (base64) {
} const entry = {
} "type": 'image',
"mimeType": mimeType,
"data": `data:${mimeType};base64,${base64}`,
"timestamp": new Date().getTime()
}
if (!textProcess.isLoading) { const exists = root.history.find(item => item.type === 'image' && item.data === entry.data)
root.initialized = true if (!exists) {
} root.history = [entry, ...root.history].slice(0, 20)
typeProcess.isLoading = false }
} }
}
stdout: StdioCollector {} if (!textProcess.isLoading) {
root.initialized = true
}
typeProcess.isLoading = false
} }
// Read text data stdout: StdioCollector {}
Process { }
id: textProcess
property bool isLoading: false
onExited: (exitCode, exitStatus) => { // Read text data
if (exitCode === 0) { Process {
const content = String(stdout.text).trim() id: textProcess
if (content) { property bool isLoading: false
const entry = {
type: 'text',
content: content,
timestamp: new Date().getTime()
}
const exists = root.history.find(item => { onExited: (exitCode, exitStatus) => {
if (item.type === 'text') { if (exitCode === 0) {
return item.content === content const content = String(stdout.text).trim()
} if (content) {
return item === content const entry = {
}) "type": 'text',
"content": content,
"timestamp": new Date().getTime()
}
if (!exists) { const exists = root.history.find(item => {
const newHistory = root.history.map(item => { if (item.type === 'text') {
if (typeof item === 'string') { return item.content === content
return { }
type: 'text', return item === content
content: item, })
timestamp: new Date().getTime()
}
}
return item
})
root.history = [entry, ...newHistory].slice(0, 20) if (!exists) {
} const newHistory = root.history.map(item => {
} if (typeof item === 'string') {
} else { return {
textProcess.isLoading = false "type": 'text',
} "content": item,
"timestamp": new Date().getTime()
}
}
return item
})
root.initialized = true root.history = [entry, ...newHistory].slice(0, 20)
typeProcess.isLoading = false }
} }
} else {
textProcess.isLoading = false
}
stdout: StdioCollector {} root.initialized = true
typeProcess.isLoading = false
} }
function refresh() { stdout: StdioCollector {}
if (!typeProcess.isLoading && !textProcess.isLoading) { }
typeProcess.isLoading = true
typeProcess.command = ["wl-paste", "-l"] function refresh() {
typeProcess.running = true if (!typeProcess.isLoading && !textProcess.isLoading) {
} typeProcess.isLoading = true
typeProcess.command = ["wl-paste", "-l"]
typeProcess.running = true
} }
}
} }

View file

@ -82,10 +82,10 @@ Singleton {
data.timestamp = Time.timestamp data.timestamp = Time.timestamp
console.log("[GitHub] Saving data to cache file:", githubDataFile) console.log("[GitHub] Saving data to cache file:", githubDataFile)
console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length) console.log("[GitHub] Data to save - version:", data.version, "contributors:", data.contributors.length)
// Ensure cache directory exists // Ensure cache directory exists
Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir]) Quickshell.execDetached(["mkdir", "-p", Settings.cacheDir])
Qt.callLater(() => { Qt.callLater(() => {
// Use direct ID reference to the FileView // Use direct ID reference to the FileView
githubDataFileView.writeAdapter() githubDataFileView.writeAdapter()
@ -147,7 +147,8 @@ Singleton {
console.log("[GitHub] Raw contributors response length:", response ? response.length : 0) console.log("[GitHub] Raw contributors response length:", response ? response.length : 0)
if (response && response.trim()) { if (response && response.trim()) {
const data = JSON.parse(response) const data = JSON.parse(response)
console.log("[GitHub] Parsed contributors data type:", typeof data, "length:", Array.isArray(data) ? data.length : "not array") console.log("[GitHub] Parsed contributors data type:", typeof data, "length:",
Array.isArray(data) ? data.length : "not array")
root.data.contributors = data || [] root.data.contributors = data || []
root.contributors = root.data.contributors root.contributors = root.data.contributors
console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length) console.log("[GitHub] Contributors fetched from GitHub:", root.contributors.length)

View file

@ -160,6 +160,7 @@ Singleton {
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
// console.log(this.text) // console.log(this.text)
} }
} }