mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-06-25 12:35:21 -04:00
578 lines
24 KiB
QML
578 lines
24 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Layouts
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
focus: true
|
|
property string highlightedId: ""
|
|
|
|
readonly property var __presentation: ({
|
|
"overview": {
|
|
"icon": "dashboard",
|
|
"text": I18n.tr("Overview"),
|
|
"description": I18n.tr("Clock, calendar, system info and profile")
|
|
},
|
|
"media": {
|
|
"icon": "music_note",
|
|
"text": I18n.tr("Media"),
|
|
"description": I18n.tr("Now playing and media controls")
|
|
},
|
|
"wallpaper": {
|
|
"icon": "wallpaper",
|
|
"text": I18n.tr("Wallpapers"),
|
|
"description": I18n.tr("Browse and set wallpapers")
|
|
},
|
|
"weather": {
|
|
"icon": "wb_sunny",
|
|
"text": I18n.tr("Weather"),
|
|
"description": SettingsData.weatherEnabled ? I18n.tr("Forecast and conditions") : I18n.tr("Hidden until weather is enabled")
|
|
},
|
|
"settings": {
|
|
"icon": "settings",
|
|
"text": I18n.tr("Settings"),
|
|
"description": I18n.tr("Shortcut that opens this settings window")
|
|
}
|
|
})
|
|
|
|
// Stable model: the canonical id list never reorders, so the Repeater keeps
|
|
// its delegates alive across commits (preserving focus for keyboard reorder)
|
|
readonly property var tabIds: SettingsData._dashTabIds
|
|
readonly property var tabState: SettingsData.getDashTabs()
|
|
readonly property int enabledContentCount: tabState.filter(t => t.enabled && t.id !== "settings").length
|
|
|
|
function presentationFor(id) {
|
|
return __presentation[id] ?? {
|
|
"icon": "tab",
|
|
"text": id,
|
|
"description": ""
|
|
};
|
|
}
|
|
function isEnabled(id) {
|
|
const t = tabState.find(t => t.id === id);
|
|
return t ? t.enabled : false;
|
|
}
|
|
|
|
readonly property real rowHeight: 70
|
|
readonly property real rowSpacing: Theme.spacingM
|
|
readonly property real dividerGap: 40
|
|
|
|
property var enabledOrder: []
|
|
property var disabledOrder: []
|
|
property string draggingId: ""
|
|
property var dragStartOrder: []
|
|
|
|
readonly property bool hasHidden: disabledOrder.length > 0
|
|
readonly property real dividerY: enabledOrder.length * (rowHeight + rowSpacing)
|
|
readonly property real totalHeight: {
|
|
const base = enabledOrder.length * (rowHeight + rowSpacing);
|
|
if (!hasHidden)
|
|
return Math.max(0, base - rowSpacing);
|
|
return base + dividerGap + disabledOrder.length * (rowHeight + rowSpacing) - rowSpacing;
|
|
}
|
|
|
|
function rebuild() {
|
|
const en = [];
|
|
const dis = [];
|
|
for (var i = 0; i < tabState.length; i++) {
|
|
if (tabState[i].enabled)
|
|
en.push(tabState[i].id);
|
|
else
|
|
dis.push(tabState[i].id);
|
|
}
|
|
enabledOrder = en;
|
|
disabledOrder = dis;
|
|
}
|
|
|
|
onTabStateChanged: rebuild()
|
|
Component.onCompleted: rebuild()
|
|
|
|
function slotYForId(id) {
|
|
const p = enabledOrder.indexOf(id);
|
|
if (p >= 0)
|
|
return p * (rowHeight + rowSpacing);
|
|
const k = disabledOrder.indexOf(id);
|
|
return dividerY + dividerGap + Math.max(0, k) * (rowHeight + rowSpacing);
|
|
}
|
|
|
|
function beginDrag(id) {
|
|
draggingId = id;
|
|
dragStartOrder = enabledOrder.slice();
|
|
}
|
|
|
|
function updateDragTarget(centerY) {
|
|
if (draggingId === "")
|
|
return;
|
|
var pos = Math.floor(centerY / (rowHeight + rowSpacing));
|
|
pos = Math.max(0, Math.min(pos, enabledOrder.length - 1));
|
|
const arr = enabledOrder.slice();
|
|
const d = arr.indexOf(draggingId);
|
|
if (d < 0 || d === pos)
|
|
return;
|
|
arr.splice(d, 1);
|
|
arr.splice(pos, 0, draggingId);
|
|
enabledOrder = arr;
|
|
}
|
|
|
|
function commit() {
|
|
SettingsData.setDashTabOrder(enabledOrder.concat(disabledOrder));
|
|
}
|
|
|
|
function endDrag() {
|
|
if (draggingId === "")
|
|
return;
|
|
const changed = JSON.stringify(enabledOrder) !== JSON.stringify(dragStartOrder);
|
|
draggingId = "";
|
|
if (changed)
|
|
commit();
|
|
}
|
|
|
|
function moveEnabled(id, delta) {
|
|
const pos = enabledOrder.indexOf(id);
|
|
const next = pos + delta;
|
|
if (pos < 0 || next < 0 || next >= enabledOrder.length)
|
|
return;
|
|
const arr = enabledOrder.slice();
|
|
arr.splice(pos, 1);
|
|
arr.splice(next, 0, id);
|
|
enabledOrder = arr;
|
|
commit();
|
|
}
|
|
|
|
function canHide(id) {
|
|
return !isEnabled(id) || id === "settings" || enabledContentCount > 1;
|
|
}
|
|
|
|
// Keyboard nav is handled at the tab root (not per-row activeFocusOnTab)
|
|
Keys.onPressed: function (event) {
|
|
const order = enabledOrder.concat(disabledOrder);
|
|
if (order.length === 0)
|
|
return;
|
|
const ctrl = (event.modifiers & Qt.ControlModifier) !== 0;
|
|
if (event.key === Qt.Key_Up || event.key === Qt.Key_Down) {
|
|
const dir = event.key === Qt.Key_Down ? 1 : -1;
|
|
if (ctrl) {
|
|
if (highlightedId !== "" && isEnabled(highlightedId))
|
|
moveEnabled(highlightedId, dir);
|
|
} else if (highlightedId === "") {
|
|
highlightedId = dir > 0 ? order[0] : order[order.length - 1];
|
|
} else {
|
|
var idx = order.indexOf(highlightedId);
|
|
idx = Math.max(0, Math.min(order.length - 1, idx + dir));
|
|
highlightedId = order[idx];
|
|
}
|
|
event.accepted = true;
|
|
} else if ((event.key === Qt.Key_Space || event.key === Qt.Key_Return) && highlightedId !== "") {
|
|
if (canHide(highlightedId))
|
|
SettingsData.setDashTabEnabled(highlightedId, !isEnabled(highlightedId));
|
|
event.accepted = true;
|
|
}
|
|
}
|
|
|
|
DankFlickable {
|
|
anchors.fill: parent
|
|
clip: true
|
|
contentHeight: mainColumn.height + Theme.spacingXL
|
|
contentWidth: width
|
|
|
|
Column {
|
|
id: mainColumn
|
|
topPadding: 4
|
|
width: Math.min(550, parent.width - Theme.spacingL * 2)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
spacing: Theme.spacingXL
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: headerContent.implicitHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
border.width: 0
|
|
|
|
Column {
|
|
id: headerContent
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingL
|
|
spacing: Theme.spacingM
|
|
|
|
RowLayout {
|
|
id: headerText
|
|
width: parent.width
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "space_dashboard"
|
|
size: Theme.iconSize
|
|
color: Theme.primary
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Dank Dash")
|
|
font.pixelSize: Theme.fontSizeLarge
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
Layout.alignment: Qt.AlignVCenter
|
|
}
|
|
|
|
Item {
|
|
height: 1
|
|
Layout.fillWidth: true
|
|
}
|
|
|
|
Rectangle {
|
|
id: resetButton
|
|
width: resetContentRow.implicitWidth + Theme.spacingM * 2
|
|
height: 28
|
|
radius: Theme.cornerRadius
|
|
color: resetArea.containsMouse ? Theme.surfacePressed : Theme.surfaceVariant
|
|
border.width: 0
|
|
Layout.alignment: Qt.AlignVCenter
|
|
|
|
Row {
|
|
id: resetContentRow
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingXS
|
|
|
|
DankIcon {
|
|
name: "refresh"
|
|
size: 14
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Reset")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
color: Theme.surfaceText
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: resetArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: SettingsData.resetDashTabs()
|
|
}
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Theme.standardEasing
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Drag to reorder or click to hide tabs. Use ↑/↓ to highlight a tab and Ctrl+↑/↓ to move it.")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: Theme.surfaceVariantText
|
|
width: parent.width
|
|
wrapMode: Text.WordWrap
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledRect {
|
|
width: parent.width
|
|
height: root.totalHeight + Theme.spacingL * 2
|
|
radius: Theme.cornerRadius
|
|
color: Theme.surfaceContainerHigh
|
|
border.width: 0
|
|
|
|
Item {
|
|
id: reorderArea
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.margins: Theme.spacingL
|
|
height: root.totalHeight
|
|
|
|
Behavior on height {
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.normal
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: hiddenDivider
|
|
width: parent.width
|
|
height: root.dividerGap
|
|
y: root.dividerY + (root.rowSpacing / 2)
|
|
opacity: root.hasHidden ? 1 : 0
|
|
visible: opacity > 0.01
|
|
|
|
Behavior on y {
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.normal
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
|
}
|
|
}
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
|
|
Row {
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
name: "visibility_off"
|
|
size: 14
|
|
color: Theme.outline
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
StyledText {
|
|
text: I18n.tr("Hidden")
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
color: Theme.outline
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
|
|
Rectangle {
|
|
width: parent.width - x
|
|
height: 1
|
|
color: Theme.outline
|
|
opacity: 0.2
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
}
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: root.tabIds
|
|
|
|
delegate: Item {
|
|
id: rowItem
|
|
required property int index
|
|
required property string modelData
|
|
|
|
readonly property var present: root.presentationFor(modelData)
|
|
readonly property bool isEnabled: root.isEnabled(modelData)
|
|
readonly property bool dragging: root.draggingId === modelData
|
|
readonly property bool highlighted: root.highlightedId === modelData
|
|
readonly property bool canHide: root.canHide(modelData)
|
|
|
|
width: reorderArea.width
|
|
height: root.rowHeight
|
|
z: dragging ? 100 : (highlighted ? 3 : 1)
|
|
|
|
Binding {
|
|
target: rowItem
|
|
property: "y"
|
|
value: root.slotYForId(rowItem.modelData)
|
|
when: !rowItem.dragging
|
|
restoreMode: Binding.RestoreNone
|
|
}
|
|
|
|
onYChanged: {
|
|
if (dragging)
|
|
root.updateDragTarget(y + height / 2);
|
|
}
|
|
|
|
Behavior on y {
|
|
enabled: !rowItem.dragging
|
|
NumberAnimation {
|
|
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
|
easing.type: Easing.BezierSpline
|
|
easing.bezierCurve: Theme.expressiveCurves.expressiveFastSpatial
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: content
|
|
anchors.fill: parent
|
|
scale: rowItem.dragging ? 1.02 : 1.0
|
|
transformOrigin: Item.Center
|
|
|
|
Behavior on scale {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: surface
|
|
anchors.fill: parent
|
|
radius: rowItem.dragging ? Theme.cornerRadius + 6 : Theme.cornerRadius
|
|
color: {
|
|
if (rowItem.dragging)
|
|
return Theme.secondaryContainer;
|
|
const base = Theme.surfaceContainer;
|
|
return Qt.rgba(base.r, base.g, base.b, rowItem.isEnabled ? 0.7 : 0.4);
|
|
}
|
|
border.width: rowItem.dragging ? 2 : 1
|
|
border.color: rowItem.dragging ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
|
|
Behavior on radius {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
easing.type: Easing.OutCubic
|
|
}
|
|
}
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
Behavior on border.color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: parent.radius
|
|
color: Theme.primary
|
|
opacity: (dragArea.containsMouse && !rowItem.dragging) ? 0.06 : 0
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
id: dragHandle
|
|
name: "drag_indicator"
|
|
size: Theme.iconSize - 4
|
|
color: rowItem.dragging ? Theme.primary : Theme.outline
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
opacity: rowItem.isEnabled ? ((dragArea.containsMouse || rowItem.dragging || rowItem.highlighted) ? 1.0 : 0.45) : 0
|
|
visible: opacity > 0.01
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
}
|
|
|
|
DankIcon {
|
|
id: tabIcon
|
|
name: rowItem.present.icon
|
|
size: Theme.iconSize
|
|
color: rowItem.isEnabled ? Theme.primary : Theme.outline
|
|
anchors.left: parent.left
|
|
anchors.leftMargin: Theme.spacingM * 2 + Theme.iconSize - 4
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
Behavior on color {
|
|
ColorAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
}
|
|
|
|
Column {
|
|
anchors.left: tabIcon.right
|
|
anchors.leftMargin: Theme.spacingM
|
|
anchors.right: visibilityButton.left
|
|
anchors.rightMargin: Theme.spacingM
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 2
|
|
|
|
StyledText {
|
|
text: rowItem.present.text
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
font.weight: Font.Medium
|
|
color: rowItem.isEnabled ? Theme.surfaceText : Theme.outline
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
}
|
|
|
|
StyledText {
|
|
text: rowItem.present.description
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
color: rowItem.isEnabled ? Theme.outline : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
|
elide: Text.ElideRight
|
|
width: parent.width
|
|
visible: text.length > 0
|
|
}
|
|
}
|
|
|
|
DankActionButton {
|
|
id: visibilityButton
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: Theme.spacingS
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
buttonSize: 36
|
|
iconName: rowItem.isEnabled ? "visibility" : "visibility_off"
|
|
iconSize: 18
|
|
iconColor: rowItem.isEnabled ? Theme.primary : Theme.outline
|
|
enabled: rowItem.canHide
|
|
onClicked: {
|
|
root.forceActiveFocus();
|
|
root.highlightedId = rowItem.modelData;
|
|
SettingsData.setDashTabEnabled(rowItem.modelData, !rowItem.isEnabled);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
anchors.margins: -2
|
|
radius: Theme.cornerRadius + 2
|
|
color: "transparent"
|
|
border.width: 2
|
|
border.color: Theme.primary
|
|
opacity: rowItem.highlighted && !rowItem.dragging ? 0.6 : 0
|
|
visible: opacity > 0.01
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation {
|
|
duration: Theme.shortDuration
|
|
}
|
|
}
|
|
}
|
|
|
|
MouseArea {
|
|
id: dragArea
|
|
anchors.fill: parent
|
|
anchors.rightMargin: 48
|
|
hoverEnabled: true
|
|
enabled: rowItem.isEnabled
|
|
cursorShape: rowItem.dragging ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
|
drag.target: rowItem
|
|
drag.axis: Drag.YAxis
|
|
drag.minimumY: -rowItem.height
|
|
drag.maximumY: reorderArea.height
|
|
drag.smoothed: false
|
|
onPressed: {
|
|
root.forceActiveFocus();
|
|
root.highlightedId = rowItem.modelData;
|
|
root.beginDrag(rowItem.modelData);
|
|
}
|
|
onReleased: root.endDrag()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|