mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-15 07:44:01 -04:00
408 lines
16 KiB
QML
408 lines
16 KiB
QML
pragma ComponentBehavior: Bound
|
||
|
||
import QtQuick
|
||
import QtQuick.Shapes
|
||
import qs.Common
|
||
import qs.Services
|
||
import qs.DCommon.Widgets
|
||
import qs.Modules.DDash
|
||
import "Wellbeing.js" as Wellbeing
|
||
import "../../../DCommon/Common/WheelInput.js" as WheelInput
|
||
|
||
DCard {
|
||
id: root
|
||
|
||
property var days: []
|
||
property date today: new Date()
|
||
property int firstDayOfWeek: 1
|
||
property real limitSeconds: 0
|
||
property bool showTitle: true
|
||
property int hoverPage: -1
|
||
property int hoverIndex: -1
|
||
property real wheelDistance: 0
|
||
property real dragStartX: 0
|
||
|
||
readonly property var weeks: Wellbeing.weeksBack(days, today, firstDayOfWeek)
|
||
readonly property int pageCount: weeks.length
|
||
property int page: pageCount - 1
|
||
readonly property int currentPage: pager.width > 0 ? Math.max(0, Math.min(pageCount - 1, Math.round(pager.contentX / pager.width))) : page
|
||
readonly property int weekOffset: page - (pageCount - 1)
|
||
readonly property var week: weeks[page] ?? []
|
||
readonly property var hoverDay: hoverIndex >= 0 ? (weeks[hoverPage] ?? [])[hoverIndex] ?? null : null
|
||
readonly property real total: Wellbeing.totalSeconds(week)
|
||
readonly property real peak: Math.max(limitSeconds, ...week.map(day => day.active))
|
||
readonly property var axis: Wellbeing.axis(peak)
|
||
readonly property real columnWidth: pager.width / Wellbeing.weekLength
|
||
readonly property real barWidth: columnWidth * WellbeingMetrics.barWidthRatio
|
||
readonly property real dayLabelHeight: Theme.fontSizeSmall + Theme.spacingXS
|
||
readonly property color pastColor: Theme.primaryContainer
|
||
readonly property color todayColor: Theme.primary
|
||
readonly property color overColor: Theme.error
|
||
readonly property string rangeText: week.length > 0 ? rangeDate(week[0].date) + " – " + rangeDate(week[week.length - 1].date) : ""
|
||
|
||
function rangeDate(key) {
|
||
return Wellbeing.parseKey(key).toLocaleDateString(I18n.locale(), Wellbeing.monthDayFormat(I18n.locale().dateFormat(Locale.ShortFormat)));
|
||
}
|
||
|
||
function barColor(day) {
|
||
if (limitSeconds > 0 && day.active > limitSeconds)
|
||
return overColor;
|
||
return day.today ? todayColor : pastColor;
|
||
}
|
||
|
||
function yFor(seconds) {
|
||
return plot.height * (1 - Math.min(1, seconds / axis.top));
|
||
}
|
||
|
||
function dayLabel(day) {
|
||
return I18n.locale().dayName(day.weekday === 0 ? Wellbeing.weekLength : day.weekday, Locale.ShortFormat);
|
||
}
|
||
|
||
function snapTo(index, animate) {
|
||
snapAnim.stop();
|
||
page = Math.max(0, Math.min(pageCount - 1, index));
|
||
const target = page * pager.width;
|
||
if (!animate || !DashMetrics.animationsEnabled) {
|
||
pager.contentX = target;
|
||
return;
|
||
}
|
||
snapAnim.to = target;
|
||
snapAnim.start();
|
||
}
|
||
|
||
function showCurrentWeek() {
|
||
snapTo(pageCount - 1, false);
|
||
}
|
||
|
||
onPageCountChanged: showCurrentWeek()
|
||
|
||
restRadius: DashMetrics.cardRadius
|
||
pad: Theme.spacingL
|
||
Accessible.role: Accessible.Chart
|
||
Accessible.name: I18n.tr("Weekly screen time", "chart title, screen time per day over the week")
|
||
|
||
NumberAnimation {
|
||
id: snapAnim
|
||
|
||
readonly property bool atEdge: root.page === 0 || root.page === root.pageCount - 1
|
||
|
||
target: pager
|
||
property: "contentX"
|
||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||
easing.type: Easing.BezierSpline
|
||
easing.bezierCurve: atEdge ? Theme.expressiveCurves.expressiveEffects : Theme.expressiveCurves.expressiveDefaultSpatial
|
||
}
|
||
|
||
Timer {
|
||
id: settleTimer
|
||
interval: WellbeingMetrics.settleDelay
|
||
onTriggered: root.snapTo(root.currentPage, true)
|
||
}
|
||
|
||
Column {
|
||
anchors.fill: parent
|
||
spacing: Theme.spacingS
|
||
|
||
StyledText {
|
||
width: parent.width
|
||
visible: root.showTitle
|
||
text: I18n.tr("Weekly screen time", "chart title, screen time per day over the week")
|
||
font.pixelSize: Theme.fontSizeLarge
|
||
font.weight: Theme.fontWeightMedium
|
||
color: root.contentColor
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
Row {
|
||
width: parent.width
|
||
spacing: Theme.spacingS
|
||
|
||
StyledText {
|
||
id: totalLabel
|
||
anchors.baseline: caption.baseline
|
||
text: WellbeingService.formatDuration(root.hoverDay ? root.hoverDay.active : root.total)
|
||
font.pixelSize: Theme.fontSizeXLarge
|
||
font.weight: Theme.fontWeightBold
|
||
font.features: ({
|
||
"tnum": 1
|
||
})
|
||
color: root.accentColor
|
||
}
|
||
|
||
StyledText {
|
||
id: caption
|
||
anchors.bottom: parent.bottom
|
||
anchors.bottomMargin: Theme.spacingXXS
|
||
text: root.hoverDay ? root.dayLabel(root.hoverDay) : I18n.tr("Total")
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.weight: Theme.fontWeightMedium
|
||
font.capitalization: Font.AllUppercase
|
||
color: root.mutedColor
|
||
}
|
||
|
||
Item {
|
||
width: parent.width - x
|
||
height: totalLabel.height
|
||
|
||
DActionButton {
|
||
anchors.right: rangeLabel.left
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: Theme.buttonHeightXS
|
||
iconName: "chevron_left"
|
||
iconColor: root.mutedColor
|
||
enabled: root.page > 0
|
||
Accessible.name: I18n.tr("Previous")
|
||
onClicked: root.snapTo(root.page - 1, true)
|
||
}
|
||
|
||
StyledText {
|
||
id: rangeLabel
|
||
anchors.right: nextButton.left
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
width: WellbeingMetrics.rangeLabelWidth
|
||
horizontalAlignment: Text.AlignHCenter
|
||
text: root.rangeText
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.weight: Theme.fontWeightMedium
|
||
font.features: ({
|
||
"tnum": 1
|
||
})
|
||
color: root.weekOffset === 0 ? root.accentColor : root.contentColor
|
||
elide: Text.ElideRight
|
||
}
|
||
|
||
DActionButton {
|
||
id: nextButton
|
||
anchors.right: parent.right
|
||
anchors.verticalCenter: parent.verticalCenter
|
||
buttonSize: Theme.buttonHeightXS
|
||
iconName: "chevron_right"
|
||
iconColor: root.mutedColor
|
||
enabled: root.weekOffset < 0
|
||
Accessible.name: I18n.tr("Next")
|
||
onClicked: root.snapTo(root.page + 1, true)
|
||
}
|
||
}
|
||
}
|
||
|
||
Item {
|
||
width: parent.width
|
||
height: parent.height - y
|
||
LayoutMirroring.enabled: false
|
||
LayoutMirroring.childrenInherit: true
|
||
|
||
Item {
|
||
id: plot
|
||
anchors.left: parent.left
|
||
anchors.right: axisLabels.left
|
||
anchors.rightMargin: Theme.spacingS
|
||
anchors.top: parent.top
|
||
anchors.topMargin: Theme.spacingS
|
||
anchors.bottom: parent.bottom
|
||
anchors.bottomMargin: root.dayLabelHeight + Theme.spacingXS
|
||
|
||
Repeater {
|
||
model: root.axis.ticks
|
||
|
||
Rectangle {
|
||
required property int modelData
|
||
width: plot.width
|
||
height: Theme.dividerWidth
|
||
y: root.yFor(modelData * Wellbeing.secondsPerHour) - height / 2
|
||
color: Theme.outlineVariant
|
||
}
|
||
}
|
||
}
|
||
|
||
DFlickable {
|
||
id: pager
|
||
anchors.left: plot.left
|
||
anchors.right: plot.right
|
||
anchors.top: plot.top
|
||
anchors.bottom: parent.bottom
|
||
contentWidth: width * root.pageCount
|
||
contentHeight: height
|
||
flickableDirection: Flickable.HorizontalFlick
|
||
wheelEnabled: false
|
||
showScrollBar: false
|
||
clip: true
|
||
|
||
onWidthChanged: {
|
||
snapAnim.stop();
|
||
contentX = root.page * width;
|
||
}
|
||
onDragStarted: {
|
||
snapAnim.stop();
|
||
settleTimer.stop();
|
||
root.dragStartX = contentX;
|
||
}
|
||
onFlickStarted: {
|
||
const forward = contentX > root.dragStartX;
|
||
cancelFlick();
|
||
root.snapTo(forward ? Math.ceil(contentX / width) : Math.floor(contentX / width), true);
|
||
}
|
||
onMovementEnded: {
|
||
if (!snapAnim.running)
|
||
root.snapTo(root.currentPage, true);
|
||
}
|
||
|
||
Repeater {
|
||
model: root.weeks
|
||
|
||
Item {
|
||
id: page
|
||
|
||
required property var modelData
|
||
required property int index
|
||
|
||
x: index * pager.width
|
||
width: pager.width
|
||
height: pager.height
|
||
|
||
Repeater {
|
||
model: page.modelData
|
||
|
||
Rectangle {
|
||
required property var modelData
|
||
required property int index
|
||
x: index * root.columnWidth + (root.columnWidth - width) / 2
|
||
y: plot.height - height
|
||
width: root.barWidth
|
||
height: Math.max(0, plot.height - root.yFor(modelData.active))
|
||
visible: !modelData.future && modelData.active > 0
|
||
topLeftRadius: Theme.cornerRadiusS
|
||
topRightRadius: Theme.cornerRadiusS
|
||
color: root.barColor(modelData)
|
||
opacity: root.hoverIndex === -1 || (root.hoverPage === page.index && root.hoverIndex === index) ? 1 : WellbeingMetrics.dimmedBarOpacity
|
||
|
||
Behavior on height {
|
||
enabled: DashMetrics.animationsEnabled
|
||
NumberAnimation {
|
||
duration: DashMetrics.meterDuration
|
||
easing.type: Easing.BezierSpline
|
||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Row {
|
||
anchors.left: parent.left
|
||
anchors.right: parent.right
|
||
anchors.bottom: parent.bottom
|
||
height: root.dayLabelHeight
|
||
|
||
Repeater {
|
||
model: page.modelData
|
||
|
||
StyledText {
|
||
required property var modelData
|
||
width: root.columnWidth
|
||
horizontalAlignment: Text.AlignHCenter
|
||
text: root.dayLabel(modelData)
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.weight: modelData.today ? Theme.fontWeightBold : Theme.fontWeightMedium
|
||
color: modelData.today ? root.accentColor : root.mutedColor
|
||
}
|
||
}
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: parent
|
||
acceptedButtons: Qt.NoButton
|
||
hoverEnabled: true
|
||
onPositionChanged: mouse => {
|
||
if (page.index !== root.page || root.columnWidth <= 0)
|
||
return;
|
||
root.hoverPage = page.index;
|
||
root.hoverIndex = Math.floor(mouse.x / root.columnWidth);
|
||
}
|
||
onExited: root.hoverIndex = -1
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Shape {
|
||
anchors.fill: plot
|
||
visible: root.limitSeconds > 0
|
||
preferredRendererType: Shape.CurveRenderer
|
||
|
||
ShapePath {
|
||
strokeColor: root.overColor
|
||
strokeWidth: Theme.outlineWidthFocused
|
||
strokeStyle: ShapePath.DashLine
|
||
dashPattern: [3, 3]
|
||
fillColor: "transparent"
|
||
capStyle: ShapePath.FlatCap
|
||
startX: 0
|
||
startY: root.yFor(root.limitSeconds)
|
||
|
||
PathLine {
|
||
x: plot.width
|
||
y: root.yFor(root.limitSeconds)
|
||
}
|
||
}
|
||
}
|
||
|
||
StyledText {
|
||
anchors.right: plot.right
|
||
y: plot.y + root.yFor(root.limitSeconds) - height - Theme.spacingXXS
|
||
visible: root.limitSeconds > 0
|
||
text: I18n.tr("Limit", "label on the daily screen time limit line of a chart")
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.weight: Theme.fontWeightBold
|
||
font.capitalization: Font.AllUppercase
|
||
color: root.overColor
|
||
}
|
||
|
||
MouseArea {
|
||
anchors.fill: pager
|
||
acceptedButtons: Qt.NoButton
|
||
onWheel: wheel => {
|
||
if (wheel.angleDelta.x === 0 && wheel.pixelDelta.x === 0) {
|
||
wheel.accepted = false;
|
||
return;
|
||
}
|
||
snapAnim.stop();
|
||
if (WheelInput.isTouchpad(wheel)) {
|
||
pager.contentX = Math.max(0, Math.min(pager.contentWidth - pager.width, pager.contentX - wheel.pixelDelta.x));
|
||
settleTimer.restart();
|
||
return;
|
||
}
|
||
root.wheelDistance += wheel.angleDelta.x;
|
||
if (Math.abs(root.wheelDistance) < WellbeingMetrics.wheelNotch)
|
||
return;
|
||
const step = root.wheelDistance > 0 ? -1 : 1;
|
||
root.wheelDistance = 0;
|
||
root.snapTo(root.page + step, true);
|
||
}
|
||
}
|
||
|
||
Item {
|
||
id: axisLabels
|
||
anchors.right: parent.right
|
||
anchors.top: plot.top
|
||
anchors.bottom: plot.bottom
|
||
width: WellbeingMetrics.axisLabelWidth
|
||
|
||
Repeater {
|
||
model: root.axis.ticks
|
||
|
||
StyledText {
|
||
required property int modelData
|
||
anchors.right: axisLabels.right
|
||
y: root.yFor(modelData * Wellbeing.secondsPerHour) - height / 2
|
||
text: I18n.tr("%1h", "hours abbreviation, %1 is a number").arg(modelData)
|
||
font.pixelSize: Theme.fontSizeSmall
|
||
font.features: ({
|
||
"tnum": 1
|
||
})
|
||
color: root.mutedColor
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|