mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-24 13:32:50 -05:00
485 lines
20 KiB
QML
485 lines
20 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property var controller: null
|
|
property int gridColumns: controller?.gridColumns ?? 4
|
|
|
|
signal itemRightClicked(int index, var item, real mouseX, real mouseY)
|
|
|
|
function resetScroll() {
|
|
mainFlickable.contentY = 0;
|
|
}
|
|
|
|
function ensureVisible(index) {
|
|
if (index < 0 || !controller?.flatModel || index >= controller.flatModel.length)
|
|
return;
|
|
var entry = controller.flatModel[index];
|
|
if (!entry || entry.isHeader)
|
|
return;
|
|
scrollItemIntoView(index, entry.sectionId);
|
|
}
|
|
|
|
function scrollItemIntoView(flatIndex, sectionId) {
|
|
var sections = controller?.sections ?? [];
|
|
var sectionIndex = -1;
|
|
for (var i = 0; i < sections.length; i++) {
|
|
if (sections[i].id === sectionId) {
|
|
sectionIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (sectionIndex < 0)
|
|
return;
|
|
var itemInSection = 0;
|
|
var foundSection = false;
|
|
for (var i = 0; i < controller.flatModel.length && i < flatIndex; i++) {
|
|
var e = controller.flatModel[i];
|
|
if (e.isHeader && e.section?.id === sectionId)
|
|
foundSection = true;
|
|
else if (foundSection && !e.isHeader && e.sectionId === sectionId)
|
|
itemInSection++;
|
|
}
|
|
|
|
var mode = controller.getSectionViewMode(sectionId);
|
|
var sectionY = 0;
|
|
for (var i = 0; i < sectionIndex; i++) {
|
|
sectionY += getSectionHeight(sections[i]);
|
|
}
|
|
|
|
var itemY, itemHeight;
|
|
if (mode === "list") {
|
|
itemY = itemInSection * 52;
|
|
itemHeight = 52;
|
|
} else {
|
|
var cols = controller.getGridColumns(sectionId);
|
|
var cellWidth = mode === "tile" ? Math.floor(mainFlickable.width / 3) : Math.floor(mainFlickable.width / root.gridColumns);
|
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
|
var row = Math.floor(itemInSection / cols);
|
|
itemY = row * cellHeight;
|
|
itemHeight = cellHeight;
|
|
}
|
|
|
|
var targetY = sectionY + 32 + itemY;
|
|
var targetBottom = targetY + itemHeight;
|
|
var stickyHeight = mainFlickable.contentY > 0 ? 32 : 0;
|
|
|
|
var shadowPadding = 24;
|
|
if (targetY < mainFlickable.contentY + stickyHeight) {
|
|
mainFlickable.contentY = Math.max(0, targetY - 32);
|
|
} else if (targetBottom > mainFlickable.contentY + mainFlickable.height - shadowPadding) {
|
|
mainFlickable.contentY = Math.min(mainFlickable.contentHeight - mainFlickable.height, targetBottom - mainFlickable.height + shadowPadding);
|
|
}
|
|
}
|
|
|
|
function getSectionHeight(section) {
|
|
var mode = controller?.getSectionViewMode(section.id) ?? "list";
|
|
if (section.collapsed)
|
|
return 32;
|
|
|
|
if (mode === "list") {
|
|
return 32 + (section.items?.length ?? 0) * 52;
|
|
} else {
|
|
var cols = controller?.getGridColumns(section.id) ?? root.gridColumns;
|
|
var rows = Math.ceil((section.items?.length ?? 0) / cols);
|
|
var cellWidth = mode === "tile" ? Math.floor(root.width / 3) : Math.floor(root.width / cols);
|
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
|
return 32 + rows * cellHeight;
|
|
}
|
|
}
|
|
|
|
function getSelectedItemPosition() {
|
|
var fallback = mapToItem(null, width / 2, height / 2);
|
|
if (!controller?.flatModel || controller.selectedFlatIndex < 0)
|
|
return fallback;
|
|
|
|
var entry = controller.flatModel[controller.selectedFlatIndex];
|
|
if (!entry || entry.isHeader)
|
|
return fallback;
|
|
|
|
var sections = controller.sections;
|
|
var sectionIndex = -1;
|
|
for (var i = 0; i < sections.length; i++) {
|
|
if (sections[i].id === entry.sectionId) {
|
|
sectionIndex = i;
|
|
break;
|
|
}
|
|
}
|
|
if (sectionIndex < 0)
|
|
return fallback;
|
|
|
|
var sectionY = 0;
|
|
for (var i = 0; i < sectionIndex; i++) {
|
|
sectionY += getSectionHeight(sections[i]);
|
|
}
|
|
|
|
var mode = controller.getSectionViewMode(entry.sectionId);
|
|
var itemInSection = entry.indexInSection || 0;
|
|
|
|
var itemY, itemX, itemH;
|
|
if (mode === "list") {
|
|
itemY = sectionY + 32 + itemInSection * 52;
|
|
itemX = width / 2;
|
|
itemH = 52;
|
|
} else {
|
|
var cols = controller.getGridColumns(entry.sectionId);
|
|
var cellWidth = mode === "tile" ? Math.floor(width / 3) : Math.floor(width / cols);
|
|
var cellHeight = mode === "tile" ? cellWidth * 0.75 : cellWidth + 24;
|
|
var row = Math.floor(itemInSection / cols);
|
|
var col = itemInSection % cols;
|
|
itemY = sectionY + 32 + row * cellHeight;
|
|
itemX = col * cellWidth + cellWidth / 2;
|
|
itemH = cellHeight;
|
|
}
|
|
|
|
var visualY = itemY - mainFlickable.contentY + itemH / 2;
|
|
var clampedY = Math.max(40, Math.min(height - 40, visualY));
|
|
return mapToItem(null, itemX, clampedY);
|
|
}
|
|
|
|
Connections {
|
|
target: root.controller
|
|
function onSelectedFlatIndexChanged() {
|
|
if (root.controller?.keyboardNavigationActive) {
|
|
Qt.callLater(() => root.ensureVisible(root.controller.selectedFlatIndex));
|
|
}
|
|
}
|
|
}
|
|
|
|
DankFlickable {
|
|
id: mainFlickable
|
|
anchors.fill: parent
|
|
contentWidth: width
|
|
contentHeight: sectionsColumn.height
|
|
clip: true
|
|
|
|
Column {
|
|
id: sectionsColumn
|
|
width: parent.width
|
|
|
|
Repeater {
|
|
model: root.controller?.sections ?? []
|
|
|
|
Column {
|
|
id: sectionDelegate
|
|
required property var modelData
|
|
required property int index
|
|
|
|
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
|
readonly property string sectionId: modelData?.id ?? ""
|
|
readonly property string currentViewMode: {
|
|
void (versionTrigger);
|
|
return root.controller?.getSectionViewMode(sectionId) ?? "list";
|
|
}
|
|
readonly property bool isGridMode: currentViewMode === "grid" || currentViewMode === "tile"
|
|
readonly property bool isCollapsed: modelData?.collapsed ?? false
|
|
|
|
width: sectionsColumn.width
|
|
|
|
SectionHeader {
|
|
width: parent.width
|
|
height: 32
|
|
section: sectionDelegate.modelData
|
|
controller: root.controller
|
|
viewMode: sectionDelegate.currentViewMode
|
|
canChangeViewMode: root.controller?.canChangeSectionViewMode(sectionDelegate.sectionId) ?? false
|
|
canCollapse: root.controller?.canCollapseSection(sectionDelegate.sectionId) ?? false
|
|
}
|
|
|
|
Column {
|
|
id: listContent
|
|
width: parent.width
|
|
visible: !sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
|
|
|
Repeater {
|
|
model: sectionDelegate.isGridMode || sectionDelegate.isCollapsed ? [] : (sectionDelegate.modelData?.items ?? [])
|
|
|
|
ResultItem {
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: listContent.width
|
|
height: 52
|
|
item: modelData
|
|
isSelected: getFlatIndex() === root.controller?.selectedFlatIndex
|
|
controller: root.controller
|
|
flatIndex: getFlatIndex()
|
|
|
|
function getFlatIndex() {
|
|
if (!sectionDelegate?.sectionId)
|
|
return -1;
|
|
var flatIdx = 0;
|
|
var sections = root.controller?.sections ?? [];
|
|
for (var i = 0; i < sections.length; i++) {
|
|
flatIdx++;
|
|
if (sections[i].id === sectionDelegate.sectionId)
|
|
return flatIdx + index;
|
|
if (!sections[i].collapsed)
|
|
flatIdx += sections[i].items?.length ?? 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
onClicked: {
|
|
if (root.controller) {
|
|
root.controller.executeItem(modelData);
|
|
}
|
|
}
|
|
|
|
onRightClicked: (mouseX, mouseY) => {
|
|
root.itemRightClicked(getFlatIndex(), modelData, mouseX, mouseY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Grid {
|
|
id: gridContent
|
|
width: parent.width
|
|
visible: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed
|
|
columns: sectionDelegate.currentViewMode === "tile" ? 3 : root.gridColumns
|
|
|
|
readonly property real cellWidth: sectionDelegate.currentViewMode === "tile" ? Math.floor(width / 3) : Math.floor(width / root.gridColumns)
|
|
readonly property real cellHeight: sectionDelegate.currentViewMode === "tile" ? cellWidth * 0.75 : cellWidth + 24
|
|
|
|
Repeater {
|
|
model: sectionDelegate.isGridMode && !sectionDelegate.isCollapsed ? (sectionDelegate.modelData?.items ?? []) : []
|
|
|
|
Item {
|
|
id: gridDelegateItem
|
|
required property var modelData
|
|
required property int index
|
|
|
|
width: gridContent.cellWidth
|
|
height: gridContent.cellHeight
|
|
|
|
function getFlatIndex() {
|
|
if (!sectionDelegate?.sectionId)
|
|
return -1;
|
|
var flatIdx = 0;
|
|
var sections = root.controller?.sections ?? [];
|
|
for (var i = 0; i < sections.length; i++) {
|
|
flatIdx++;
|
|
if (sections[i].id === sectionDelegate.sectionId)
|
|
return flatIdx + index;
|
|
if (!sections[i].collapsed)
|
|
flatIdx += sections[i].items?.length ?? 0;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
readonly property int cachedFlatIndex: getFlatIndex()
|
|
|
|
GridItem {
|
|
width: parent.width - 4
|
|
height: parent.height - 4
|
|
anchors.centerIn: parent
|
|
visible: sectionDelegate.currentViewMode === "grid"
|
|
item: gridDelegateItem.modelData
|
|
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
|
controller: root.controller
|
|
flatIndex: gridDelegateItem.cachedFlatIndex
|
|
|
|
onClicked: {
|
|
if (root.controller) {
|
|
root.controller.executeItem(gridDelegateItem.modelData);
|
|
}
|
|
}
|
|
|
|
onRightClicked: (mouseX, mouseY) => {
|
|
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
|
}
|
|
}
|
|
|
|
TileItem {
|
|
width: parent.width - 4
|
|
height: parent.height - 4
|
|
anchors.centerIn: parent
|
|
visible: sectionDelegate.currentViewMode === "tile"
|
|
item: gridDelegateItem.modelData
|
|
isSelected: gridDelegateItem.cachedFlatIndex === root.controller?.selectedFlatIndex
|
|
controller: root.controller
|
|
flatIndex: gridDelegateItem.cachedFlatIndex
|
|
|
|
onClicked: {
|
|
if (root.controller) {
|
|
root.controller.executeItem(gridDelegateItem.modelData);
|
|
}
|
|
}
|
|
|
|
onRightClicked: (mouseX, mouseY) => {
|
|
root.itemRightClicked(gridDelegateItem.cachedFlatIndex, gridDelegateItem.modelData, mouseX, mouseY);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: bottomShadow
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.bottom: parent.bottom
|
|
height: 24
|
|
z: 100
|
|
visible: {
|
|
if (mainFlickable.contentHeight <= mainFlickable.height)
|
|
return false;
|
|
var atBottom = mainFlickable.contentY >= mainFlickable.contentHeight - mainFlickable.height - 5;
|
|
if (atBottom)
|
|
return false;
|
|
|
|
var flatModel = root.controller?.flatModel;
|
|
if (!flatModel || flatModel.length === 0)
|
|
return false;
|
|
var lastItemIdx = -1;
|
|
for (var i = flatModel.length - 1; i >= 0; i--) {
|
|
if (!flatModel[i].isHeader) {
|
|
lastItemIdx = i;
|
|
break;
|
|
}
|
|
}
|
|
if (lastItemIdx >= 0 && root.controller?.selectedFlatIndex === lastItemIdx)
|
|
return false;
|
|
return true;
|
|
}
|
|
gradient: Gradient {
|
|
GradientStop {
|
|
position: 0.0
|
|
color: "transparent"
|
|
}
|
|
GradientStop {
|
|
position: 1.0
|
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
}
|
|
}
|
|
}
|
|
|
|
Rectangle {
|
|
id: stickyHeader
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
height: 32
|
|
z: 101
|
|
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
|
visible: stickyHeaderSection !== null
|
|
|
|
readonly property int versionTrigger: root.controller?.viewModeVersion ?? 0
|
|
|
|
readonly property var stickyHeaderSection: {
|
|
if (!root.controller?.sections || root.controller.sections.length === 0)
|
|
return null;
|
|
var sections = root.controller.sections;
|
|
if (sections.length === 0)
|
|
return null;
|
|
|
|
var scrollY = mainFlickable.contentY;
|
|
if (scrollY <= 0)
|
|
return null;
|
|
|
|
var y = 0;
|
|
for (var i = 0; i < sections.length; i++) {
|
|
var section = sections[i];
|
|
var sectionHeight = root.getSectionHeight(section);
|
|
if (scrollY < y + sectionHeight)
|
|
return section;
|
|
y += sectionHeight;
|
|
}
|
|
return sections[sections.length - 1];
|
|
}
|
|
|
|
SectionHeader {
|
|
width: parent.width
|
|
section: stickyHeader.stickyHeaderSection
|
|
controller: root.controller
|
|
viewMode: {
|
|
void (stickyHeader.versionTrigger);
|
|
return root.controller?.getSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? "list";
|
|
}
|
|
canChangeViewMode: {
|
|
void (stickyHeader.versionTrigger);
|
|
return root.controller?.canChangeSectionViewMode(stickyHeader.stickyHeaderSection?.id) ?? false;
|
|
}
|
|
canCollapse: {
|
|
void (stickyHeader.versionTrigger);
|
|
return root.controller?.canCollapseSection(stickyHeader.stickyHeaderSection?.id) ?? false;
|
|
}
|
|
isSticky: true
|
|
}
|
|
}
|
|
|
|
Item {
|
|
anchors.centerIn: parent
|
|
visible: (!root.controller?.sections || root.controller.sections.length === 0) && !root.controller?.isFileSearching
|
|
width: emptyColumn.implicitWidth
|
|
height: emptyColumn.implicitHeight
|
|
|
|
Column {
|
|
id: emptyColumn
|
|
spacing: Theme.spacingM
|
|
|
|
DankIcon {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
name: getEmptyIcon()
|
|
size: 48
|
|
color: Theme.outlineButton
|
|
|
|
function getEmptyIcon() {
|
|
var mode = root.controller?.searchMode ?? "all";
|
|
switch (mode) {
|
|
case "files":
|
|
return "folder_open";
|
|
case "plugins":
|
|
return "extension";
|
|
case "apps":
|
|
return "apps";
|
|
default:
|
|
return root.controller?.searchQuery?.length > 0 ? "search_off" : "search";
|
|
}
|
|
}
|
|
}
|
|
|
|
StyledText {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
text: getEmptyText()
|
|
font.pixelSize: Theme.fontSizeMedium
|
|
color: Theme.surfaceVariantText
|
|
horizontalAlignment: Text.AlignHCenter
|
|
|
|
function getEmptyText() {
|
|
var mode = root.controller?.searchMode ?? "all";
|
|
var hasQuery = root.controller?.searchQuery?.length > 0;
|
|
|
|
switch (mode) {
|
|
case "files":
|
|
if (!DSearchService.dsearchAvailable)
|
|
return I18n.tr("File search requires dsearch\nInstall from github.com/morelazers/dsearch");
|
|
if (!hasQuery)
|
|
return I18n.tr("Type to search files");
|
|
if (root.controller.searchQuery.length < 2)
|
|
return I18n.tr("Type at least 2 characters");
|
|
return I18n.tr("No files found");
|
|
case "plugins":
|
|
return hasQuery ? I18n.tr("No plugin results") : I18n.tr("Browse or search plugins");
|
|
case "apps":
|
|
return hasQuery ? I18n.tr("No apps found") : I18n.tr("Type to search apps");
|
|
default:
|
|
return hasQuery ? I18n.tr("No results found") : I18n.tr("Type to search");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|