From 3a03799074f04a0a248caeae8df30cb741d6113c Mon Sep 17 00:00:00 2001 From: Timo T Date: Fri, 27 May 2022 15:53:18 +0200 Subject: [PATCH] Refactor: Extract all cross-platform code from CustomSaveLocationHelpers into core module (#6962) * Also fixes the GameInfo.customSaveLocation to work for Android --- android/src/com/unciv/app/AndroidLauncher.kt | 17 ++- .../app/CustomFileLocationHelperAndroid.kt | 98 ++++++++++++++ .../app/CustomSaveLocationHelperAndroid.kt | 124 ------------------ core/src/com/unciv/UncivGame.kt | 2 +- core/src/com/unciv/UncivGameParameters.kt | 4 +- .../unciv/logic/CustomFileLocationHelper.kt | 95 ++++++++++++++ .../unciv/logic/CustomSaveLocationHelper.kt | 37 ------ core/src/com/unciv/logic/GameSaver.kt | 71 +++++++++- core/src/com/unciv/ui/saves/LoadGameScreen.kt | 13 +- core/src/com/unciv/ui/saves/SaveGameScreen.kt | 15 ++- .../CustomFileLocationHelperDesktop.kt | 60 +++++++++ .../CustomSaveLocationHelperDesktop.kt | 98 -------------- .../com/unciv/app/desktop/DesktopLauncher.kt | 2 +- 13 files changed, 346 insertions(+), 290 deletions(-) create mode 100644 android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt delete mode 100644 android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt create mode 100644 core/src/com/unciv/logic/CustomFileLocationHelper.kt delete mode 100644 core/src/com/unciv/logic/CustomSaveLocationHelper.kt create mode 100644 desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt delete mode 100644 desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt diff --git a/android/src/com/unciv/app/AndroidLauncher.kt b/android/src/com/unciv/app/AndroidLauncher.kt index 3f070ce02a..8bf4f1cbb6 100644 --- a/android/src/com/unciv/app/AndroidLauncher.kt +++ b/android/src/com/unciv/app/AndroidLauncher.kt @@ -15,13 +15,13 @@ import com.unciv.utils.Log import java.io.File open class AndroidLauncher : AndroidApplication() { - private var customSaveLocationHelper: CustomSaveLocationHelperAndroid? = null + private var customFileLocationHelper: CustomFileLocationHelperAndroid? = null private var game: UncivGame? = null private var deepLinkedMultiplayerGame: String? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.backend = AndroidLogBackend() - customSaveLocationHelper = CustomSaveLocationHelperAndroid(this) + customFileLocationHelper = CustomFileLocationHelperAndroid(this) MultiplayerTurnCheckWorker.createNotificationChannels(applicationContext) copyMods() @@ -41,7 +41,7 @@ open class AndroidLauncher : AndroidApplication() { version = BuildConfig.VERSION_NAME, crashReportSysInfo = CrashReportSysInfoAndroid, fontImplementation = NativeFontAndroid(Fonts.ORIGINAL_FONT_SIZE.toInt(), fontFamily), - customSaveLocationHelper = customSaveLocationHelper, + customFileLocationHelper = customFileLocationHelper, platformSpecificHelper = platformSpecificHelper ) @@ -72,9 +72,12 @@ open class AndroidLauncher : AndroidApplication() { if (UncivGame.isCurrentInitialized() && UncivGame.Current.isGameInfoInitialized() && UncivGame.Current.settings.multiplayer.turnCheckerEnabled - && UncivGame.Current.gameSaver.getMultiplayerSaves().any()) { - MultiplayerTurnCheckWorker.startTurnChecker(applicationContext, UncivGame.Current.gameSaver, - UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer) + && UncivGame.Current.gameSaver.getMultiplayerSaves().any() + ) { + MultiplayerTurnCheckWorker.startTurnChecker( + applicationContext, UncivGame.Current.gameSaver, + UncivGame.Current.gameInfo, UncivGame.Current.settings.multiplayer + ) } super.onPause() } @@ -115,7 +118,7 @@ open class AndroidLauncher : AndroidApplication() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - customSaveLocationHelper?.handleIntentData(requestCode, data?.data) + customFileLocationHelper?.onActivityResult(requestCode, data) super.onActivityResult(requestCode, resultCode, data) } } diff --git a/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt b/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt new file mode 100644 index 0000000000..195e619871 --- /dev/null +++ b/android/src/com/unciv/app/CustomFileLocationHelperAndroid.kt @@ -0,0 +1,98 @@ +package com.unciv.app + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.provider.DocumentsContract +import android.provider.OpenableColumns +import androidx.annotation.GuardedBy +import com.unciv.logic.CustomFileLocationHelper +import com.unciv.ui.crashhandling.postCrashHandlingRunnable +import java.io.InputStream +import java.io.OutputStream + +class CustomFileLocationHelperAndroid(private val activity: Activity) : CustomFileLocationHelper() { + + @GuardedBy("this") + private val callbacks = mutableListOf() + @GuardedBy("this") + private var curActivityRequestCode = 100 + + override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) { + val requestCode = createActivityCallback(callback) { activity.contentResolver.openOutputStream(it, "rwt") } + + // When we loaded, we returned a "content://" URI as file location. + val uri = Uri.parse(suggestedLocation) + val fileName = if (uri.scheme == "content") { + val cursor = activity.contentResolver.query(uri, null, null, null, null) + cursor.use { + // we should have a direct URI to a file, so first is enough + if (it?.moveToFirst() == true) { + it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME)) + } else { + "" + } + } + } else { + // if we didn't load, this is some file name entered by the user + suggestedLocation + } + + Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + type = "application/json" + putExtra(Intent.EXTRA_TITLE, fileName) + if (uri.scheme == "content") { + putExtra(DocumentsContract.EXTRA_INITIAL_URI, uri) + } + activity.startActivityForResult(this, requestCode) + } + } + + override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) { + val callbackIndex = createActivityCallback(callback, activity.contentResolver::openInputStream) + + Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + type = "*/*" + // It is theoretically possible to use an initial URI here, however, the only Android URIs we have are obtained from here, so, no dice + activity.startActivityForResult(this, callbackIndex) + } + } + + private fun createActivityCallback(callback: (String?, T?, Exception?) -> Unit, + createValue: (Uri) -> T): Int { + synchronized(this) { + val requestCode = curActivityRequestCode++ + val activityCallback = ActivityCallback(requestCode) { uri -> + if (uri == null) { + callback(null, null, null) + return@ActivityCallback + } + + try { + val outputStream = createValue(uri) + callback(uri.toString(), outputStream, null) + } catch (ex: Exception) { + callback(null, null, ex) + } + } + callbacks.add(activityCallback) + return requestCode + } + } + + fun onActivityResult(requestCode: Int, data: Intent?) { + val callback = synchronized(this) { + val index = callbacks.indexOfFirst { it.requestCode == requestCode } + if (index == -1) return + callbacks.removeAt(index) + } + postCrashHandlingRunnable { + callback.callback(data?.data) + } + } +} + +private class ActivityCallback( + val requestCode: Int, + val callback: (Uri?) -> Unit +) diff --git a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt b/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt deleted file mode 100644 index a12502ef34..0000000000 --- a/android/src/com/unciv/app/CustomSaveLocationHelperAndroid.kt +++ /dev/null @@ -1,124 +0,0 @@ -package com.unciv.app - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Build -import androidx.annotation.GuardedBy -import androidx.annotation.RequiresApi -import com.unciv.logic.CustomSaveLocationHelper -import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver - -// The Storage Access Framework is available from API 19 and up: -// https://developer.android.com/guide/topics/providers/document-provider -@RequiresApi(Build.VERSION_CODES.KITKAT) -class CustomSaveLocationHelperAndroid(private val activity: Activity) : CustomSaveLocationHelper { - // This looks a little scary but it's really not so bad. Whenever a load or save operation is - // attempted, the game automatically autosaves as well (but on a separate thread), so we end up - // with a race condition when trying to handle both operations in parallel. In order to work - // around that, the callbacks are given an arbitrary index beginning at 100 and incrementing - // each time, and this index is used as the requestCode for the call to startActivityForResult() - // so that we can map it back to the corresponding callback when onActivityResult is called - @GuardedBy("this") - @Volatile - private var callbackIndex = 100 - - @GuardedBy("this") - private val callbacks = ArrayList() - - override fun saveGame(gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { - val callbackIndex = synchronized(this) { - val index = callbackIndex++ - callbacks.add(IndexedCallback( - index, - { uri -> - if (uri != null) { - saveGame(gameInfo, uri) - saveCompleteCallback?.invoke(null) - } else { - saveCompleteCallback?.invoke(RuntimeException("Uri was null")) - } - } - )) - index - } - if (!forcePrompt && gameInfo.customSaveLocation != null) { - handleIntentData(callbackIndex, Uri.parse(gameInfo.customSaveLocation)) - return - } - - Intent(Intent.ACTION_CREATE_DOCUMENT).apply { - type = "application/json" - putExtra(Intent.EXTRA_TITLE, gameName) - activity.startActivityForResult(this, callbackIndex) - } - } - - - // This will be called on the main thread - fun handleIntentData(requestCode: Int, uri: Uri?) { - val callback = synchronized(this) { - val index = callbacks.indexOfFirst { it.index == requestCode } - if (index == -1) return - callbacks.removeAt(index) - } - callback.thread.run { - callback.callback(uri) - } - } - - private fun saveGame(gameInfo: GameInfo, uri: Uri) { - gameInfo.customSaveLocation = uri.toString() - activity.contentResolver.openOutputStream(uri, "rwt") - ?.writer() - ?.use { - it.write(GameSaver.gameInfoToString(gameInfo)) - } - } - - override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { - val callbackIndex = synchronized(this) { - val index = callbackIndex++ - callbacks.add(IndexedCallback( - index, - callback@{ uri -> - if (uri == null) return@callback - var exception: Exception? = null - val game = try { - activity.contentResolver.openInputStream(uri) - ?.reader() - ?.readText() - ?.run { - GameSaver.gameInfoFromString(this) - } - } catch (e: Exception) { - exception = e - null - } - if (game != null) { - // If the user has saved the game from another platform (like Android), - // then the save location might not be right so we have to correct for that - // here - game.customSaveLocation = uri.toString() - loadCompleteCallback(game, null) - } else { - loadCompleteCallback(null, RuntimeException("Failed to load save game", exception)) - } - } - )) - index - } - - Intent(Intent.ACTION_OPEN_DOCUMENT).apply { - type = "*/*" - activity.startActivityForResult(this, callbackIndex) - } - } -} - -data class IndexedCallback( - val index: Int, - val callback: (Uri?) -> Unit, - val thread: Thread = Thread.currentThread() -) diff --git a/core/src/com/unciv/UncivGame.kt b/core/src/com/unciv/UncivGame.kt index e210339f48..fdd6e6b478 100644 --- a/core/src/com/unciv/UncivGame.kt +++ b/core/src/com/unciv/UncivGame.kt @@ -41,7 +41,7 @@ class UncivGame(parameters: UncivGameParameters) : Game() { val cancelDiscordEvent = parameters.cancelDiscordEvent var fontImplementation = parameters.fontImplementation val consoleMode = parameters.consoleMode - private val customSaveLocationHelper = parameters.customSaveLocationHelper + private val customSaveLocationHelper = parameters.customFileLocationHelper val platformSpecificHelper = parameters.platformSpecificHelper private val audioExceptionHelper = parameters.audioExceptionHelper diff --git a/core/src/com/unciv/UncivGameParameters.kt b/core/src/com/unciv/UncivGameParameters.kt index 9b854f2b54..1ac4ee1976 100644 --- a/core/src/com/unciv/UncivGameParameters.kt +++ b/core/src/com/unciv/UncivGameParameters.kt @@ -1,6 +1,6 @@ package com.unciv -import com.unciv.logic.CustomSaveLocationHelper +import com.unciv.logic.CustomFileLocationHelper import com.unciv.ui.crashhandling.CrashReportSysInfo import com.unciv.ui.utils.AudioExceptionHelper import com.unciv.ui.utils.GeneralPlatformSpecificHelpers @@ -11,7 +11,7 @@ class UncivGameParameters(val version: String, val cancelDiscordEvent: (() -> Unit)? = null, val fontImplementation: NativeFontImplementation? = null, val consoleMode: Boolean = false, - val customSaveLocationHelper: CustomSaveLocationHelper? = null, + val customFileLocationHelper: CustomFileLocationHelper? = null, val platformSpecificHelper: GeneralPlatformSpecificHelpers? = null, val audioExceptionHelper: AudioExceptionHelper? = null ) diff --git a/core/src/com/unciv/logic/CustomFileLocationHelper.kt b/core/src/com/unciv/logic/CustomFileLocationHelper.kt new file mode 100644 index 0000000000..39fd62a078 --- /dev/null +++ b/core/src/com/unciv/logic/CustomFileLocationHelper.kt @@ -0,0 +1,95 @@ +package com.unciv.logic + +import com.unciv.logic.GameSaver.CustomLoadResult +import com.unciv.logic.GameSaver.CustomSaveResult +import com.unciv.ui.crashhandling.postCrashHandlingRunnable +import java.io.InputStream +import java.io.OutputStream + +/** + * Contract for platform-specific helper classes to handle saving and loading games to and from + * arbitrary external locations. + * + * Implementation note: If a game is loaded with [loadGame] and the same game is saved with [saveGame], + * the suggestedLocation in [saveGame] will be the location returned by [loadGame]. + */ +abstract class CustomFileLocationHelper { + /** + * Saves a game asynchronously to a location selected by the user. + * + * Prefills their UI with a [suggestedLocation]. + * + * Calls the [saveCompleteCallback] on the main thread with the save location on success or the [Exception] on error or null in both on cancel. + */ + fun saveGame( + gameData: String, + suggestedLocation: String, + saveCompleteCallback: (CustomSaveResult) -> Unit = {} + ) { + createOutputStream(suggestedLocation) { location, outputStream, exception -> + if (outputStream == null) { + callSaveCallback(saveCompleteCallback, exception = exception) + return@createOutputStream + } + + try { + outputStream.writer().use { it.write(gameData) } + callSaveCallback(saveCompleteCallback, location) + } catch (ex: Exception) { + callSaveCallback(saveCompleteCallback, exception = ex) + } + } + } + + /** + * Loads a game asynchronously from a location selected by the user. + * + * Calls the [loadCompleteCallback] on the main thread. + */ + fun loadGame(loadCompleteCallback: (CustomLoadResult) -> Unit) { + createInputStream { location, inputStream, exception -> + if (inputStream == null) { + callLoadCallback(loadCompleteCallback, exception = exception) + return@createInputStream + } + + try { + val gameData = inputStream.reader().use { it.readText() } + callLoadCallback(loadCompleteCallback, location, gameData) + } catch (ex: Exception) { + callLoadCallback(loadCompleteCallback, exception = ex) + } + } + } + + /** + * [callback] should be called with the actual selected location and an OutputStream to the location, or an exception if something failed. + */ + protected abstract fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) + + /** + * [callback] should be called with the actual selected location and an InputStream to read the location, or an exception if something failed. + */ + protected abstract fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) +} + +private fun callLoadCallback(loadCompleteCallback: (CustomLoadResult) -> Unit, + location: String? = null, + gameData: String? = null, + exception: Exception? = null) { + val result = if (location != null && gameData != null && exception == null) { + CustomLoadResult(location to gameData) + } else { + CustomLoadResult(null, exception) + } + postCrashHandlingRunnable { + loadCompleteCallback(result) + } +} +private fun callSaveCallback(saveCompleteCallback: (CustomSaveResult) -> Unit, + location: String? = null, + exception: Exception? = null) { + postCrashHandlingRunnable { + saveCompleteCallback(CustomSaveResult(location, exception)) + } +} diff --git a/core/src/com/unciv/logic/CustomSaveLocationHelper.kt b/core/src/com/unciv/logic/CustomSaveLocationHelper.kt deleted file mode 100644 index 4d33a9a279..0000000000 --- a/core/src/com/unciv/logic/CustomSaveLocationHelper.kt +++ /dev/null @@ -1,37 +0,0 @@ -package com.unciv.logic - -/** - * Contract for platform-specific helper classes to handle saving and loading games to and from - * arbitrary external locations - */ -interface CustomSaveLocationHelper { - /**### Save to custom location - * Saves a game asynchronously with a given default name and then calls the [saveCompleteCallback] callback - * upon completion. The [saveCompleteCallback] callback will be called from the same thread that this method - * is called from. If the [GameInfo] object already has the - * [customSaveLocation][GameInfo.customSaveLocation] property defined (not null), then the user - * will not be prompted to select a location for the save unless [forcePrompt] is set to true - * (think of this like "Save as...") - * On success, this is also expected to set [customSaveLocation][GameInfo.customSaveLocation]. - * - * @param gameInfo Game data to save - * @param gameName Suggestion for the save name - * @param forcePrompt Bypass UI if location contained in [gameInfo] and [forcePrompt]==`false` - * @param saveCompleteCallback Action to call upon completion (success _and_ failure) - */ - fun saveGame( - gameInfo: GameInfo, - gameName: String, - forcePrompt: Boolean = false, - saveCompleteCallback: ((Exception?) -> Unit)? = null - ) - - /**### Load from custom location - * Loads a game from an external source asynchronously, then calls [loadCompleteCallback] with the loaded [GameInfo]. - * On success, this is also expected to set the loaded [GameInfo]'s property [customSaveLocation][GameInfo.customSaveLocation]. - * Note that there is no hint so pass a default location or a way to remember the folder the user chose last time. - * - * @param loadCompleteCallback Action to call upon completion (success _and_ failure) - */ - fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) -} diff --git a/core/src/com/unciv/logic/GameSaver.kt b/core/src/com/unciv/logic/GameSaver.kt index 2f75270a8f..e05bd52952 100644 --- a/core/src/com/unciv/logic/GameSaver.kt +++ b/core/src/com/unciv/logic/GameSaver.kt @@ -28,7 +28,7 @@ class GameSaver( * which is normally responsible for keeping the [Gdx] static variables from being garbage collected. */ private val files: Files, - private val customSaveLocationHelper: CustomSaveLocationHelper? = null, + private val customFileLocationHelper: CustomFileLocationHelper? = null, /** When set, we know we're on Android and can save to the app's personal external file directory * See https://developer.android.com/training/data-storage/app-specific#external-access-files */ private val externalFilesDirForAndroid: String? = null @@ -71,7 +71,7 @@ class GameSaver( return localSaves + files.absolute(externalFilesDirForAndroid + "/${saveFolder}").list().asSequence() } - fun canLoadFromCustomSaveLocation() = customSaveLocationHelper != null + fun canLoadFromCustomSaveLocation() = customFileLocationHelper != null fun deleteSave(gameName: String) { getSave(gameName).delete() @@ -88,6 +88,15 @@ class GameSaver( file.delete() } + interface ChooseLocationResult { + val location: String? + val exception: Exception? + + fun isCanceled(): Boolean = location == null && exception == null + fun isError(): Boolean = exception != null + fun isSuccessful(): Boolean = location != null + } + //endregion //region Saving @@ -130,8 +139,31 @@ class GameSaver( } } - fun saveGameToCustomLocation(game: GameInfo, GameName: String, saveCompletionCallback: (Exception?) -> Unit) { - customSaveLocationHelper!!.saveGame(game, GameName, forcePrompt = true, saveCompleteCallback = saveCompletionCallback) + class CustomSaveResult( + override val location: String? = null, + override val exception: Exception? = null + ) : ChooseLocationResult + + /** + * [gameName] is a suggested name for the file. If the file has already been saved to or loaded from a custom location, + * this previous custom location will be used. + * + * Calls the [saveCompleteCallback] on the main thread with the save location on success, an [Exception] on error, or both null on cancel. + */ + fun saveGameToCustomLocation(game: GameInfo, gameName: String, saveCompletionCallback: (CustomSaveResult) -> Unit) { + val saveLocation = game.customSaveLocation ?: Gdx.files.local(gameName).path() + val gameData = try { + gameInfoToString(game) + } catch (ex: Exception) { + postCrashHandlingRunnable { saveCompletionCallback(CustomSaveResult(exception = ex)) } + return + } + customFileLocationHelper!!.saveGame(gameData, saveLocation) { + if (it.isSuccessful()) { + game.customSaveLocation = it.location + } + saveCompletionCallback(it) + } } //endregion @@ -151,9 +183,34 @@ class GameSaver( return json().fromJson(GameInfoPreview::class.java, gameFile) } - fun loadGameFromCustomLocation(loadCompletionCallback: (GameInfo?, Exception?) -> Unit) { - customSaveLocationHelper!!.loadGame { game, e -> - loadCompletionCallback(game?.apply { setTransients() }, e) + class CustomLoadResult( + private val locationAndGameData: Pair? = null, + override val exception: Exception? = null + ) : ChooseLocationResult { + override val location: String? get() = locationAndGameData?.first + val gameData: T? get() = locationAndGameData?.second + } + + /** + * Calls the [loadCompleteCallback] on the main thread with the [GameInfo] on success or the [Exception] on error or null in both on cancel. + */ + fun loadGameFromCustomLocation(loadCompletionCallback: (CustomLoadResult) -> Unit) { + customFileLocationHelper!!.loadGame { result -> + val location = result.location + val gameData = result.gameData + if (location == null || gameData == null) { + loadCompletionCallback(CustomLoadResult(exception = result.exception)) + return@loadGame + } + + try { + val gameInfo = gameInfoFromString(gameData) + gameInfo.customSaveLocation = location + gameInfo.setTransients() + loadCompletionCallback(CustomLoadResult(location to gameInfo)) + } catch (ex: Exception) { + loadCompletionCallback(CustomLoadResult(exception = ex)) + } } } diff --git a/core/src/com/unciv/ui/saves/LoadGameScreen.kt b/core/src/com/unciv/ui/saves/LoadGameScreen.kt index a61a73596c..8c48f37a6b 100644 --- a/core/src/com/unciv/ui/saves/LoadGameScreen.kt +++ b/core/src/com/unciv/ui/saves/LoadGameScreen.kt @@ -99,13 +99,12 @@ class LoadGameScreen(previousScreen:BaseScreen) : PickerScreen(disableScroll = t if (game.gameSaver.canLoadFromCustomSaveLocation()) { val loadFromCustomLocation = "Load from custom location".toTextButton() loadFromCustomLocation.onClick { - game.gameSaver.loadGameFromCustomLocation { gameInfo, exception -> - if (gameInfo != null) { - postCrashHandlingRunnable { - game.loadGame(gameInfo) - } - } else if (exception !is CancellationException) - handleLoadGameException("Could not load game from custom location!", exception) + game.gameSaver.loadGameFromCustomLocation { result -> + if (result.isError()) { + handleLoadGameException("Could not load game from custom location!", result.exception) + } else if (result.isSuccessful()) { + game.loadGame(result.gameData!!) + } } } rightSideTable.add(loadFromCustomLocation).row() diff --git a/core/src/com/unciv/ui/saves/SaveGameScreen.kt b/core/src/com/unciv/ui/saves/SaveGameScreen.kt index 43bc0b8009..4a23bdd5a1 100644 --- a/core/src/com/unciv/ui/saves/SaveGameScreen.kt +++ b/core/src/com/unciv/ui/saves/SaveGameScreen.kt @@ -4,6 +4,7 @@ import com.badlogic.gdx.Gdx import com.badlogic.gdx.graphics.Color import com.badlogic.gdx.scenes.scene2d.ui.CheckBox import com.badlogic.gdx.scenes.scene2d.ui.Table +import com.badlogic.gdx.scenes.scene2d.ui.TextButton import com.badlogic.gdx.scenes.scene2d.ui.TextField import com.unciv.UncivGame import com.unciv.logic.GameInfo @@ -53,7 +54,8 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true newSave.add(copyJsonButton).row() if (game.gameSaver.canLoadFromCustomSaveLocation()) { - val saveToCustomLocation = "Save to custom location".toTextButton() + val saveText = "Save to custom location".tr() + val saveToCustomLocation = TextButton(saveText, BaseScreen.skin) val errorLabel = "".toLabel(Color.RED) saveToCustomLocation.enable() saveToCustomLocation.onClick { @@ -61,14 +63,15 @@ class SaveGameScreen(val gameInfo: GameInfo) : PickerScreen(disableScroll = true saveToCustomLocation.setText("Saving...".tr()) saveToCustomLocation.disable() launchCrashHandling("SaveGame", runAsDaemon = false) { - game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { e -> - if (e == null) { - postCrashHandlingRunnable { game.resetToWorldScreen() } - } else if (e !is CancellationException) { + game.gameSaver.saveGameToCustomLocation(gameInfo, gameNameTextField.text) { result -> + if (result.isError()) { errorLabel.setText("Could not save game to custom location!".tr()) - e.printStackTrace() + result.exception?.printStackTrace() + } else if (result.isSuccessful()) { + game.resetToWorldScreen() } saveToCustomLocation.enable() + saveToCustomLocation.setText(saveText) } } } diff --git a/desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt b/desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt new file mode 100644 index 0000000000..deedd2bb75 --- /dev/null +++ b/desktop/src/com/unciv/app/desktop/CustomFileLocationHelperDesktop.kt @@ -0,0 +1,60 @@ +package com.unciv.app.desktop + +import com.badlogic.gdx.Gdx +import com.unciv.logic.CustomFileLocationHelper +import java.awt.Component +import java.awt.EventQueue +import java.awt.event.WindowEvent +import java.io.File +import java.io.InputStream +import java.io.OutputStream +import javax.swing.JFileChooser +import javax.swing.JFrame + +class CustomFileLocationHelperDesktop : CustomFileLocationHelper() { + + override fun createOutputStream(suggestedLocation: String, callback: (String?, OutputStream?, Exception?) -> Unit) { + pickFile(callback, JFileChooser::showSaveDialog, File::outputStream, suggestedLocation) + } + + override fun createInputStream(callback: (String?, InputStream?, Exception?) -> Unit) { + pickFile(callback, JFileChooser::showOpenDialog, File::inputStream) + } + + private fun pickFile(callback: (String?, T?, Exception?) -> Unit, + chooseAction: (JFileChooser, Component) -> Int, + createValue: (File) -> T, + suggestedLocation: String? = null) { + EventQueue.invokeLater { + try { + val fileChooser = JFileChooser().apply fileChooser@{ + if (suggestedLocation == null) { + currentDirectory = Gdx.files.local("").file() + } else { + selectedFile = File(suggestedLocation) + } + } + + val result: Int + val frame = JFrame().apply frame@{ + setLocationRelativeTo(null) + isVisible = true + toFront() + result = chooseAction(fileChooser, this@frame) + dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING)) + } + + frame.dispose() + + if (result == JFileChooser.CANCEL_OPTION) { + callback(null, null, null) + } else { + val value = createValue(fileChooser.selectedFile) + callback(fileChooser.selectedFile.absolutePath, value, null) + } + } catch (ex: Exception) { + callback(null, null, ex) + } + } + } +} diff --git a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt b/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt deleted file mode 100644 index af6e281a9a..0000000000 --- a/desktop/src/com/unciv/app/desktop/CustomSaveLocationHelperDesktop.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.unciv.app.desktop - -import com.badlogic.gdx.Gdx -import com.unciv.json.json -import com.unciv.logic.CustomSaveLocationHelper -import com.unciv.logic.GameInfo -import com.unciv.logic.GameSaver -import java.awt.event.WindowEvent -import java.io.File -import java.util.concurrent.CancellationException -import javax.swing.JFileChooser -import javax.swing.JFrame - -class CustomSaveLocationHelperDesktop : CustomSaveLocationHelper { - override fun saveGame(gameInfo: GameInfo, gameName: String, forcePrompt: Boolean, saveCompleteCallback: ((Exception?) -> Unit)?) { - val customSaveLocation = gameInfo.customSaveLocation - if (customSaveLocation != null && !forcePrompt) { - try { - File(customSaveLocation).outputStream() - .writer() - .use { writer -> - writer.write(GameSaver.gameInfoToString(gameInfo)) - } - saveCompleteCallback?.invoke(null) - } catch (e: Exception) { - saveCompleteCallback?.invoke(e) - } - return - } - - val fileChooser = JFileChooser().apply fileChooser@{ - currentDirectory = Gdx.files.local("").file() - selectedFile = File(gameInfo.customSaveLocation ?: gameName) - } - - JFrame().apply frame@{ - setLocationRelativeTo(null) - isVisible = true - toFront() - fileChooser.showSaveDialog(this@frame) - dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING)) - } - val file = fileChooser.selectedFile - var exception: Exception? = null - if (file != null) { - gameInfo.customSaveLocation = file.absolutePath - try { - file.outputStream() - .writer() - .use { - it.write(json().toJson(gameInfo)) - } - } catch (e: Exception) { - exception = e - } - } else { - exception = CancellationException() - } - saveCompleteCallback?.invoke(exception) - } - - override fun loadGame(loadCompleteCallback: (GameInfo?, Exception?) -> Unit) { - val fileChooser = JFileChooser().apply fileChooser@{ - currentDirectory = Gdx.files.local("").file() - } - - JFrame().apply frame@{ - setLocationRelativeTo(null) - isVisible = true - toFront() - fileChooser.showOpenDialog(this@frame) - dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING)) - } - val file = fileChooser.selectedFile - var exception: Exception? = null - var gameInfo: GameInfo? = null - if (file != null) { - try { - file.inputStream() - .reader() - .readText() - .run { GameSaver.gameInfoFromString(this) } - .apply { - // If the user has saved the game from another platform (like Android), - // then the save location might not be right so we have to correct for that - // here - customSaveLocation = file.absolutePath - gameInfo = this - } - } catch (e: Exception) { - exception = e - } - } else { - exception = CancellationException() - } - loadCompleteCallback(gameInfo, exception) - } -} diff --git a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt index d7e996e3d7..cd33049e3b 100644 --- a/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt +++ b/desktop/src/com/unciv/app/desktop/DesktopLauncher.kt @@ -54,7 +54,7 @@ internal object DesktopLauncher { versionFromJar, cancelDiscordEvent = { discordTimer?.cancel() }, fontImplementation = NativeFontDesktop(Fonts.ORIGINAL_FONT_SIZE.toInt(), settings.fontFamily), - customSaveLocationHelper = CustomSaveLocationHelperDesktop(), + customFileLocationHelper = CustomFileLocationHelperDesktop(), crashReportSysInfo = CrashReportSysInfoDesktop(), platformSpecificHelper = platformSpecificHelper, audioExceptionHelper = HardenGdxAudio()