diff --git a/src/css/states/puzzle_menu.scss b/src/css/states/puzzle_menu.scss index 5f28d902..97586402 100644 --- a/src/css/states/puzzle_menu.scss +++ b/src/css/states/puzzle_menu.scss @@ -159,6 +159,25 @@ } } + > button.delete { + position: absolute; + @include S(top, 5px); + @include S(right, 5px); + background-repeat: no-repeat; + background-position: center center; + background-size: 70%; + background-color: transparent !important; + @include S(width, 20px); + @include S(height, 20px); + padding: 0; + opacity: 0.7; + + & { + /* @load-async */ + background-image: uiResource("icons/delete.png") !important; + } + } + > .stats { grid-column: 2 / 3; grid-row: 3 / 4; diff --git a/src/js/platform/api.js b/src/js/platform/api.js index ea6bcd1c..db27360d 100644 --- a/src/js/platform/api.js +++ b/src/js/platform/api.js @@ -154,6 +154,20 @@ export class ClientAPI { return this._request("/v1/puzzles/download/" + puzzleId, {}); } + /** + * @param {number} puzzleId + * @returns {Promise} + */ + apiDeletePuzzle(puzzleId) { + if (!this.isLoggedIn()) { + return Promise.reject("not-logged-in"); + } + return this._request("/v1/puzzles/delete/" + puzzleId, { + method: "POST", + body: {}, + }); + } + /** * @param {number} shortKey * @returns {Promise} diff --git a/src/js/states/puzzle_menu.js b/src/js/states/puzzle_menu.js index 22a492d5..bded8e1e 100644 --- a/src/js/states/puzzle_menu.js +++ b/src/js/states/puzzle_menu.js @@ -185,6 +185,7 @@ export class PuzzleMenuState extends TextualGameState { for (const puzzle of puzzles) { const elem = document.createElement("div"); elem.classList.add("puzzle"); + elem.setAttribute("data-puzzle-id", String(puzzle.id)); if (this.activeCategory !== "mine") { elem.classList.toggle("completed", puzzle.completed); @@ -255,6 +256,23 @@ export class PuzzleMenuState extends TextualGameState { icon.appendChild(canvas); elem.appendChild(icon); + if (this.activeCategory === "mine") { + const deleteButton = document.createElement("button"); + deleteButton.classList.add("styledButton", "delete"); + this.trackClicks( + deleteButton, + () => { + this.tryDeletePuzzle(puzzle); + }, + { + consumeEvents: true, + preventClick: true, + preventDefault: true, + } + ); + elem.appendChild(deleteButton); + } + container.appendChild(elem); this.trackClicks(elem, () => this.playPuzzle(puzzle)); @@ -268,6 +286,33 @@ export class PuzzleMenuState extends TextualGameState { } } + /** + * @param {import("../savegame/savegame_typedefs").PuzzleMetadata} puzzle + */ + tryDeletePuzzle(puzzle) { + const signals = this.dialogs.showWarning( + T.dialogs.puzzleDelete.title, + T.dialogs.puzzleDelete.desc.replace("", puzzle.title), + ["delete:bad", "cancel:good"] + ); + signals.delete.add(() => { + const closeLoading = this.dialogs.showLoadingDialog(); + + this.asyncChannel + .watch(this.app.clientApi.apiDeletePuzzle(puzzle.id)) + .then(() => { + const element = this.htmlElement.querySelector("[data-puzzle-id='" + puzzle.id + "']"); + if (element) { + element.remove(); + } + }) + .catch(err => { + this.dialogs.showWarning(T.global.error, String(err)); + }) + .then(closeLoading); + }); + } + /** * * @param {*} category diff --git a/translations/base-en.yaml b/translations/base-en.yaml index 0b094b69..8e2ae866 100644 --- a/translations/base-en.yaml +++ b/translations/base-en.yaml @@ -393,6 +393,11 @@ dialogs: desc: >- Enter the short key of the puzzle to load it. + puzzleDelete: + title: Delete Puzzle? + desc: >- + Are you sure you want to delete '<title>'? This can not be undone! + ingame: # This is shown in the top left corner and displays useful keybindings in # every situation @@ -1400,6 +1405,8 @@ backendErrors: bad-payload: The request contains invalid data. bad-building-placement: Your puzzle contains invalid placed buildings. timeout: The request timed out. + too-many-likes-already: The puzzle alreay got too many likes. If you still want to remove it, please contact support@shapez.io! + no-permission: You do not have the permission to perform this action. tips: - The hub will accept any input, not just the current shape!