new builds menu and modloaders implementation

This commit is contained in:
2026-08-27 09:21:51 +03:00
parent 5bc59f8018
commit 168884127b
23 changed files with 3979 additions and 1081 deletions
+250
View File
@@ -0,0 +1,250 @@
import QtQuick
import QtQuick.Controls 2.15
// Выпадающий список версий: сверху установленные, ниже через разделитель
// весь каталог Mojang, сверху поле поиска.
//
// Это Item с Popup, а не ComboBox: editable-ComboBox привязывает поле ввода
// к currentText и запускает своё автодополнение, а при активном фильтре
// currentIndex перестаёт указывать в исходную модель.
Item {
id: vcombo
property var catalog: []
property string selectedId: ""
property string placeholder: qsTr("Выберите версию")
property string filterText: ""
signal aboutToOpen()
implicitHeight: 36
height: implicitHeight
// Фильтр по номеру версии. ~900 строк за нажатие — доли миллисекунды,
// а делегаты ListView создаёт только для видимых строк.
function filterEntries(needle) {
const query = needle.trim().toLowerCase()
if (query === "")
return vcombo.catalog
const out = []
for (var i = 0; i < vcombo.catalog.length; ++i) {
const entry = vcombo.catalog[i]
if (entry.selectable === false)
continue
if (entry.search.indexOf(query) !== -1)
out.push(entry)
}
return out
}
readonly property var visibleEntries: vcombo.filterEntries(vcombo.filterText)
function openPopup() {
vcombo.aboutToOpen()
vcombo.filterText = ""
vcomboFilter.text = ""
vcomboPopup.open()
vcomboFilter.forceActiveFocus()
}
Rectangle {
id: vcomboField
anchors.fill: parent
color: "#2a2a2a"
radius: 6
border.color: vcomboPopup.opened ? "#91B315" : "#444444"
border.width: 1
Text {
anchors.fill: parent
leftPadding: 10
rightPadding: 32
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
color: vcombo.selectedId === "" ? "#666666" : "#ffffff"
text: vcombo.selectedId === "" ? vcombo.placeholder : vcombo.selectedId
}
// Та же стрелка, что у остальных списков окна.
Image {
width: 10; height: 10
x: vcomboField.width - width - 12
y: (vcomboField.height - height) / 2
source: vcomboPopup.opened ? "images/Profile_Box/Asset_23.svg"
: "images/Profile_Box/Asset_24.svg"
rotation: 180
sourceSize.width: 10; sourceSize.height: 10
fillMode: Image.PreserveAspectFit
}
MouseArea {
anchors.fill: parent
onClicked: vcombo.openPopup()
}
}
Popup {
id: vcomboPopup
y: vcombo.height + 2
width: vcombo.width
padding: 1
height: 44 + Math.min(vcomboList.contentHeight, 240)
background: Rectangle {
color: "#2a2a2a"
radius: 6
border.color: "#444444"
border.width: 1
}
contentItem: Item {
TextField {
id: vcomboFilter
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 4
height: 30
placeholderText: qsTr("Поиск версии…")
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#232323"
radius: 5
border.color: vcomboFilter.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
onTextChanged: {
vcombo.filterText = text
vcomboList.currentIndex = vcomboList.firstSelectable(0, 1)
}
Keys.onEscapePressed: vcomboPopup.close()
Keys.onDownPressed: vcomboList.step(1)
Keys.onUpPressed: vcomboList.step(-1)
Keys.onReturnPressed: vcomboList.acceptCurrent()
Keys.onEnterPressed: vcomboList.acceptCurrent()
}
ListView {
id: vcomboList
anchors.top: vcomboFilter.bottom
anchors.topMargin: 4
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
clip: true
model: vcombo.visibleEntries
ScrollIndicator.vertical: ScrollIndicator {}
// Разделитель и служебные строки клавишами пропускаем.
function firstSelectable(from, delta) {
for (var i = from; i >= 0 && i < count; i += delta) {
if (model[i].selectable !== false)
return i
}
return -1
}
function step(delta) {
const next = firstSelectable(currentIndex + delta, delta)
if (next >= 0) {
currentIndex = next
positionViewAtIndex(next, ListView.Contain)
}
}
function acceptCurrent() {
if (currentIndex < 0 || currentIndex >= count)
return
const entry = model[currentIndex]
if (entry.selectable === false)
return
vcombo.selectedId = entry.id
vcomboPopup.close()
}
delegate: Item {
id: vcomboRow
required property var modelData
required property int index
width: vcomboList.width
height: modelData.kind === "separator" ? 26
: modelData.kind === "status" ? 30 : 34
// Разделитель между установленными и каталогом.
Item {
anchors.fill: parent
visible: vcomboRow.modelData.kind === "separator"
Rectangle {
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
height: 1
color: "#3a3a3a"
}
Rectangle {
anchors.centerIn: parent
color: "#2a2a2a"
width: separatorLabel.implicitWidth + 12
height: 18
Text {
id: separatorLabel
anchors.centerIn: parent
text: vcomboRow.modelData.label
color: "#888888"
font.pixelSize: 10
}
}
}
// «Загрузка списка…» или сообщение о недоступности.
Text {
anchors.centerIn: parent
visible: vcomboRow.modelData.kind === "status"
text: vcomboRow.modelData.label
color: "#888888"
font.pixelSize: 11
font.italic: true
}
Rectangle {
anchors.fill: parent
visible: vcomboRow.modelData.selectable !== false
color: (vcomboRowArea.containsMouse || vcomboList.currentIndex === vcomboRow.index)
? "#3a3a3a" : "#2a2a2a"
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 100
elide: Text.ElideRight
text: vcomboRow.modelData.label
color: "#ffffff"
font.pixelSize: 12
}
Text {
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: vcomboRow.modelData.installed ? qsTr("установлена")
: vcomboRow.modelData.type
color: vcomboRow.modelData.installed ? "#91B315" : "#666666"
font.pixelSize: 10
}
MouseArea {
id: vcomboRowArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
vcombo.selectedId = vcomboRow.modelData.id
vcomboPopup.close()
}
}
}
}
}
}
}
}