diff --git a/res/ui/icons/download.png b/res/ui/icons/download.png new file mode 100644 index 00000000..bb28bf81 Binary files /dev/null and b/res/ui/icons/download.png differ diff --git a/src/css/states/main_menu.scss b/src/css/states/main_menu.scss index a67cef3a..458bda6f 100644 --- a/src/css/states/main_menu.scss +++ b/src/css/states/main_menu.scss @@ -85,21 +85,22 @@ .savegames { @include S(max-height, 92px); overflow-y: auto; - @include S(width, 200px); + @include S(width, 250px); pointer-events: all; @include S(padding-right, 5px); display: grid; grid-auto-flow: row; @include S(grid-gap, 5px); @include S(margin-top, 10px); + .savegame { background: #eee; @include BorderRadius(4px); @include S(padding, 5px); display: grid; - grid-template-columns: 1fr auto; + grid-template-columns: 1fr auto auto; grid-template-rows: auto auto; - @include S(grid-column-gap, 15px); + @include S(grid-column-gap, 5px); .internalId { grid-column: 1 / 2; @@ -114,8 +115,9 @@ @include PlainText; } - button.resumeGame { - grid-column: 2 / 3; + button.resumeGame, + button.downloadGame { + grid-column: 3 / 4; grid-row: 1 / 3; @include S(width, 30px); @include S(height, 30px); @@ -123,6 +125,15 @@ align-self: center; background: #44484a uiResource("icons/play.png") center center / 40% no-repeat; } + + button.downloadGame { + grid-column: 2 / 3; + background-image: uiResource("icons/download.png"); + @include S(width, 15px); + @include S(height, 15px); + align-self: end; + background-size: 60%; + } } } } diff --git a/src/js/core/read_write_proxy.js b/src/js/core/read_write_proxy.js index a735f9ad..b0f16704 100644 --- a/src/js/core/read_write_proxy.js +++ b/src/js/core/read_write_proxy.js @@ -79,6 +79,16 @@ export class ReadWriteProxy { return this.currentData; } + /** + * + * @param {object} obj + */ + static serializeObject(obj) { + const jsonString = JSON_stringify(compressObject(obj)); + const checksum = sha1(jsonString + salt); + return compressionPrefix + compressX64(checksum + jsonString); + } + /** * Writes the data asychronously, fails if verify() fails * @returns {Promise} diff --git a/src/js/core/utils.js b/src/js/core/utils.js index 08011a45..e6736ed1 100644 --- a/src/js/core/utils.js +++ b/src/js/core/utils.js @@ -859,3 +859,20 @@ export function formatSecondsToTimeAgo(secs) { return days + " days ago"; } } + +/** + * Generates a file download + * @param {string} filename + * @param {string} text + */ +export function generateFileDownload(filename, text) { + var element = document.createElement("a"); + element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text)); + element.setAttribute("download", filename); + + element.style.display = "none"; + document.body.appendChild(element); + + element.click(); + document.body.removeChild(element); +} diff --git a/src/js/states/main_menu.js b/src/js/states/main_menu.js index 4cf3ad79..8dfc8c83 100644 --- a/src/js/states/main_menu.js +++ b/src/js/states/main_menu.js @@ -1,7 +1,8 @@ import { GameState } from "../core/game_state"; import { cachebust } from "../core/cachebust"; import { globalConfig } from "../core/config"; -import { makeDiv, formatSecondsToTimeAgo } from "../core/utils"; +import { makeDiv, formatSecondsToTimeAgo, generateFileDownload } from "../core/utils"; +import { ReadWriteProxy } from "../core/read_write_proxy"; export class MainMenuState extends GameState { constructor() { @@ -90,10 +91,15 @@ export class MainMenuState extends GameState { formatSecondsToTimeAgo((new Date().getTime() - games[i].lastUpdate) / 1000.0) ); + const downloadButton = document.createElement("button"); + downloadButton.classList.add("styledButton", "downloadGame"); + elem.appendChild(downloadButton); + const resumeBtn = document.createElement("button"); resumeBtn.classList.add("styledButton", "resumeGame"); elem.appendChild(resumeBtn); + this.trackClicks(downloadButton, () => this.downloadGame(games[i])); this.trackClicks(resumeBtn, () => this.resumeGame(games[i])); } } @@ -111,6 +117,17 @@ export class MainMenuState extends GameState { }); } + /** + * @param {object} game + */ + downloadGame(game) { + const savegame = this.app.savegameMgr.getSavegameById(game.internalId); + savegame.readAsync().then(() => { + const data = ReadWriteProxy.serializeObject(savegame.currentData); + generateFileDownload(savegame.filename, data); + }); + } + onPlayButtonClicked() { const savegame = this.app.savegameMgr.createNewSavegame();