LoadGamePanel.qml 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. import QtQml 2.15
  2. import QtQuick 2.15
  3. import QtQuick.Controls 2.15
  4. import QtQuick.Layouts 1.3
  5. import StandardOfIron 1.0
  6. Item {
  7. id: root
  8. signal cancelled()
  9. signal loadRequested(string slotName)
  10. anchors.fill: parent
  11. z: 25
  12. onVisibleChanged: {
  13. if (!visible)
  14. return ;
  15. if (typeof loadListModel !== 'undefined')
  16. loadListModel.loadFromGame();
  17. if (typeof loadListView !== 'undefined')
  18. loadListView.selectedIndex = loadListModel.count > 0 && !loadListModel.get(0).isEmpty ? 0 : -1;
  19. }
  20. Keys.onPressed: function(event) {
  21. if (event.key === Qt.Key_Escape) {
  22. root.cancelled();
  23. event.accepted = true;
  24. } else if (event.key === Qt.Key_Down) {
  25. if (loadListView.selectedIndex < loadListModel.count - 1)
  26. loadListView.selectedIndex++;
  27. event.accepted = true;
  28. } else if (event.key === Qt.Key_Up) {
  29. if (loadListView.selectedIndex > 0)
  30. loadListView.selectedIndex--;
  31. event.accepted = true;
  32. } else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
  33. if (loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty)
  34. root.loadRequested(loadListModel.get(loadListView.selectedIndex).slotName);
  35. event.accepted = true;
  36. }
  37. }
  38. Component.onCompleted: {
  39. forceActiveFocus();
  40. if (loadListModel.count > 0 && !loadListModel.get(0).isEmpty)
  41. loadListView.selectedIndex = 0;
  42. }
  43. Connections {
  44. function onSaveSlotsChanged() {
  45. if (typeof loadListModel === 'undefined')
  46. return ;
  47. var previousSlot = "";
  48. if (typeof loadListView !== 'undefined' && loadListView.selectedIndex >= 0 && loadListView.selectedIndex < loadListModel.count) {
  49. var current = loadListModel.get(loadListView.selectedIndex);
  50. if (current && !current.isEmpty)
  51. previousSlot = current.slotName;
  52. }
  53. loadListModel.loadFromGame();
  54. if (typeof loadListView === 'undefined')
  55. return ;
  56. var newIndex = -1;
  57. if (previousSlot !== "") {
  58. for (var i = 0; i < loadListModel.count; ++i) {
  59. var slot = loadListModel.get(i);
  60. if (!slot.isEmpty && slot.slotName === previousSlot) {
  61. newIndex = i;
  62. break;
  63. }
  64. }
  65. }
  66. if (newIndex === -1) {
  67. if (loadListModel.count > 0 && !loadListModel.get(0).isEmpty)
  68. newIndex = 0;
  69. }
  70. loadListView.selectedIndex = newIndex;
  71. }
  72. target: typeof game !== 'undefined' ? game : null
  73. }
  74. Rectangle {
  75. anchors.fill: parent
  76. color: Theme.dim
  77. }
  78. Rectangle {
  79. id: container
  80. width: Math.min(parent.width * 0.7, 900)
  81. height: Math.min(parent.height * 0.8, 600)
  82. anchors.centerIn: parent
  83. radius: Theme.radiusPanel
  84. color: Theme.panelBase
  85. border.color: Theme.panelBr
  86. border.width: 1
  87. opacity: 0.98
  88. ColumnLayout {
  89. anchors.fill: parent
  90. anchors.margins: Theme.spacingXLarge
  91. spacing: Theme.spacingLarge
  92. RowLayout {
  93. Layout.fillWidth: true
  94. spacing: Theme.spacingMedium
  95. Label {
  96. text: qsTr("Load Game")
  97. color: Theme.textMain
  98. font.pointSize: Theme.fontSizeHero
  99. font.bold: true
  100. Layout.fillWidth: true
  101. }
  102. Button {
  103. text: qsTr("Cancel")
  104. onClicked: root.cancelled()
  105. }
  106. }
  107. Rectangle {
  108. Layout.fillWidth: true
  109. Layout.preferredHeight: 1
  110. color: Theme.border
  111. }
  112. Rectangle {
  113. Layout.fillWidth: true
  114. Layout.fillHeight: true
  115. color: Theme.cardBase
  116. border.color: Theme.border
  117. border.width: 1
  118. radius: Theme.radiusLarge
  119. ScrollView {
  120. anchors.fill: parent
  121. anchors.margins: Theme.spacingSmall
  122. clip: true
  123. ListView {
  124. id: loadListView
  125. property int selectedIndex: -1
  126. spacing: Theme.spacingSmall
  127. model: ListModel {
  128. id: loadListModel
  129. function loadFromGame() {
  130. clear();
  131. if (typeof game === 'undefined' || !game.get_save_slots) {
  132. append({
  133. "slotName": qsTr("No saves found"),
  134. "title": "",
  135. "timestamp": 0,
  136. "map_name": "",
  137. "playTime": "",
  138. "thumbnail": "",
  139. "isEmpty": true
  140. });
  141. return ;
  142. }
  143. var slots = game.get_save_slots();
  144. for (var i = 0; i < slots.length; i++) {
  145. append({
  146. "slotName": slots[i].slotName || slots[i].name,
  147. "title": slots[i].title || slots[i].name || slots[i].slotName || "Untitled Save",
  148. "timestamp": slots[i].timestamp,
  149. "map_name": slots[i].map_name || "Unknown Map",
  150. "playTime": slots[i].playTime || "",
  151. "thumbnail": slots[i].thumbnail || "",
  152. "isEmpty": false
  153. });
  154. }
  155. if (count === 0)
  156. append({
  157. "slotName": qsTr("No saves found"),
  158. "title": "",
  159. "timestamp": 0,
  160. "map_name": "",
  161. "playTime": "",
  162. "thumbnail": "",
  163. "isEmpty": true
  164. });
  165. }
  166. Component.onCompleted: {
  167. loadFromGame();
  168. }
  169. }
  170. delegate: Rectangle {
  171. width: loadListView.width
  172. height: model.isEmpty ? 100 : 130
  173. color: loadListView.selectedIndex === index ? Theme.selectedBg : mouseArea.containsMouse ? Theme.hoverBg : Qt.rgba(0, 0, 0, 0)
  174. radius: Theme.radiusMedium
  175. border.color: loadListView.selectedIndex === index ? Theme.selectedBr : Theme.cardBorder
  176. border.width: 1
  177. visible: !model.isEmpty || loadListModel.count === 1
  178. RowLayout {
  179. anchors.fill: parent
  180. anchors.margins: Theme.spacingMedium
  181. spacing: Theme.spacingMedium
  182. Rectangle {
  183. id: loadThumbnail
  184. Layout.preferredWidth: 128
  185. Layout.preferredHeight: 80
  186. radius: Theme.radiusSmall
  187. color: Theme.cardBase
  188. border.color: Theme.cardBorder
  189. border.width: 1
  190. clip: true
  191. visible: !model.isEmpty
  192. Image {
  193. id: loadThumbnailImage
  194. anchors.fill: parent
  195. anchors.margins: 2
  196. fillMode: Image.PreserveAspectCrop
  197. source: model.thumbnail && model.thumbnail.length > 0 ? "data:image/png;base64," + model.thumbnail : ""
  198. visible: source !== ""
  199. }
  200. Label {
  201. anchors.centerIn: parent
  202. visible: !loadThumbnailImage.visible
  203. text: qsTr("No Preview")
  204. color: Theme.textHint
  205. font.pointSize: Theme.fontSizeTiny
  206. }
  207. }
  208. ColumnLayout {
  209. Layout.fillWidth: true
  210. spacing: Theme.spacingTiny
  211. visible: !model.isEmpty
  212. Label {
  213. text: model.title
  214. color: Theme.textMain
  215. font.pointSize: Theme.fontSizeLarge
  216. font.bold: true
  217. Layout.fillWidth: true
  218. elide: Label.ElideRight
  219. }
  220. Label {
  221. text: qsTr("Slot: %1").arg(model.slotName)
  222. color: Theme.textSub
  223. font.pointSize: Theme.fontSizeSmall
  224. Layout.fillWidth: true
  225. elide: Label.ElideRight
  226. }
  227. Label {
  228. text: model.map_name
  229. color: Theme.textSub
  230. font.pointSize: Theme.fontSizeMedium
  231. Layout.fillWidth: true
  232. elide: Label.ElideRight
  233. }
  234. RowLayout {
  235. Layout.fillWidth: true
  236. spacing: Theme.spacingLarge
  237. Label {
  238. text: qsTr("Last saved: %1").arg(Qt.formatDateTime(new Date(model.timestamp), "yyyy-MM-dd hh:mm:ss"))
  239. color: Theme.textHint
  240. font.pointSize: Theme.fontSizeSmall
  241. Layout.fillWidth: true
  242. elide: Label.ElideRight
  243. }
  244. Label {
  245. text: model.playTime !== "" ? qsTr("Play time: %1").arg(model.playTime) : ""
  246. color: Theme.textHint
  247. font.pointSize: Theme.fontSizeSmall
  248. visible: model.playTime !== ""
  249. }
  250. }
  251. }
  252. Label {
  253. text: model.slotName
  254. color: Theme.textDim
  255. font.pointSize: Theme.fontSizeLarge
  256. Layout.fillWidth: true
  257. horizontalAlignment: Text.AlignHCenter
  258. visible: model.isEmpty
  259. }
  260. Button {
  261. text: qsTr("Load")
  262. highlighted: true
  263. visible: !model.isEmpty
  264. onClicked: {
  265. root.loadRequested(model.slotName);
  266. }
  267. }
  268. Button {
  269. text: qsTr("Delete")
  270. visible: !model.isEmpty
  271. onClicked: {
  272. confirmDeleteDialog.slotName = model.slotName;
  273. confirmDeleteDialog.slotIndex = index;
  274. confirmDeleteDialog.open();
  275. }
  276. }
  277. }
  278. MouseArea {
  279. id: mouseArea
  280. anchors.fill: parent
  281. hoverEnabled: true
  282. enabled: !model.isEmpty
  283. onClicked: {
  284. loadListView.selectedIndex = index;
  285. }
  286. onDoubleClicked: {
  287. if (!model.isEmpty)
  288. root.loadRequested(model.slotName);
  289. }
  290. }
  291. }
  292. }
  293. }
  294. }
  295. RowLayout {
  296. Layout.fillWidth: true
  297. spacing: Theme.spacingMedium
  298. Item {
  299. Layout.fillWidth: true
  300. }
  301. Label {
  302. text: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty ? qsTr("Selected: %1").arg(loadListModel.get(loadListView.selectedIndex).title) : qsTr("Select a save to load")
  303. color: Theme.textSub
  304. font.pointSize: Theme.fontSizeMedium
  305. }
  306. Button {
  307. text: qsTr("Load Selected")
  308. enabled: loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty
  309. highlighted: true
  310. onClicked: {
  311. if (loadListView.selectedIndex >= 0 && !loadListModel.get(loadListView.selectedIndex).isEmpty)
  312. root.loadRequested(loadListModel.get(loadListView.selectedIndex).slotName);
  313. }
  314. }
  315. }
  316. }
  317. }
  318. Dialog {
  319. id: confirmDeleteDialog
  320. property string slotName: ""
  321. property int slotIndex: -1
  322. anchors.centerIn: parent
  323. width: Math.min(parent.width * 0.5, 400)
  324. title: qsTr("Confirm Delete")
  325. modal: true
  326. standardButtons: Dialog.Yes | Dialog.No
  327. onAccepted: {
  328. if (typeof game !== 'undefined' && game.delete_save_slot) {
  329. if (game.delete_save_slot(slotName)) {
  330. loadListModel.remove(slotIndex);
  331. if (loadListModel.count === 0)
  332. loadListModel.append({
  333. "slotName": qsTr("No saves found"),
  334. "title": "",
  335. "timestamp": 0,
  336. "map_name": "",
  337. "playTime": "",
  338. "thumbnail": "",
  339. "isEmpty": true
  340. });
  341. if (loadListView.selectedIndex >= loadListModel.count)
  342. loadListView.selectedIndex = loadListModel.count > 0 && !loadListModel.get(0).isEmpty ? loadListModel.count - 1 : -1;
  343. }
  344. }
  345. }
  346. contentItem: Rectangle {
  347. color: Theme.cardBase
  348. implicitHeight: warningText.implicitHeight + 40
  349. ColumnLayout {
  350. anchors.fill: parent
  351. anchors.margins: Theme.spacingMedium
  352. spacing: Theme.spacingMedium
  353. Label {
  354. id: warningText
  355. text: qsTr("Are you sure you want to delete the save:\n\"%1\"?\n\nThis action cannot be undone.").arg(confirmDeleteDialog.slotName)
  356. color: Theme.textMain
  357. wrapMode: Text.WordWrap
  358. Layout.fillWidth: true
  359. font.pointSize: Theme.fontSizeMedium
  360. }
  361. }
  362. }
  363. }
  364. }