This commit is contained in:
timka123 committed 2026-10-18 18:02:05 +03:00
commit 17a0a3c4c9
121 files changed
+17641

No files matched your search

@@ -0,0 +1,36 @@
// An inline warning strip — used for "the backend is gone" and "the helper is
// not installed", the two states where the app cannot do its job.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
Rectangle {
id: banner
property string text: ""
property string actionText: ""
signal actionClicked()
implicitHeight: visible ? row.implicitHeight + 20 : 0
color: "#e34948"
opacity: 0.92
RowLayout {
id: row
anchors.fill: parent
anchors.margins: 10
spacing: 12
Label {
text: banner.text
color: "white"
wrapMode: Text.WordWrap
Layout.fillWidth: true
}
Button {
visible: banner.actionText !== ""
text: banner.actionText
flat: true
Material.foreground: "white"
onClicked: banner.actionClicked()
}
}
}
@@ -0,0 +1,79 @@
// A tiny bar chart: magnitude over time, one hue, baseline at zero.
//
// The ramp step says "did this day clear the goal" without adding a second
// colour, and every bar carries its own tooltip, so no bar needs a label.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
Item {
id: chart
property var days: [] // [{date, pomodoros, focus_minutes, goal_met}]
property int goal: 8
property int barHeight: 110
implicitHeight: barHeight + 18
readonly property int maxValue: {
var top = goal
for (var i = 0; i < days.length; i++) top = Math.max(top, days[i].pomodoros)
return Math.max(top, 1)
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
y: chart.barHeight - (chart.goal / chart.maxValue) * chart.barHeight
height: 1
color: Material.hintTextColor
opacity: 0.45
visible: chart.goal > 0
}
RowLayout {
anchors.left: parent.left
anchors.right: parent.right
height: chart.barHeight
spacing: 2
Repeater {
model: chart.days
delegate: Item {
required property var modelData
Layout.fillWidth: true
Layout.fillHeight: true
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: Math.max(modelData.pomodoros > 0
? (modelData.pomodoros / chart.maxValue) * chart.barHeight : 2, 2)
radius: 4
color: modelData.pomodoros === 0 ? Material.dividerColor
: (modelData.goal_met ? "#256abf" : "#6da7ec")
ToolTip.visible: hover.hovered
ToolTip.text: modelData.date + ": " + modelData.pomodoros
+ " pomodoros · " + modelData.focus_minutes + " min"
HoverHandler { id: hover }
Behavior on height { NumberAnimation { duration: 250 } }
}
}
}
}
RowLayout {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
Label {
text: chart.days.length ? chart.days[0].date.substring(5) : ""
font.pixelSize: 10; opacity: 0.6
}
Item { Layout.fillWidth: true }
Label {
text: chart.days.length ? chart.days[chart.days.length - 1].date.substring(5) : ""
font.pixelSize: 10; opacity: 0.6
}
}
}
@@ -0,0 +1,51 @@
// A Material surface with a title. Used everywhere instead of bare Panes so the
// pages share one radius, one padding and one elevation.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
Pane {
id: card
property string title: ""
property string subtitle: ""
default property alias content: inner.data
Material.elevation: 1
Material.roundedScale: Material.LargeScale
padding: 18
Layout.fillWidth: true
background: Rectangle {
radius: 18
color: Material.dialogColor
border.width: 1
border.color: Material.dividerColor
}
ColumnLayout {
anchors.fill: parent
spacing: 10
ColumnLayout {
spacing: 2
visible: card.title !== ""
Layout.fillWidth: true
Label { text: card.title; font.pixelSize: 16; font.weight: Font.DemiBold }
Label {
text: card.subtitle
visible: card.subtitle !== ""
font.pixelSize: 12
opacity: 0.7
Layout.fillWidth: true
wrapMode: Text.WordWrap
}
}
ColumnLayout {
id: inner
Layout.fillWidth: true
Layout.fillHeight: true
spacing: 10
}
}
}
@@ -0,0 +1,34 @@
// The filled Material button, painted explicitly.
//
// Relying on `highlighted: true` leaves the fill to the style, and under some
// renderers that fill never appears — white text on a white surface. The
// primary action on a page is exactly the thing that must never be invisible,
// so it draws its own pill.
import QtQuick
import QtQuick.Controls.Material
Button {
id: control
Material.foreground: "white"
font.weight: Font.Medium
leftPadding: 20
rightPadding: 20
implicitHeight: 40
background: Rectangle {
radius: height / 2
color: control.enabled ? Material.accent : Material.buttonDisabledColor
opacity: control.pressed ? 0.82 : (control.hovered ? 0.92 : 1)
Behavior on opacity { NumberAnimation { duration: 120 } }
}
contentItem: Label {
text: control.text
font: control.font
color: control.enabled ? "white" : Material.hintTextColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
}
}
@@ -0,0 +1,33 @@
// Transient messages. Material's own popup, anchored to the bottom.
import QtQuick
import QtQuick.Controls.Material
Popup {
id: snackbar
property string message: ""
function show(text) {
message = text
open()
timer.restart()
}
x: Math.round((parent.width - width) / 2)
y: parent.height - height - 72
width: Math.min(parent.width - 48, Math.max(260, label.implicitWidth + 48))
padding: 16
modal: false
focus: false
closePolicy: Popup.NoAutoClose
Material.elevation: 6
Material.roundedScale: Material.SmallScale
Label {
id: label
anchors.fill: parent
text: snackbar.message
wrapMode: Text.WordWrap
}
Timer { id: timer; interval: 5000; onTriggered: snackbar.close() }
}
@@ -0,0 +1,38 @@
// One number, read at a glance. Not a chart.
import QtQuick
import QtQuick.Controls.Material
import QtQuick.Layouts
Pane {
property string label: ""
property string value: "—"
property string caption: ""
property color accent: Material.foreground
Material.elevation: 1
Material.roundedScale: Material.LargeScale
padding: 16
Layout.fillWidth: true
background: Rectangle {
radius: 16
color: Material.dialogColor
border.width: 1
border.color: Material.dividerColor
}
ColumnLayout {
anchors.fill: parent
spacing: 2
Label { text: label.toUpperCase(); font.pixelSize: 10; font.letterSpacing: 1; opacity: 0.6 }
Label { text: value; font.pixelSize: 26; font.weight: Font.Light; color: accent }
Label {
text: caption
visible: caption !== ""
font.pixelSize: 11
opacity: 0.7
Layout.fillWidth: true
elide: Text.ElideRight
}
}
}
@@ -0,0 +1,29 @@
// The little "what is the timer doing" chip, shown in the rail and the toolbar.
import QtQuick
import QtQuick.Controls.Material
Rectangle {
property bool running: false
property bool paused: false
property bool connected: true
property string text: ""
property color accent: "#2a78d6"
implicitWidth: Math.max(78, label.implicitWidth + 28)
implicitHeight: 32
radius: 16
color: !connected ? "#e34948" : (running ? accent : "transparent")
opacity: paused ? 0.6 : 1
border.width: running || !connected ? 0 : 1
border.color: Material.hintTextColor
Label {
id: label
anchors.centerIn: parent
text: parent.text
font.pixelSize: 13
font.weight: Font.DemiBold
font.family: "Roboto Mono, monospace"
color: (parent.running || !parent.connected) ? "white" : Material.foreground
}
}
@@ -0,0 +1,87 @@
// The countdown ring, drawn with Canvas.
//
// A hero number inside a progress arc: the number is what you read, the arc is
// the glance. The arc colour is the only thing that says focus vs break.
import QtQuick
import QtQuick.Controls.Material
Item {
id: dial
property real progress: 0 // 0..1 elapsed
property string clockText: "00:00"
property string label: ""
property string sublabel: ""
property color accent: "#2a78d6"
property bool paused: false
implicitWidth: 260
implicitHeight: 260
onProgressChanged: canvas.requestPaint()
onAccentChanged: canvas.requestPaint()
onPausedChanged: canvas.requestPaint()
Canvas {
id: canvas
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d")
ctx.reset()
var centre = Math.min(width, height) / 2
var stroke = Math.max(10, centre * 0.11)
var radius = centre - stroke
ctx.lineCap = "round"
ctx.lineWidth = stroke
ctx.beginPath()
ctx.strokeStyle = Qt.rgba(0.5, 0.5, 0.5, 0.18)
ctx.arc(centre, centre, radius, 0, Math.PI * 2)
ctx.stroke()
var remaining = 1 - Math.max(0, Math.min(1, dial.progress))
if (remaining > 0) {
ctx.beginPath()
ctx.globalAlpha = dial.paused ? 0.45 : 1
ctx.strokeStyle = dial.accent
ctx.arc(centre, centre, radius, -Math.PI / 2, -Math.PI / 2 + remaining * Math.PI * 2)
ctx.stroke()
}
}
}
Column {
anchors.centerIn: parent
spacing: 4
width: parent.width * 0.72
Label {
width: parent.width
text: dial.label.toUpperCase()
font.pixelSize: 11
font.letterSpacing: 1.5
opacity: 0.65
horizontalAlignment: Text.AlignHCenter
}
Label {
width: parent.width
text: dial.clockText
font.pixelSize: Math.round(dial.width * 0.2)
font.weight: Font.Light
font.family: "Roboto Mono, monospace"
opacity: dial.paused ? 0.6 : 1
horizontalAlignment: Text.AlignHCenter
}
Label {
width: parent.width
text: dial.sublabel
visible: dial.sublabel !== ""
font.pixelSize: 13
opacity: 0.75
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
}
}
}