mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-29 22:47:47 -04:00
wellbeing: add Digital Wellbeing, with a built-in dash plugin
This commit is contained in:
1 parent
a348d8bc5e
commit
d77c4f6298
31 files changed
+2439
-8
No files matched your search
@@ -172,6 +172,25 @@ Singleton {
|
||||
},
|
||||
"options": [cardOnly(toggle("city", I18n.tr("Show city"), false)), cardOnly(toggle("readings", I18n.tr("Show readings"), true)), cardOnly(toneOption())]
|
||||
},
|
||||
{
|
||||
"id": "wellbeing",
|
||||
"text": I18n.tr("Digital wellbeing"),
|
||||
"icon": "digital_wellbeing",
|
||||
"description": SettingsData.wellbeingEnabled ? I18n.tr("Screen time and app limits") : I18n.tr("Hidden until screen time tracking is enabled"),
|
||||
"available": SettingsData.wellbeingEnabled,
|
||||
"tab": {
|
||||
"component": wellbeingTab,
|
||||
"async": true
|
||||
},
|
||||
"card": {
|
||||
"component": wellbeingCard,
|
||||
"w": 2,
|
||||
"h": 1,
|
||||
"minW": 1,
|
||||
"minH": 1,
|
||||
"maxH": 3
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "notifications",
|
||||
"text": I18n.tr("Notifications"),
|
||||
@@ -716,6 +735,16 @@ Singleton {
|
||||
NotificationsOverviewCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wellbeingTab
|
||||
WellbeingTab {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: wellbeingCard
|
||||
WellbeingOverviewCard {}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: clockCard
|
||||
ClockCard {}
|
||||
|
||||
@@ -13,6 +13,11 @@ CcSheetDialog {
|
||||
property bool tabScope: false
|
||||
|
||||
readonly property var entry: DashRegistry.entry(entryId)
|
||||
readonly property var settingsPages: ({
|
||||
"weather": "weather",
|
||||
"media": "media_player",
|
||||
"wellbeing": "wellbeing"
|
||||
})
|
||||
readonly property var specs: DashRegistry.sheetOptionSpecs(entryId, tabScope)
|
||||
|
||||
function presentFor(id) {
|
||||
@@ -44,14 +49,14 @@ CcSheetDialog {
|
||||
}
|
||||
|
||||
SettingsNavRow {
|
||||
visible: root.entryId === "weather" || root.entryId === "media"
|
||||
visible: root.entryId in root.settingsPages
|
||||
width: parent.width
|
||||
title: root.entryId === "media" ? I18n.tr("Media player") : I18n.tr("Weather")
|
||||
title: root.entryId === "media" ? I18n.tr("Media player") : root.entry?.text ?? ""
|
||||
iconName: "settings"
|
||||
onClicked: {
|
||||
root.dismiss();
|
||||
PopoutService.closeDankDash();
|
||||
PopoutService.openSettingsWithTab(root.entryId === "media" ? "media_player" : "weather");
|
||||
PopoutService.openSettingsWithTab(root.settingsPages[root.entryId]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.DankDash.Wellbeing
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import "../Wellbeing/Wellbeing.js" as Wellbeing
|
||||
|
||||
Card {
|
||||
id: root
|
||||
|
||||
property bool live: Window.window?.visible ?? false
|
||||
|
||||
readonly property bool wide: width >= DashMetrics.gridRowUnit * 2
|
||||
readonly property bool tall: height >= DashMetrics.gridRowUnit * 2
|
||||
readonly property var today: WellbeingService.today
|
||||
readonly property real appRowHeight: Theme.iconSizeMedium + Theme.spacingXS
|
||||
readonly property int appRows: tall ? Math.min(WellbeingMetrics.cardApps, Math.floor((height - pad * 2 - header.height - value.height - Theme.spacingS * 2) / (appRowHeight + Theme.spacingXS))) : 0
|
||||
readonly property var apps: appRows > 0 ? Wellbeing.topApps([today], appRows) : []
|
||||
readonly property real limitSeconds: SettingsData.wellbeingDailyLimit * 60
|
||||
readonly property bool overLimit: limitSeconds > 0 && today.active >= limitSeconds
|
||||
|
||||
entryId: "wellbeing"
|
||||
clipContent: true
|
||||
pad: Theme.spacingM
|
||||
Accessible.name: I18n.tr("Screen time")
|
||||
|
||||
Row {
|
||||
id: header
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "digital_wellbeing"
|
||||
size: Theme.iconSizeMedium
|
||||
color: root.accentColor
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSizeMedium - parent.spacing
|
||||
text: I18n.tr("Screen time")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: root.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: value
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
y: root.apps.length > 0 ? header.height + Theme.spacingXS : parent.height - height
|
||||
text: WellbeingService.formatDuration(root.today.active)
|
||||
font.pixelSize: root.wide ? Theme.fontSizeXXLarge : DashMetrics.tileValueSizeCompact
|
||||
font.weight: Theme.fontWeightMedium
|
||||
font.features: ({
|
||||
"tnum": 1
|
||||
})
|
||||
color: root.overLimit ? Theme.error : root.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: value.bottom
|
||||
anchors.topMargin: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
visible: root.apps.length > 0
|
||||
|
||||
Repeater {
|
||||
model: root.apps
|
||||
|
||||
Item {
|
||||
id: appRow
|
||||
required property var modelData
|
||||
width: parent.width
|
||||
height: root.appRowHeight
|
||||
|
||||
CcAppIcon {
|
||||
id: icon
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
appId: appRow.modelData.appId
|
||||
iconSize: Theme.iconSizeMedium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: time.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: WellbeingService.appName(appRow.modelData.appId)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: time
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: WellbeingService.formatDuration(appRow.modelData.seconds)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.features: ({
|
||||
"tnum": 1
|
||||
})
|
||||
color: root.mutedColor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Modules.DankDash
|
||||
|
||||
CcSheetDialog {
|
||||
id: root
|
||||
|
||||
property string appId: ""
|
||||
readonly property int minutes: WellbeingService.appLimitMinutes(appId)
|
||||
|
||||
function presentFor(id) {
|
||||
appId = id;
|
||||
present();
|
||||
}
|
||||
|
||||
panelWidth: DashMetrics.optionSheetWidth
|
||||
iconName: "timer"
|
||||
title: WellbeingService.appName(appId)
|
||||
subtitle: I18n.tr("Daily limit")
|
||||
showScrollBar: false
|
||||
|
||||
SettingsGroup {
|
||||
width: parent.width
|
||||
slotColor: Theme.chipSurface
|
||||
|
||||
SettingsRow {
|
||||
title: I18n.tr("Daily limit")
|
||||
subtitle: root.minutes > 0 ? I18n.tr("Notifies when today's use of this app passes the limit") : I18n.tr("Off")
|
||||
|
||||
DurationSteppers {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minutes: root.minutes
|
||||
onCommitted: next => WellbeingService.setAppLimit(root.appId, next)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import "Wellbeing.js" as Wellbeing
|
||||
|
||||
DankCard {
|
||||
id: root
|
||||
|
||||
property var days: []
|
||||
property int firstDayOfWeek: 1
|
||||
property string period: "week"
|
||||
property bool showTitle: true
|
||||
property bool fillHeight: true
|
||||
|
||||
readonly property var periods: [
|
||||
{
|
||||
"value": "day",
|
||||
"text": I18n.tr("Day")
|
||||
},
|
||||
{
|
||||
"value": "week",
|
||||
"text": I18n.tr("Week")
|
||||
},
|
||||
{
|
||||
"value": "month",
|
||||
"text": I18n.tr("Month")
|
||||
}
|
||||
]
|
||||
readonly property var apps: Wellbeing.topApps(Wellbeing.periodDays(days, new Date(), firstDayOfWeek, period), WellbeingMetrics.listedApps)
|
||||
readonly property real longest: apps.length > 0 ? apps[0].seconds : 0
|
||||
readonly property real listHeight: Math.max(1, apps.length) * WellbeingMetrics.appRowHeight + Math.max(0, apps.length - 1) * Theme.groupedListGap
|
||||
|
||||
signal limitRequested(string appId)
|
||||
|
||||
restRadius: DashMetrics.cardRadius
|
||||
pad: Theme.spacingL
|
||||
implicitHeight: pad * 2 + header.height + Theme.spacingM + listHeight
|
||||
|
||||
Item {
|
||||
id: header
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
height: Theme.buttonHeightXS
|
||||
|
||||
StyledText {
|
||||
anchors.left: parent.left
|
||||
anchors.right: periodGroup.left
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.showTitle
|
||||
text: I18n.tr("Most used apps")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: root.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
DankButtonGroup {
|
||||
id: periodGroup
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
size: "small"
|
||||
checkEnabled: false
|
||||
model: root.periods.map(p => p.text)
|
||||
currentIndex: root.periods.findIndex(p => p.value === root.period)
|
||||
onSelectionChanged: (index, selected) => {
|
||||
if (selected)
|
||||
root.period = root.periods[index].value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: listHost
|
||||
width: listHost.width - Theme.spacingL * 2
|
||||
visible: root.apps.length === 0
|
||||
text: I18n.tr("Nothing tracked yet", "empty state of the screen time app list")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.mutedColor
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: listHost
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: header.bottom
|
||||
anchors.topMargin: Theme.spacingM
|
||||
height: root.fillHeight ? parent.height - y : root.listHeight
|
||||
sourceComponent: root.fillHeight ? scrolling : stacked
|
||||
}
|
||||
|
||||
// a nested list view would take the wheel from the settings page
|
||||
Component {
|
||||
id: stacked
|
||||
|
||||
Column {
|
||||
spacing: Theme.groupedListGap
|
||||
|
||||
Repeater {
|
||||
model: root.apps
|
||||
|
||||
AppRow {
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: scrolling
|
||||
|
||||
DankListView {
|
||||
clip: true
|
||||
spacing: Theme.groupedListGap
|
||||
showScrollBar: false
|
||||
model: root.apps
|
||||
|
||||
delegate: AppRow {
|
||||
width: ListView.view.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component AppRow: DankListItem {
|
||||
id: row
|
||||
|
||||
required property var modelData
|
||||
required property int index
|
||||
|
||||
readonly property string appId: modelData.appId
|
||||
readonly property int limitMinutes: WellbeingService.appLimitMinutes(appId)
|
||||
readonly property bool overLimit: limitMinutes > 0 && modelData.seconds >= limitMinutes * 60
|
||||
|
||||
height: WellbeingMetrics.appRowHeight
|
||||
surfaceColor: root.chipColor
|
||||
firstInGroup: index === 0
|
||||
lastInGroup: index === root.apps.length - 1
|
||||
Accessible.name: WellbeingService.appName(appId)
|
||||
onClicked: root.limitRequested(appId)
|
||||
|
||||
CcAppIcon {
|
||||
id: icon
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
appId: row.appId
|
||||
iconSize: Theme.iconSizeLarge
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: name
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: WellbeingMetrics.appNameWidth
|
||||
text: WellbeingService.appName(row.appId)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: row.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: name.right
|
||||
anchors.right: duration.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: DashMetrics.meterBarThickness
|
||||
radius: Theme.fullRadius(width, height)
|
||||
color: Theme.withAlpha(row.contentColor, Theme.stateLayerFocus)
|
||||
|
||||
Rectangle {
|
||||
width: root.longest > 0 ? parent.width * row.modelData.seconds / root.longest : 0
|
||||
height: parent.height
|
||||
radius: parent.radius
|
||||
color: row.overLimit ? Theme.error : Theme.primary
|
||||
|
||||
Behavior on width {
|
||||
enabled: DashMetrics.animationsEnabled
|
||||
NumberAnimation {
|
||||
duration: DashMetrics.meterDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NumericText {
|
||||
id: duration
|
||||
anchors.right: limitButton.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: reservedWidth
|
||||
horizontalAlignment: Text.AlignRight
|
||||
reserveText: WellbeingService.formatDuration(10 * Wellbeing.secondsPerHour)
|
||||
text: WellbeingService.formatDuration(row.modelData.seconds)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: row.overLimit ? Theme.error : row.contentColor
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
id: limitButton
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonSize: Theme.buttonHeightXS
|
||||
iconName: row.limitMinutes > 0 ? "timer" : "timer_off"
|
||||
iconColor: row.limitMinutes > 0 ? Theme.primary : row.supportingContentColor
|
||||
tooltipText: row.limitMinutes > 0 ? I18n.tr("%1 min").arg(row.limitMinutes) : I18n.tr("Set limit", "button that opens the per-app screen time limit editor")
|
||||
onClicked: root.limitRequested(row.appId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
Row {
|
||||
id: root
|
||||
|
||||
property int minutes: 0
|
||||
readonly property int hours: Math.floor(minutes / 60)
|
||||
readonly property int remainder: minutes % 60
|
||||
readonly property int maxMinutes: 24 * 60 - 1
|
||||
|
||||
signal committed(int minutes)
|
||||
|
||||
function commit(next) {
|
||||
committed(Math.max(0, Math.min(maxMinutes, next)));
|
||||
}
|
||||
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankNumberStepper {
|
||||
text: I18n.tr("%1h").arg(root.hours)
|
||||
incrementEnabled: root.minutes + 60 <= root.maxMinutes
|
||||
decrementEnabled: root.hours > 0
|
||||
onIncrement: () => root.commit(root.minutes + 60)
|
||||
onDecrement: () => root.commit(root.minutes - 60)
|
||||
}
|
||||
|
||||
DankNumberStepper {
|
||||
text: I18n.tr("%1m").arg(root.remainder)
|
||||
incrementEnabled: root.minutes < root.maxMinutes
|
||||
decrementEnabled: root.minutes > 0
|
||||
onIncrement: () => root.commit(root.minutes + 1)
|
||||
onDecrement: () => root.commit(root.minutes - 1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
import "Wellbeing.js" as Wellbeing
|
||||
|
||||
DankCard {
|
||||
id: root
|
||||
|
||||
property var week: []
|
||||
property real limitSeconds: 0
|
||||
property bool showTitle: true
|
||||
property int hoverIndex: -1
|
||||
|
||||
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 var hoverDay: hoverIndex >= 0 && hoverIndex < week.length ? week[hoverIndex] : null
|
||||
readonly property real columnWidth: week.length > 0 ? plot.width / week.length : 0
|
||||
readonly property real barWidth: columnWidth * WellbeingMetrics.barWidthRatio
|
||||
readonly property color pastColor: Theme.primaryContainer
|
||||
readonly property color todayColor: Theme.primary
|
||||
readonly property color overColor: Theme.error
|
||||
|
||||
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 Qt.locale().dayName(day.weekday, Locale.ShortFormat);
|
||||
}
|
||||
|
||||
restRadius: DashMetrics.cardRadius
|
||||
pad: Theme.spacingL
|
||||
Accessible.role: Accessible.Chart
|
||||
Accessible.name: I18n.tr("Weekly screen time")
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
visible: root.showTitle
|
||||
text: I18n.tr("Weekly screen time")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Theme.fontWeightMedium
|
||||
color: root.contentColor
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledText {
|
||||
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
|
||||
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: dayLabels.top
|
||||
anchors.bottomMargin: 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
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.week
|
||||
|
||||
Rectangle {
|
||||
id: bar
|
||||
required property var modelData
|
||||
required property int index
|
||||
readonly property real targetHeight: Math.max(0, plot.height - root.yFor(modelData.active))
|
||||
x: index * root.columnWidth + (root.columnWidth - width) / 2
|
||||
y: plot.height - height
|
||||
width: root.barWidth
|
||||
height: targetHeight
|
||||
visible: !modelData.future && modelData.active > 0
|
||||
topLeftRadius: Theme.cornerRadiusS
|
||||
topRightRadius: Theme.cornerRadiusS
|
||||
color: root.barColor(modelData)
|
||||
opacity: root.hoverIndex === -1 || 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
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: parent.right
|
||||
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: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
onPositionChanged: mouse => root.hoverIndex = root.columnWidth > 0 ? Math.floor(mouse.x / root.columnWidth) : -1
|
||||
onExited: root.hoverIndex = -1
|
||||
}
|
||||
}
|
||||
|
||||
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").arg(modelData)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.features: ({
|
||||
"tnum": 1
|
||||
})
|
||||
color: root.mutedColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: dayLabels
|
||||
anchors.left: plot.left
|
||||
anchors.right: plot.right
|
||||
anchors.bottom: parent.bottom
|
||||
height: Theme.fontSizeSmall + Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: root.week
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
.pragma library
|
||||
|
||||
const secondsPerHour = 3600;
|
||||
const weekLength = 7;
|
||||
const monthLength = 30;
|
||||
|
||||
function dateKey(date) {
|
||||
const pad = n => String(n).padStart(2, "0");
|
||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
||||
}
|
||||
|
||||
function parseKey(key) {
|
||||
const [year, month, day] = String(key).split("-").map(Number);
|
||||
return new Date(year, month - 1, day);
|
||||
}
|
||||
|
||||
function emptyDay(key) {
|
||||
return {
|
||||
date: key,
|
||||
active: 0,
|
||||
apps: {}
|
||||
};
|
||||
}
|
||||
|
||||
function byDate(days) {
|
||||
const map = {};
|
||||
for (const day of Array.isArray(days) ? days : []) {
|
||||
if (day && typeof day.date === "string")
|
||||
map[day.date] = day;
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function weekDays(days, today, firstDayOfWeek) {
|
||||
const map = byDate(days);
|
||||
const todayKey = dateKey(today);
|
||||
const offset = (today.getDay() - firstDayOfWeek + weekLength) % weekLength;
|
||||
const result = [];
|
||||
for (let i = 0; i < weekLength; i++) {
|
||||
const date = new Date(today.getFullYear(), today.getMonth(), today.getDate() - offset + i);
|
||||
const key = dateKey(date);
|
||||
result.push({
|
||||
date: key,
|
||||
weekday: date.getDay(),
|
||||
active: map[key]?.active ?? 0,
|
||||
apps: map[key]?.apps ?? {},
|
||||
today: key === todayKey,
|
||||
future: key > todayKey
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function periodDays(days, today, firstDayOfWeek, period) {
|
||||
switch (period) {
|
||||
case "day":
|
||||
return weekDays(days, today, firstDayOfWeek).filter(day => day.today);
|
||||
case "week":
|
||||
return weekDays(days, today, firstDayOfWeek).filter(day => !day.future);
|
||||
default:
|
||||
return Array.isArray(days) ? days.slice(-monthLength) : [];
|
||||
}
|
||||
}
|
||||
|
||||
function totalSeconds(days) {
|
||||
return days.reduce((sum, day) => sum + (day?.active ?? 0), 0);
|
||||
}
|
||||
|
||||
function topApps(days, limit) {
|
||||
const totals = {};
|
||||
for (const day of days) {
|
||||
const apps = day?.apps ?? {};
|
||||
for (const appId in apps) {
|
||||
if (apps[appId] > 0)
|
||||
totals[appId] = (totals[appId] ?? 0) + apps[appId];
|
||||
}
|
||||
}
|
||||
return Object.keys(totals).map(appId => ({
|
||||
appId,
|
||||
seconds: totals[appId]
|
||||
})).sort((a, b) => b.seconds - a.seconds || a.appId.localeCompare(b.appId)).slice(0, limit ?? Infinity);
|
||||
}
|
||||
|
||||
function axisStepHours(hours) {
|
||||
if (hours <= 4)
|
||||
return 1;
|
||||
if (hours <= 8)
|
||||
return 2;
|
||||
if (hours <= 16)
|
||||
return 4;
|
||||
return 6;
|
||||
}
|
||||
|
||||
function axis(peakSeconds) {
|
||||
const hours = Math.max(1, Math.ceil(peakSeconds / secondsPerHour));
|
||||
const step = axisStepHours(hours);
|
||||
const top = Math.ceil(hours / step) * step;
|
||||
const ticks = [];
|
||||
for (let hour = 0; hour <= top; hour += step)
|
||||
ticks.push(hour);
|
||||
return {
|
||||
top: top * secondsPerHour,
|
||||
ticks
|
||||
};
|
||||
}
|
||||
|
||||
function splitDuration(seconds) {
|
||||
const total = Math.max(0, Math.round(seconds));
|
||||
return {
|
||||
hours: Math.floor(total / secondsPerHour),
|
||||
minutes: Math.floor((total % secondsPerHour) / 60)
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
pragma Singleton
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Modules.DankDash
|
||||
|
||||
Singleton {
|
||||
readonly property real chartHeight: DashMetrics.gridRowUnit * 2.5
|
||||
readonly property real barWidthRatio: 0.72
|
||||
readonly property real dimmedBarOpacity: 0.55
|
||||
readonly property real axisLabelWidth: 28
|
||||
readonly property real appNameWidth: 120
|
||||
readonly property real appRowHeight: Theme.listItemHeight
|
||||
readonly property int listedApps: 12
|
||||
readonly property int cardApps: 3
|
||||
readonly property int defaultLimitMinutes: 60
|
||||
readonly property int retentionDays: 92
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modules.DankDash
|
||||
import qs.Modules.DankDash.Wellbeing
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import "Wellbeing/Wellbeing.js" as Wellbeing
|
||||
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
property string entryId: "wellbeing"
|
||||
property bool live: Window.window?.visible ?? false
|
||||
property var transientSurfaceTracker: null
|
||||
property var days: []
|
||||
|
||||
readonly property int firstDayOfWeek: SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0 ? Qt.locale().firstDayOfWeek : SettingsData.firstDayOfWeek
|
||||
readonly property var week: Wellbeing.weekDays(days, new Date(), firstDayOfWeek)
|
||||
readonly property int revision: WellbeingService.revision
|
||||
readonly property Item focusTarget: root
|
||||
readonly property var menuActions: [
|
||||
{
|
||||
label: I18n.tr("Settings"),
|
||||
iconName: "settings",
|
||||
action: () => {
|
||||
PopoutService.closeDankDash();
|
||||
PopoutService.openSettingsWithTab("wellbeing");
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
implicitHeight: DashMetrics.tabMinHeight
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
onLiveChanged: {
|
||||
if (live)
|
||||
refresh();
|
||||
else
|
||||
limitSheet.dismiss();
|
||||
}
|
||||
onRevisionChanged: {
|
||||
if (live)
|
||||
refresh();
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
WellbeingService.summary(result => root.days = result);
|
||||
}
|
||||
|
||||
function handleKeyEvent(event) {
|
||||
if (event.key !== Qt.Key_Escape || !limitSheet.shown)
|
||||
return false;
|
||||
limitSheet.dismiss();
|
||||
return true;
|
||||
}
|
||||
|
||||
CcEmptyState {
|
||||
anchors.centerIn: parent
|
||||
visible: !WellbeingService.available
|
||||
iconName: "digital_wellbeing"
|
||||
title: I18n.tr("DMS out of date")
|
||||
subtitle: I18n.tr("Update the dms package with your package manager, then restart the shell.")
|
||||
}
|
||||
|
||||
Column {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
spacing: DashMetrics.contentGap
|
||||
visible: WellbeingService.available
|
||||
|
||||
WeeklyChart {
|
||||
width: parent.width
|
||||
height: WellbeingMetrics.chartHeight
|
||||
week: root.week
|
||||
limitSeconds: SettingsData.wellbeingDailyLimit * 60
|
||||
}
|
||||
|
||||
AppUsageList {
|
||||
width: parent.width
|
||||
height: parent.height - y
|
||||
days: root.days
|
||||
firstDayOfWeek: root.firstDayOfWeek
|
||||
onLimitRequested: appId => limitSheet.presentFor(appId)
|
||||
}
|
||||
}
|
||||
|
||||
AppLimitSheet {
|
||||
id: limitSheet
|
||||
backdrop: content
|
||||
}
|
||||
}
|
||||
@@ -213,7 +213,7 @@ PanelWindow {
|
||||
readonly property bool contentAnchorsTop: isTopCenter || SettingsData.notificationPopupPosition === SettingsData.Position.Top || SettingsData.notificationPopupPosition === SettingsData.Position.Left
|
||||
readonly property real renderedContentOffsetY: contentAnchorsTop ? 0 : Math.max(0, allocatedAlignedHeight - renderedAlignedHeight)
|
||||
readonly property real contentTop: Theme.snap(windowShadowPad + renderedContentOffsetY, dpr)
|
||||
implicitWidth: contentImplicitWidth + (windowShadowPad * 2)
|
||||
implicitWidth: contentImplicitWidth + (windowShadowPad * 2) + edgeBleed
|
||||
implicitHeight: allocatedAlignedHeight + (windowShadowPad * 2)
|
||||
|
||||
function syncInlineTargetHeight() {
|
||||
@@ -330,6 +330,14 @@ PanelWindow {
|
||||
readonly property real maxPopupShadowOffsetYPx: Math.max(Math.abs(Theme.elevationOffsetY(Theme.elevationLevel3, 6)), Math.abs(Theme.elevationOffsetY(Theme.elevationLevel4, 8)))
|
||||
readonly property bool popupWindowShadowActive: Theme.elevationEnabled && SettingsData.notificationPopupShadowEnabled && !connectedFrameMode
|
||||
readonly property real windowShadowPad: popupWindowShadowActive ? Theme.snap(Math.max(16, maxPopupShadowBlurPx + Math.max(maxPopupShadowOffsetXPx, maxPopupShadowOffsetYPx) + 8), dpr) : 0
|
||||
readonly property bool bleedsLeft: _frameEdgeSwipeDirection() < 0
|
||||
readonly property real edgeBleed: {
|
||||
if (isCenterPosition || !CompositorService.frameWindowVisibleForScreen(screen))
|
||||
return 0;
|
||||
const sideMargin = bleedsLeft ? getLeftMargin() : getRightMargin();
|
||||
return Math.max(0, Theme.snap(sideMargin, dpr) - windowShadowPad);
|
||||
}
|
||||
readonly property real contentWindowX: Theme.snap(windowShadowPad + (bleedsLeft ? edgeBleed : 0), dpr)
|
||||
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
@@ -492,7 +500,7 @@ PanelWindow {
|
||||
function getWindowLeftMargin() {
|
||||
if (!screen)
|
||||
return 0;
|
||||
return Theme.snap(getContentX() - windowShadowPad, dpr);
|
||||
return Theme.snap(getContentX() - contentWindowX, dpr);
|
||||
}
|
||||
|
||||
function getWindowTopMargin() {
|
||||
@@ -526,7 +534,7 @@ PanelWindow {
|
||||
|
||||
readonly property bool screenValid: win.screen && !_isDestroying
|
||||
readonly property real dpr: screenValid ? CompositorService.getScreenScale(win.screen) : 1
|
||||
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2)), dpr)
|
||||
readonly property real alignedWidth: Theme.px(Math.max(0, implicitWidth - (windowShadowPad * 2) - edgeBleed), dpr)
|
||||
readonly property real alignedHeight: renderedAlignedHeight
|
||||
onScreenYChanged: if (connectedFrameMode)
|
||||
popupChromeGeometryChanged()
|
||||
@@ -564,7 +572,7 @@ PanelWindow {
|
||||
id: content
|
||||
parent: slideClip
|
||||
|
||||
x: Theme.snap(windowShadowPad, dpr)
|
||||
x: win.contentWindowX
|
||||
y: win.contentTop - slideClip.y
|
||||
width: alignedWidth
|
||||
height: alignedHeight
|
||||
|
||||
@@ -44,6 +44,19 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: I18n.tr("Digital wellbeing")
|
||||
|
||||
SettingsNavRow {
|
||||
tab: "dank_dash"
|
||||
settingKey: "dashWellbeingSettings"
|
||||
title: I18n.tr("Digital wellbeing")
|
||||
hint: I18n.tr("Screen time and app limits")
|
||||
iconName: "digital_wellbeing"
|
||||
onClicked: root.parentModal?.navigateTo("wellbeing")
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
title: I18n.tr("Tabs", "noun, card title for dashboard tabs")
|
||||
settingKey: "dashTabs"
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import qs.Modals.Common
|
||||
import qs.Modules.Settings.Widgets
|
||||
import qs.Modules.ControlCenter.Widgets
|
||||
import qs.Modules.DankDash.Wellbeing
|
||||
import "../DankDash/Wellbeing/Wellbeing.js" as Wellbeing
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var parentModal: null
|
||||
property var days: []
|
||||
|
||||
readonly property var limits: SettingsData.wellbeingAppLimits ?? {}
|
||||
readonly property var limitedApps: Object.keys(limits).filter(appId => Number(limits[appId]) > 0).sort((a, b) => WellbeingService.appName(a).localeCompare(WellbeingService.appName(b)))
|
||||
readonly property bool available: WellbeingService.available
|
||||
readonly property int revision: WellbeingService.revision
|
||||
readonly property int firstDayOfWeek: SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0 ? Qt.locale().firstDayOfWeek : SettingsData.firstDayOfWeek
|
||||
|
||||
onAvailableChanged: refresh()
|
||||
onRevisionChanged: refresh()
|
||||
Component.onCompleted: refresh()
|
||||
|
||||
function toggleLimit(appId) {
|
||||
WellbeingService.setAppLimit(appId, WellbeingService.appLimitMinutes(appId) > 0 ? 0 : WellbeingMetrics.defaultLimitMinutes);
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
if (!available)
|
||||
return;
|
||||
WellbeingService.summary(result => root.days = result);
|
||||
}
|
||||
|
||||
ConfirmModal {
|
||||
id: clearConfirm
|
||||
}
|
||||
|
||||
SettingsPage {
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
visible: root.available && SettingsData.wellbeingEnabled
|
||||
iconName: "show_chart"
|
||||
title: I18n.tr("Weekly screen time")
|
||||
settingKey: "wellbeingChart"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "chart", "week"]
|
||||
|
||||
WeeklyChart {
|
||||
height: WellbeingMetrics.chartHeight
|
||||
color: "transparent"
|
||||
pad: 0
|
||||
showTitle: false
|
||||
week: Wellbeing.weekDays(root.days, new Date(), root.firstDayOfWeek)
|
||||
limitSeconds: SettingsData.wellbeingDailyLimit * 60
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
visible: root.available && SettingsData.wellbeingEnabled
|
||||
iconName: "apps"
|
||||
title: I18n.tr("Most used apps")
|
||||
settingKey: "wellbeingApps"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "apps", "usage"]
|
||||
|
||||
AppUsageList {
|
||||
height: implicitHeight
|
||||
color: "transparent"
|
||||
pad: 0
|
||||
showTitle: false
|
||||
fillHeight: false
|
||||
days: root.days
|
||||
firstDayOfWeek: root.firstDayOfWeek
|
||||
onLimitRequested: appId => root.toggleLimit(appId)
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "digital_wellbeing"
|
||||
title: I18n.tr("Screen time")
|
||||
settingKey: "wellbeing"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "usage", "limit"]
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: "wellbeingEnabled"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "tracking"]
|
||||
text: I18n.tr("Track screen time")
|
||||
description: I18n.tr("Counts time in the focused app while the session is active. Stays on this device.")
|
||||
checked: SettingsData.wellbeingEnabled
|
||||
onToggled: checked => SettingsData.set("wellbeingEnabled", checked)
|
||||
}
|
||||
|
||||
SettingsRow {
|
||||
settingKey: "wellbeingDailyLimit"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "limit", "daily"]
|
||||
title: I18n.tr("Daily limit")
|
||||
subtitle: SettingsData.wellbeingDailyLimit > 0 ? I18n.tr("Notifies when today's screen time passes the limit") : I18n.tr("Off")
|
||||
|
||||
DurationSteppers {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minutes: SettingsData.wellbeingDailyLimit
|
||||
onCommitted: next => SettingsData.set("wellbeingDailyLimit", next)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "timer"
|
||||
title: I18n.tr("App limits")
|
||||
settingKey: "wellbeingAppLimits"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "limit", "app"]
|
||||
|
||||
SettingsRow {
|
||||
visible: root.limitedApps.length === 0
|
||||
title: I18n.tr("No app limits")
|
||||
subtitle: I18n.tr("Pick an app from the list above or the dashboard tab")
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: root.limitedApps
|
||||
|
||||
SettingsRow {
|
||||
id: limitRow
|
||||
|
||||
required property string modelData
|
||||
|
||||
settingKey: "wellbeingAppLimits:" + modelData
|
||||
resetKeys: []
|
||||
title: WellbeingService.appName(modelData)
|
||||
|
||||
leading: CcAppIcon {
|
||||
appId: limitRow.modelData
|
||||
iconSize: Theme.iconSizeLarge
|
||||
}
|
||||
|
||||
DurationSteppers {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
minutes: WellbeingService.appLimitMinutes(limitRow.modelData)
|
||||
onCommitted: next => WellbeingService.setAppLimit(limitRow.modelData, next)
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "close"
|
||||
iconColor: Theme.surfaceVariantText
|
||||
Accessible.name: I18n.tr("Remove")
|
||||
onClicked: WellbeingService.setAppLimit(limitRow.modelData, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "history"
|
||||
title: I18n.tr("History")
|
||||
settingKey: "wellbeingHistory"
|
||||
tab: "wellbeing"
|
||||
tags: ["wellbeing", "screen time", "history", "clear"]
|
||||
|
||||
SettingsRow {
|
||||
title: I18n.tr("Screen time history")
|
||||
subtitle: I18n.tr("Kept for %1 days on this device").arg(WellbeingMetrics.retentionDays)
|
||||
|
||||
DankButton {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buttonHeight: Theme.buttonHeightXS
|
||||
text: I18n.tr("Clear")
|
||||
backgroundColor: Theme.errorContainer
|
||||
textColor: Theme.error
|
||||
enabled: root.available
|
||||
onClicked: clearConfirm.showWithOptions({
|
||||
"title": I18n.tr("Clear History?"),
|
||||
"message": I18n.tr("Deletes all stored screen time"),
|
||||
"confirmText": I18n.tr("Clear"),
|
||||
"confirmColor": Theme.error,
|
||||
"onConfirm": () => {
|
||||
WellbeingService.clearHistory();
|
||||
root.days = [];
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user