Files
kde-codex-dashboard/contents/ui/main.qml
T

456 lines
20 KiB
QML

/* OpenAI Codex Usage — dashboard desktop per KDE Plasma 6. */
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls as QQC2
import org.kde.plasma.plasmoid
import org.kde.plasma.core as PlasmaCore
import org.kde.plasma.components 3.0 as PC3
import org.kde.plasma.plasma5support as Plasma5Support
import org.kde.kirigami as Kirigami
PlasmoidItem {
id: root
property var data: null
property string lastUpdate: ""
property string errorMsg: ""
property bool loading: false
property int clockTick: 0
property int refreshSecs: Plasmoid.configuration.refreshIntervalSec
property int warnThreshold: Plasmoid.configuration.warnThreshold
property int critThreshold: Plasmoid.configuration.critThreshold
readonly property color brandColor: "#7cb342"
Plasmoid.title: i18n("OpenAI Codex")
Plasmoid.icon: "code-context"
Plasmoid.backgroundHints: PlasmaCore.Types.DefaultBackground
toolTipMainText: i18n("OpenAI Codex — Rate Limits")
toolTipSubText: root.data ? i18n("Settimana (7D): %1% · Finestra 5h: %2% · Piano: %3 · Reset disp: %4",
root.secRemainingPct(),
root.primRemainingPct(),
(root.data.plan_type || "—").toUpperCase(),
root.data.reset_credits || 0) : (root.errorMsg || "")
preferredRepresentation: fullRepresentation
switchWidth: 150
switchHeight: 150
Plasma5Support.DataSource {
id: executable
engine: "executable"
connectedSources: []
onNewData: function(sourceName, result) {
root.loading = false
executable.disconnectSource(sourceName)
if (result["exit code"] !== 0) {
root.errorMsg = i18n("comando fallito (exit %1)", result["exit code"])
return
}
try {
const parsed = JSON.parse((result["stdout"] || "").trim())
if (parsed && parsed.ok === false) {
root.errorMsg = parsed.error + (parsed.detail ? " — " + parsed.detail : "")
root.data = null
} else if (parsed && parsed.ok && parsed.primary_window && parsed.secondary_window) {
root.data = parsed
root.errorMsg = ""
root.lastUpdate = new Date().toLocaleTimeString(Qt.locale(), "HH:mm:ss")
} else {
root.errorMsg = i18n("risposta inattesa")
root.data = null
}
} catch (e) {
root.errorMsg = i18n("JSON non valido: %1", e.message)
root.data = null
}
}
}
function refresh() {
if (root.loading) return
root.loading = true
const path = Qt.resolvedUrl("../scripts/usage.sh").toString().replace(/^file:\/\//, "")
executable.connectSource("/bin/bash " + shellQuote(path))
}
function shellQuote(s) { return "'" + String(s).replace(/'/g, "'\"'\"'") + "'" }
function primRemainingPct() {
return root.data && root.data.primary_window ? Math.round((root.data.primary_window.remaining || 0) * 100) : 0
}
function primUsedPct() {
return root.data && root.data.primary_window ? Math.round(root.data.primary_window.used_percent || 0) : 0
}
function secRemainingPct() {
return root.data && root.data.secondary_window ? Math.round((root.data.secondary_window.remaining || 0) * 100) : 0
}
function secUsedPct() {
return root.data && root.data.secondary_window ? Math.round(root.data.secondary_window.used_percent || 0) : 0
}
function effectiveRemainingPct() {
if (!root.data) return 0
return Math.min(primRemainingPct(), secRemainingPct())
}
function accentColorFor(pct) {
if (pct <= root.critThreshold) return Kirigami.Theme.negativeTextColor
if (pct <= root.warnThreshold) return Kirigami.Theme.neutralTextColor
return root.brandColor
}
function globalAccentColor() {
if (!root.data) return Kirigami.Theme.disabledTextColor
if (root.data.limit_reached || root.data.allowed === false) return Kirigami.Theme.negativeTextColor
return accentColorFor(effectiveRemainingPct())
}
function formatResetRemaining(resetAt) {
const tick = root.clockTick
if (!resetAt) return "—"
let mins = Math.max(0, Math.floor((resetAt * 1000 - Date.now()) / 60000))
if (mins === 0) return i18n("ora")
const days = Math.floor(mins / 1440)
mins -= days * 1440
const hours = Math.floor(mins / 60)
mins -= hours * 60
return days > 0 ? i18n("%1g %2h", days, hours) : i18n("%1h %2m", hours, mins)
}
Component.onCompleted: root.refresh()
Timer {
interval: Math.max(30, root.refreshSecs) * 1000
repeat: true
running: true
onTriggered: root.refresh()
}
Timer {
interval: 30000
repeat: true
running: true
onTriggered: root.clockTick += 1
}
compactRepresentation: Component {
Item {
implicitWidth: Kirigami.Units.iconSizes.smallMedium
implicitHeight: Kirigami.Units.iconSizes.smallMedium
Kirigami.Icon {
anchors.fill: parent
source: Plasmoid.icon
color: root.data ? root.globalAccentColor() : Kirigami.Theme.disabledTextColor
}
MouseArea { anchors.fill: parent; onClicked: Plasmoid.expanded = !Plasmoid.expanded }
}
}
fullRepresentation: Component {
Item {
implicitWidth: 224
implicitHeight: 224
Layout.preferredWidth: 224
Layout.preferredHeight: 224
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
spacing: 4
// Header
RowLayout {
Layout.fillWidth: true
spacing: 5
Kirigami.Icon {
source: Plasmoid.icon
color: root.brandColor
Layout.preferredWidth: 18
Layout.preferredHeight: 18
}
ColumnLayout {
spacing: 0
PC3.Label {
text: i18n("CODEX")
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize
}
PC3.Label {
text: root.data ? (root.data.plan_type || "—").toUpperCase() : i18n("QUOTA DASHBOARD")
color: root.brandColor
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
Item { Layout.fillWidth: true }
Rectangle {
Layout.preferredWidth: 8
Layout.preferredHeight: 8
radius: 4
color: root.errorMsg ? Kirigami.Theme.negativeTextColor :
(root.data && (root.data.limit_reached || root.data.allowed === false) ? Kirigami.Theme.negativeTextColor : root.brandColor)
}
PC3.Button {
icon.name: "view-refresh"
display: QQC2.AbstractButton.IconOnly
Layout.preferredWidth: 22
Layout.preferredHeight: 22
enabled: !root.loading
onClicked: root.refresh()
}
}
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 1
color: Qt.rgba(0.49, 0.76, 0.26, 0.35)
}
PC3.Label {
visible: root.errorMsg.length > 0
Layout.fillWidth: true
wrapMode: Text.Wrap
color: Kirigami.Theme.negativeTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 2
text: i18n("Errore: %1", root.errorMsg)
}
// Card 1: Settimanale (7D)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
border.width: 1
border.color: root.accentColorFor(root.secRemainingPct())
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 4
Rectangle {
Layout.preferredWidth: 22
Layout.preferredHeight: 14
radius: 7
color: root.accentColorFor(root.secRemainingPct())
PC3.Label {
anchors.centerIn: parent
text: "7D"
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
PC3.Label {
text: i18n("Settimanale")
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
Layout.fillWidth: true
}
PC3.Label {
text: root.secRemainingPct() + "%"
color: root.accentColorFor(root.secRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 6
Rectangle {
anchors.fill: parent
radius: 3
color: Qt.rgba(0, 0, 0, 0.55)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, root.data && root.data.secondary_window ? root.data.secondary_window.remaining : 0))
height: parent.height
radius: 3
color: root.accentColorFor(root.secRemainingPct())
}
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: i18n("%1% usato", root.secUsedPct())
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("Reset: %1", root.formatResetRemaining(root.data && root.data.secondary_window ? root.data.secondary_window.reset_at : null))
color: root.accentColorFor(root.secRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
// Card 2: Finestra 5 Ore (5H)
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 52
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
border.width: 1
border.color: root.accentColorFor(root.primRemainingPct())
ColumnLayout {
anchors.fill: parent
anchors.margins: 5
spacing: 2
RowLayout {
Layout.fillWidth: true
spacing: 4
Rectangle {
Layout.preferredWidth: 22
Layout.preferredHeight: 14
radius: 7
color: root.accentColorFor(root.primRemainingPct())
PC3.Label {
anchors.centerIn: parent
text: "5H"
color: "#101012"
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
PC3.Label {
text: i18n("Finestra 5 ore")
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
Layout.fillWidth: true
}
PC3.Label {
text: root.primRemainingPct() + "%"
color: root.accentColorFor(root.primRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize + 1
}
}
Item {
Layout.fillWidth: true
Layout.preferredHeight: 6
Rectangle {
anchors.fill: parent
radius: 3
color: Qt.rgba(0, 0, 0, 0.55)
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: parent.width * Math.max(0, Math.min(1, root.data && root.data.primary_window ? root.data.primary_window.remaining : 0))
height: parent.height
radius: 3
color: root.accentColorFor(root.primRemainingPct())
}
}
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: i18n("%1% usato", root.primUsedPct())
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: i18n("Reset: %1", root.formatResetRemaining(root.data && root.data.primary_window ? root.data.primary_window.reset_at : null))
color: root.accentColorFor(root.primRemainingPct())
font.bold: true
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
// Card 3: Crediti Extra + Reset Disponibili
Rectangle {
Layout.fillWidth: true
Layout.preferredHeight: 40
radius: 5
color: Qt.rgba(0, 0, 0, 0.25)
RowLayout {
anchors.fill: parent
anchors.leftMargin: 7
anchors.rightMargin: 7
anchors.topMargin: 4
anchors.bottomMargin: 6
spacing: 8
ColumnLayout {
spacing: 1
PC3.Label {
text: i18n("CREDITI EXTRA")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: root.data && root.data.credits && root.data.credits.balance ? "$" + root.data.credits.balance : "$0.00"
font.bold: true
color: root.brandColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
}
}
Item { Layout.fillWidth: true }
ColumnLayout {
spacing: 1
Layout.alignment: Qt.AlignRight
PC3.Label {
text: i18n("RESET SCORTA")
color: Kirigami.Theme.disabledTextColor
Layout.alignment: Qt.AlignRight
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
PC3.Label {
text: (root.data && root.data.reset_credits !== undefined) ? i18n("↺ %1 disp.", root.data.reset_credits) : "—"
font.bold: true
color: "#38bdf8"
Layout.alignment: Qt.AlignRight
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 1
}
}
}
}
// Footer
RowLayout {
Layout.fillWidth: true
PC3.Label {
text: root.lastUpdate ? i18n("SYNC %1", root.lastUpdate) : i18n("SYNC…")
color: Kirigami.Theme.disabledTextColor
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
Item { Layout.fillWidth: true }
PC3.Label {
text: root.data && (root.data.limit_reached || root.data.allowed === false)
? i18n("✕ LIMITE ATTIVO")
: i18n("STATO ATTIVO")
color: root.data && (root.data.limit_reached || root.data.allowed === false)
? Kirigami.Theme.negativeTextColor
: Kirigami.Theme.disabledTextColor
font.bold: Boolean(root.data && (root.data.limit_reached || root.data.allowed === false))
font.pointSize: Kirigami.Theme.defaultFont.pointSize - 3
}
}
}
}
}
}