Files

617 lines
22 KiB
QML
Raw Permalink Normal View History

2026-08-27 16:44:42 +03:00
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls 2.15
import QtQuick.Layouts 2.15
// Выбор версии Minecraft отдельным окном: слева категории, справа сами версии
// с поиском сверху.
//
// Раньше это был выпадающий список на всю тысячу версий: найти в нём бету 1.7
// можно было только поиском по точному номеру. Категории делят список на
// обозримые части, а поиск работает внутри выбранной.
Dialog {
id: picker
required property var backend
// Версия, с которой окно открылось: по «Отмене» выбор к ней и возвращается.
property string selectedId: ""
property string category: "release"
property string filterText: ""
property bool installedOnly: false
// Выбранная версия подтверждена кнопкой или двойным щелчком. Имя не
// accepted(): такой сигнал у Dialog уже есть, и переопределить его нельзя.
signal versionChosen(string versionId)
modal: true
padding: 0
width: 720
height: 480
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: "#1e1e1e"
radius: 10
border.color: "#91B315"
border.width: 1
}
readonly property var categories: [
{ key: "release", title: Loc.t.version.filter.releases },
{ key: "snapshot", title: Loc.t.version.filter.snapshots },
{ key: "old_beta", title: Loc.t.version.filter.betas },
{ key: "old_alpha", title: Loc.t.version.filter.alphas },
{ key: "other", title: Loc.t.version.filter.other }
2026-08-27 16:44:42 +03:00
]
// Каталог приходит из C++ уже отсортированным (новые сверху), поэтому здесь
// только отбор — порядок не трогаем.
readonly property var visibleEntries: {
const query = picker.filterText.trim().toLowerCase()
const source = picker.backend.versionCatalog
const out = []
for (var i = 0; i < source.length; ++i) {
const entry = source[i]
if (entry.category !== picker.category)
continue
if (picker.installedOnly && !entry.installed)
continue
if (query !== "" && entry.search.indexOf(query) === -1)
continue
out.push(entry)
}
return out
}
function openFor(versionId) {
picker.selectedId = versionId
picker.filterText = ""
searchField.text = ""
// Открываемся на категории текущей версии, а не всегда на релизах.
picker.category = picker.categoryOf(versionId)
picker.backend.refreshVersionCatalog()
picker.open()
picker.revealSelected()
}
function categoryOf(versionId) {
const source = picker.backend.versionCatalog
for (var i = 0; i < source.length; ++i) {
if (source[i].id === versionId)
return source[i].category
}
return "release"
}
function indexOfSelected() {
const list = picker.visibleEntries
for (var i = 0; i < list.length; ++i) {
if (list[i].id === picker.selectedId)
return i
}
return -1
}
function revealSelected() {
const index = picker.indexOfSelected()
versionList.currentIndex = index
if (index >= 0)
versionList.positionViewAtIndex(index, ListView.Center)
}
function acceptSelection() {
if (picker.selectedId === "")
return
picker.versionChosen(picker.selectedId)
picker.close()
}
2026-08-27 21:05:37 +03:00
function catalogHas(versionId) {
const source = picker.backend.versionCatalog
for (var i = 0; i < source.length; ++i) {
if (source[i].id === versionId)
return true
}
return false
}
// Спрашиваем до удаления: версию придётся качать заново, а профили
// модлоадеров поверх неё — переустанавливать. Id запоминаем здесь:
// к моменту ответа строка под курсором может быть уже другой.
function askRemove(versionId) {
const info = picker.backend.versionRemovalInfo(versionId)
if (!info || info.installed !== true)
return
removeConfirm.versionId = versionId
removeConfirm.sizeMb = info.sizeMb || 0
removeConfirm.dependents = info.dependents || []
removeConfirm.builds = info.builds || []
removeConfirm.open()
}
function performRemove(versionId) {
picker.backend.removeVersion(versionId)
// Профиль модлоадера был в каталоге только потому, что установлен:
// после удаления строка исчезает совсем, и выбор надо снять.
if (picker.selectedId === versionId && !picker.catalogHas(versionId))
picker.selectedId = ""
picker.revealSelected()
}
Dialog {
id: removeConfirm
property string versionId: ""
property int sizeMb: 0
property var dependents: []
property var builds: []
modal: true
padding: 0
width: 420
parent: picker.parent
x: (picker.parent.width - width) / 2
y: (picker.parent.height - height) / 2
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: "#1e1e1e"
radius: 10
border.color: "#cc6666"
border.width: 1
}
header: Item {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: Loc.t.version.title.removeConfirm
2026-08-27 21:05:37 +03:00
color: "#ffffff"
font.pixelSize: 17
font.bold: true
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: "#333333"
}
}
contentItem: Column {
spacing: 10
topPadding: 18
bottomPadding: 18
Text {
x: 20
width: parent.width - 40
wrapMode: Text.Wrap
color: "#ffffff"
font.pixelSize: 13
text: removeConfirm.sizeMb > 0
? Loc.t.version.hint.removeWithSize
2026-08-27 21:05:37 +03:00
.arg(removeConfirm.versionId).arg(removeConfirm.sizeMb)
: Loc.t.version.hint.remove
2026-08-27 21:05:37 +03:00
.arg(removeConfirm.versionId)
}
Text {
x: 20
width: parent.width - 40
visible: removeConfirm.dependents.length > 0
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: Loc.t.version.warning.loaderProfilesDepend
2026-08-27 21:05:37 +03:00
.arg(removeConfirm.dependents.join(", "))
}
Text {
x: 20
width: parent.width - 40
visible: removeConfirm.builds.length > 0
wrapMode: Text.Wrap
color: "#cc6666"
font.pixelSize: 12
text: Loc.t.version.warning.buildsDepend
2026-08-27 21:05:37 +03:00
.arg(removeConfirm.builds.join(", "))
}
Text {
x: 20
width: parent.width - 40
wrapMode: Text.Wrap
color: "#888888"
font.pixelSize: 12
text: Loc.t.version.hint.sharedFilesStay
2026-08-27 21:05:37 +03:00
}
}
footer: Item {
implicitHeight: 60
Rectangle {
anchors.top: parent.top
width: parent.width
height: 1
color: "#333333"
}
Row {
anchors.centerIn: parent
spacing: 12
Button {
text: Loc.t.common.button.cancel
2026-08-27 21:05:37 +03:00
width: 130; height: 36
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#6a8510" : "#91B315"
radius: 6
}
onClicked: removeConfirm.close()
}
Button {
text: Loc.t.common.button.remove
2026-08-27 21:05:37 +03:00
width: 130; height: 36
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#8a3a3a" : "#663333"
radius: 6
border.color: "#cc6666"
border.width: 1
}
onClicked: {
const id = removeConfirm.versionId
removeConfirm.close()
picker.performRemove(id)
}
}
}
}
onClosed: removeConfirm.versionId = ""
}
2026-08-27 16:44:42 +03:00
header: Item {
implicitHeight: 52
Text {
anchors.centerIn: parent
text: Loc.t.common.label.minecraftVersion
2026-08-27 16:44:42 +03:00
color: "#ffffff"
font.pixelSize: 17
font.bold: true
}
Rectangle {
anchors.bottom: parent.bottom
width: parent.width
height: 1
color: "#333333"
}
}
contentItem: RowLayout {
spacing: 0
// ── Категории ───────────────────────────────────────────────────
Rectangle {
Layout.preferredWidth: 150
Layout.fillHeight: true
color: "transparent"
Column {
anchors.fill: parent
anchors.margins: 8
spacing: 2
Repeater {
model: picker.categories
delegate: Rectangle {
id: categoryRow
required property var modelData
width: parent.width
height: 34
radius: 5
color: picker.category === categoryRow.modelData.key ? "#91B315"
: categoryArea.containsMouse ? "#2d2d2d" : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
text: categoryRow.modelData.title
color: "#ffffff"
font.pixelSize: 13
}
MouseArea {
id: categoryArea
anchors.fill: parent
hoverEnabled: true
onClicked: {
picker.category = categoryRow.modelData.key
picker.revealSelected()
}
}
}
}
}
}
Rectangle {
Layout.preferredWidth: 1
Layout.fillHeight: true
color: "#333333"
}
// ── Версии ──────────────────────────────────────────────────────
Item {
Layout.fillWidth: true
Layout.fillHeight: true
TextField {
id: searchField
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 8
height: 32
placeholderText: Loc.t.common.placeholder.searchVersion
2026-08-27 16:44:42 +03:00
color: "#ffffff"
placeholderTextColor: "#666666"
background: Rectangle {
color: "#232323"
radius: 5
border.color: searchField.activeFocus ? "#91B315" : "#444444"
border.width: 1
}
onTextChanged: {
picker.filterText = text
versionList.currentIndex = picker.indexOfSelected()
}
Keys.onEscapePressed: picker.close()
Keys.onReturnPressed: picker.acceptSelection()
Keys.onEnterPressed: picker.acceptSelection()
Keys.onDownPressed: versionList.step(1)
Keys.onUpPressed: versionList.step(-1)
}
CheckBox {
id: installedOnlyBox
anchors.left: parent.left
anchors.leftMargin: 8
anchors.bottom: parent.bottom
anchors.bottomMargin: 4
height: 28
checked: picker.installedOnly
onToggled: {
picker.installedOnly = checked
picker.revealSelected()
}
indicator: Rectangle {
implicitWidth: 16
implicitHeight: 16
x: 0
y: (installedOnlyBox.height - height) / 2
radius: 4
color: installedOnlyBox.checked ? "#91B315" : "#2a2a2a"
border.color: installedOnlyBox.checked ? "#91B315" : "#444444"
border.width: 1
Text {
anchors.centerIn: parent
visible: installedOnlyBox.checked
text: "✓"
color: "#1e1e1e"
font.pixelSize: 12
font.bold: true
}
}
contentItem: Text {
leftPadding: installedOnlyBox.indicator.width + 8
text: Loc.t.version.filter.installedOnly
2026-08-27 16:44:42 +03:00
color: "#aaaaaa"
font.pixelSize: 11
verticalAlignment: Text.AlignVCenter
}
}
ListView {
id: versionList
anchors.top: searchField.bottom
anchors.topMargin: 6
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: installedOnlyBox.top
anchors.leftMargin: 8
anchors.rightMargin: 8
clip: true
spacing: 1
model: picker.visibleEntries
ScrollIndicator.vertical: ScrollIndicator {}
function step(delta) {
const next = versionList.currentIndex + delta
if (next < 0 || next >= versionList.count)
return
versionList.currentIndex = next
versionList.positionViewAtIndex(next, ListView.Contain)
picker.selectedId = picker.visibleEntries[next].id
}
delegate: Rectangle {
id: versionRow
required property var modelData
required property int index
width: versionList.width
height: 32
radius: 4
color: picker.selectedId === versionRow.modelData.id ? "#91B315"
: versionArea.containsMouse ? "#2d2d2d" : "transparent"
Text {
anchors.left: parent.left
anchors.leftMargin: 10
2026-08-27 21:05:37 +03:00
anchors.right: rightMarks.left
2026-08-27 16:44:42 +03:00
anchors.rightMargin: 6
anchors.verticalCenter: parent.verticalCenter
text: versionRow.modelData.label
color: "#ffffff"
elide: Text.ElideRight
font.pixelSize: 13
}
2026-08-27 21:05:37 +03:00
Row {
id: rightMarks
2026-08-27 16:44:42 +03:00
anchors.right: parent.right
anchors.rightMargin: 10
anchors.verticalCenter: parent.verticalCenter
2026-08-27 21:05:37 +03:00
spacing: 8
Text {
visible: versionRow.modelData.installed
text: "✓"
color: picker.selectedId === versionRow.modelData.id ? "#1e1e1e" : "#91B315"
font.pixelSize: 13
font.bold: true
anchors.verticalCenter: parent.verticalCenter
}
// Версия весит от десятков мегабайт, а галочка рядом —
// единственное место, где видно, что именно скачано.
Image {
visible: versionRow.modelData.installed
source: "images/Trash.svg"
width: 14
height: 14
fillMode: Image.PreserveAspectFit
anchors.verticalCenter: parent.verticalCenter
opacity: trashArea.containsMouse ? 1.0 : 0.55
MouseArea {
id: trashArea
anchors.fill: parent
anchors.margins: -5
hoverEnabled: true
onClicked: picker.askRemove(versionRow.modelData.id)
}
}
2026-08-27 16:44:42 +03:00
}
MouseArea {
id: versionArea
anchors.fill: parent
2026-08-27 21:05:37 +03:00
anchors.rightMargin: 40
2026-08-27 16:44:42 +03:00
hoverEnabled: true
onClicked: {
picker.selectedId = versionRow.modelData.id
versionList.currentIndex = versionRow.index
}
onDoubleClicked: {
picker.selectedId = versionRow.modelData.id
picker.acceptSelection()
}
}
}
}
// Пустая категория, ничего не нашлось или манифест ещё не пришёл.
Text {
anchors.centerIn: versionList
width: versionList.width - 20
visible: picker.visibleEntries.length === 0
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
color: "#888888"
font.pixelSize: 12
font.italic: true
text: picker.backend.catalogLoading
? Loc.t.version.progress.loadingList
2026-08-27 16:44:42 +03:00
: picker.backend.versionCatalog.length === 0
? Loc.t.version.error.listUnreachable
: Loc.t.common.status.nothingFound
2026-08-27 16:44:42 +03:00
}
}
}
footer: Item {
implicitHeight: 60
Rectangle {
anchors.top: parent.top
width: parent.width
height: 1
color: "#333333"
}
Text {
anchors.left: parent.left
anchors.leftMargin: 20
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 300
elide: Text.ElideRight
color: picker.selectedId === "" ? "#666666" : "#91B315"
font.pixelSize: 13
text: picker.selectedId === "" ? Loc.t.version.status.notSelected : picker.selectedId
2026-08-27 16:44:42 +03:00
}
Row {
anchors.right: parent.right
anchors.rightMargin: 20
anchors.verticalCenter: parent.verticalCenter
spacing: 12
Button {
text: Loc.t.common.button.cancel
2026-08-27 16:44:42 +03:00
width: 110; height: 36
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#444444" : "#333333"
radius: 6
}
onClicked: picker.close()
}
Button {
text: Loc.t.common.button.select
2026-08-27 16:44:42 +03:00
width: 110; height: 36
enabled: picker.selectedId !== ""
opacity: enabled ? 1.0 : 0.45
contentItem: Text {
text: parent.text
color: "#ffffff"
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.pressed ? "#6a8510" : "#91B315"
radius: 6
}
onClicked: picker.acceptSelection()
}
}
}
onOpened: searchField.forceActiveFocus()
}