mirror of
https://github.com/DaanVandenBosch/phantasmal-world.git
synced 2025-04-08 01:01:36 +08:00
"Save" and "Save as..." buttons are now disabled while saving, mainly as visual feedback that a save happened.
This commit is contained in:
parent
4b8241ba80
commit
5133235040
@ -1,11 +1,12 @@
|
|||||||
package world.phantasmal.core
|
package world.phantasmal.core
|
||||||
|
|
||||||
|
import kotlinx.coroutines.await
|
||||||
import world.phantasmal.core.externals.browser.WritableStream
|
import world.phantasmal.core.externals.browser.WritableStream
|
||||||
|
|
||||||
inline fun <S : WritableStream, R> S.use(block: (S) -> R): R {
|
suspend inline fun <S : WritableStream, R> S.use(block: (S) -> R): R {
|
||||||
try {
|
try {
|
||||||
return block(this)
|
return block(this)
|
||||||
} finally {
|
} finally {
|
||||||
close()
|
close().await()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,16 @@
|
|||||||
package world.phantasmal.web.core.files
|
package world.phantasmal.web.core.files
|
||||||
|
|
||||||
|
import kotlinx.coroutines.await
|
||||||
|
import world.phantasmal.core.use
|
||||||
import world.phantasmal.lib.Endianness
|
import world.phantasmal.lib.Endianness
|
||||||
|
import world.phantasmal.lib.buffer.Buffer
|
||||||
import world.phantasmal.lib.cursor.Cursor
|
import world.phantasmal.lib.cursor.Cursor
|
||||||
import world.phantasmal.lib.cursor.cursor
|
import world.phantasmal.lib.cursor.cursor
|
||||||
import world.phantasmal.webui.files.FileHandle
|
import world.phantasmal.webui.files.FileHandle
|
||||||
|
|
||||||
suspend fun FileHandle.cursor(endianness: Endianness): Cursor =
|
suspend fun FileHandle.cursor(endianness: Endianness): Cursor =
|
||||||
arrayBuffer().cursor(endianness)
|
arrayBuffer().cursor(endianness)
|
||||||
|
|
||||||
|
suspend fun FileHandle.Fsaa.writeBuffer(buffer: Buffer) {
|
||||||
|
writableStream().use { it.write(buffer.arrayBuffer).await() }
|
||||||
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package world.phantasmal.web.questEditor.controllers
|
package world.phantasmal.web.questEditor.controllers
|
||||||
|
|
||||||
import kotlinx.browser.document
|
import kotlinx.browser.document
|
||||||
import kotlinx.coroutines.await
|
|
||||||
import mu.KotlinLogging
|
import mu.KotlinLogging
|
||||||
import org.w3c.dom.HTMLAnchorElement
|
import org.w3c.dom.HTMLAnchorElement
|
||||||
import org.w3c.dom.url.URL
|
import org.w3c.dom.url.URL
|
||||||
@ -15,6 +14,7 @@ import world.phantasmal.observable.value.list.MutableListVal
|
|||||||
import world.phantasmal.observable.value.list.mutableListVal
|
import world.phantasmal.observable.value.list.mutableListVal
|
||||||
import world.phantasmal.web.core.PwToolType
|
import world.phantasmal.web.core.PwToolType
|
||||||
import world.phantasmal.web.core.files.cursor
|
import world.phantasmal.web.core.files.cursor
|
||||||
|
import world.phantasmal.web.core.files.writeBuffer
|
||||||
import world.phantasmal.web.core.stores.UiStore
|
import world.phantasmal.web.core.stores.UiStore
|
||||||
import world.phantasmal.web.questEditor.models.AreaModel
|
import world.phantasmal.web.questEditor.models.AreaModel
|
||||||
import world.phantasmal.web.questEditor.stores.AreaStore
|
import world.phantasmal.web.questEditor.stores.AreaStore
|
||||||
@ -37,10 +37,14 @@ class QuestEditorToolbarController(
|
|||||||
private val areaStore: AreaStore,
|
private val areaStore: AreaStore,
|
||||||
private val questEditorStore: QuestEditorStore,
|
private val questEditorStore: QuestEditorStore,
|
||||||
) : Controller() {
|
) : Controller() {
|
||||||
private val questLoaded = questEditorStore.currentQuest.isNotNull()
|
|
||||||
private val _resultDialogVisible = mutableVal(false)
|
private val _resultDialogVisible = mutableVal(false)
|
||||||
private val _result = mutableVal<PwResult<*>?>(null)
|
private val _result = mutableVal<PwResult<*>?>(null)
|
||||||
private val _saveAsDialogVisible = mutableVal(false)
|
private val _saveAsDialogVisible = mutableVal(false)
|
||||||
|
private val saving = mutableVal(false)
|
||||||
|
|
||||||
|
// We mainly disable saving while a save is underway for visual feedback that a save is
|
||||||
|
// happening/has happened.
|
||||||
|
private val savingEnabled = questEditorStore.currentQuest.isNotNull() and !saving
|
||||||
private val files: MutableListVal<FileHandle> = mutableListVal()
|
private val files: MutableListVal<FileHandle> = mutableListVal()
|
||||||
private val _filename = mutableVal("")
|
private val _filename = mutableVal("")
|
||||||
private val _version = mutableVal(Version.BB)
|
private val _version = mutableVal(Version.BB)
|
||||||
@ -59,12 +63,13 @@ class QuestEditorToolbarController(
|
|||||||
|
|
||||||
// Save as
|
// Save as
|
||||||
|
|
||||||
val saveEnabled: Val<Boolean> = questLoaded and files.notEmpty and BrowserFeatures.fileSystemApi
|
val saveEnabled: Val<Boolean> =
|
||||||
|
savingEnabled and files.notEmpty and BrowserFeatures.fileSystemApi
|
||||||
val saveTooltip: Val<String> = value(
|
val saveTooltip: Val<String> = value(
|
||||||
if (BrowserFeatures.fileSystemApi) "Save changes (Ctrl-S)"
|
if (BrowserFeatures.fileSystemApi) "Save changes (Ctrl-S)"
|
||||||
else "This browser doesn't support saving to an existing file"
|
else "This browser doesn't support saving to an existing file"
|
||||||
)
|
)
|
||||||
val saveAsEnabled: Val<Boolean> = questLoaded
|
val saveAsEnabled: Val<Boolean> = savingEnabled
|
||||||
val saveAsDialogVisible: Val<Boolean> = _saveAsDialogVisible
|
val saveAsDialogVisible: Val<Boolean> = _saveAsDialogVisible
|
||||||
val filename: Val<String> = _filename
|
val filename: Val<String> = _filename
|
||||||
val version: Val<Version> = _version
|
val version: Val<Version> = _version
|
||||||
@ -170,7 +175,7 @@ class QuestEditorToolbarController(
|
|||||||
if (binFile == null || datFile == null) {
|
if (binFile == null || datFile == null) {
|
||||||
setResult(Failure(listOf(Problem(
|
setResult(Failure(listOf(Problem(
|
||||||
Severity.Error,
|
Severity.Error,
|
||||||
"Please select a .qst file or one .bin and one .dat file."
|
"Please select a .qst file or one .bin and one .dat file.",
|
||||||
))))
|
))))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -198,13 +203,16 @@ class QuestEditorToolbarController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun save() {
|
suspend fun save() {
|
||||||
if (saveEnabled.value) {
|
if (!saveEnabled.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
val quest = questEditorStore.currentQuest.value ?: return
|
val quest = questEditorStore.currentQuest.value ?: return
|
||||||
val headerFilename = filename.value.trim()
|
val headerFilename = filename.value.trim()
|
||||||
|
val files = files.value.filterIsInstance<FileHandle.Fsaa>()
|
||||||
|
|
||||||
files.value.find { it.extension().equals("qst", ignoreCase = true) }
|
files.find { it.extension().equals("qst", ignoreCase = true) }?.let { qstFile ->
|
||||||
?.let { qstFile ->
|
|
||||||
val buffer = writeQuestToQst(
|
val buffer = writeQuestToQst(
|
||||||
convertQuestFromModel(quest),
|
convertQuestFromModel(quest),
|
||||||
headerFilename,
|
headerFilename,
|
||||||
@ -212,13 +220,11 @@ class QuestEditorToolbarController(
|
|||||||
online = true,
|
online = true,
|
||||||
)
|
)
|
||||||
|
|
||||||
qstFile.writableStream()!!.use {
|
qstFile.writeBuffer(buffer)
|
||||||
it.write(buffer.arrayBuffer).await()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val binFile = files.value.find { it.extension().equals("bin", ignoreCase = true) }
|
val binFile = files.find { it.extension().equals("bin", ignoreCase = true) }
|
||||||
val datFile = files.value.find { it.extension().equals("dat", ignoreCase = true) }
|
val datFile = files.find { it.extension().equals("dat", ignoreCase = true) }
|
||||||
|
|
||||||
if (binFile != null && datFile != null) {
|
if (binFile != null && datFile != null) {
|
||||||
val (bin, dat) = writeQuestToBinDat(
|
val (bin, dat) = writeQuestToBinDat(
|
||||||
@ -226,8 +232,8 @@ class QuestEditorToolbarController(
|
|||||||
version.value,
|
version.value,
|
||||||
)
|
)
|
||||||
|
|
||||||
binFile.writableStream()!!.use { it.write(bin.arrayBuffer).await() }
|
binFile.writeBuffer(bin)
|
||||||
datFile.writableStream()!!.use { it.write(dat.arrayBuffer).await() }
|
datFile.writeBuffer(dat)
|
||||||
}
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
setResult(
|
setResult(
|
||||||
@ -235,7 +241,8 @@ class QuestEditorToolbarController(
|
|||||||
.addProblem(Severity.Error, "Couldn't save file.", cause = e)
|
.addProblem(Severity.Error, "Couldn't save file.", cause = e)
|
||||||
.failure()
|
.failure()
|
||||||
)
|
)
|
||||||
}
|
} finally {
|
||||||
|
saving.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -254,7 +261,13 @@ class QuestEditorToolbarController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun saveAsDialogSave() {
|
fun saveAsDialogSave() {
|
||||||
|
if (!saveAsEnabled.value) return
|
||||||
|
|
||||||
val quest = questEditorStore.currentQuest.value ?: return
|
val quest = questEditorStore.currentQuest.value ?: return
|
||||||
|
|
||||||
|
try {
|
||||||
|
saving.value = true
|
||||||
|
|
||||||
val headerFilename = filename.value.trim()
|
val headerFilename = filename.value.trim()
|
||||||
val filename =
|
val filename =
|
||||||
if (headerFilename.endsWith(".qst")) headerFilename
|
if (headerFilename.endsWith(".qst")) headerFilename
|
||||||
@ -280,6 +293,10 @@ class QuestEditorToolbarController(
|
|||||||
a.download = filename
|
a.download = filename
|
||||||
document.body?.appendChild(a)
|
document.body?.appendChild(a)
|
||||||
a.click()
|
a.click()
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url)
|
||||||
|
document.body?.removeChild(a)
|
||||||
|
}
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
setResult(
|
setResult(
|
||||||
PwResult.build<Nothing>(logger)
|
PwResult.build<Nothing>(logger)
|
||||||
@ -287,11 +304,9 @@ class QuestEditorToolbarController(
|
|||||||
.failure()
|
.failure()
|
||||||
)
|
)
|
||||||
} finally {
|
} finally {
|
||||||
URL.revokeObjectURL(url)
|
|
||||||
document.body?.removeChild(a)
|
|
||||||
}
|
|
||||||
|
|
||||||
dismissSaveAsDialog()
|
dismissSaveAsDialog()
|
||||||
|
saving.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dismissSaveAsDialog() {
|
fun dismissSaveAsDialog() {
|
||||||
|
@ -18,21 +18,9 @@ import world.phantasmal.webui.BrowserFeatures
|
|||||||
import world.phantasmal.webui.dom.disposableListener
|
import world.phantasmal.webui.dom.disposableListener
|
||||||
import world.phantasmal.webui.obj
|
import world.phantasmal.webui.obj
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.js.Promise
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
sealed class FileHandle {
|
||||||
class FileHandle private constructor(
|
abstract val name: String
|
||||||
private val handle: FileSystemFileHandle?,
|
|
||||||
private val file: File?,
|
|
||||||
) {
|
|
||||||
constructor(file: File) : this(null, file)
|
|
||||||
constructor(handle: FileSystemFileHandle) : this(handle, null)
|
|
||||||
|
|
||||||
val name: String = handle?.name ?: file!!.name
|
|
||||||
|
|
||||||
init {
|
|
||||||
require((handle == null) xor (file == null))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the filename without extension if there is one.
|
* Returns the filename without extension if there is one.
|
||||||
@ -44,20 +32,29 @@ class FileHandle private constructor(
|
|||||||
*/
|
*/
|
||||||
fun extension(): String? = filenameExtension(name)
|
fun extension(): String? = filenameExtension(name)
|
||||||
|
|
||||||
/**
|
suspend fun arrayBuffer(): ArrayBuffer =
|
||||||
* Returns a writable stream if this [FileHandle] represents a [FileSystemFileHandle].
|
getFile().arrayBuffer().await()
|
||||||
*/
|
|
||||||
suspend fun writableStream(): FileSystemWritableFileStream? =
|
|
||||||
handle?.createWritable()?.await()
|
|
||||||
|
|
||||||
suspend fun arrayBuffer(): ArrayBuffer = suspendCancellableCoroutine { cont ->
|
protected abstract suspend fun getFile(): File
|
||||||
getFile()
|
|
||||||
.then { it.arrayBuffer() }
|
/**
|
||||||
.then({ cont.resume(it) {} }, cont::cancel)
|
* File system access API file handle.
|
||||||
|
*/
|
||||||
|
class Fsaa(private val handle: FileSystemFileHandle) : FileHandle() {
|
||||||
|
override val name: String = handle.name
|
||||||
|
|
||||||
|
suspend fun writableStream(): FileSystemWritableFileStream =
|
||||||
|
handle.createWritable().await()
|
||||||
|
|
||||||
|
override suspend fun getFile(): File =
|
||||||
|
handle.getFile().await()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getFile(): Promise<File> =
|
class Simple(private val file: File) : FileHandle() {
|
||||||
handle?.getFile() ?: Promise.resolve(file!!)
|
override val name: String = file.name
|
||||||
|
|
||||||
|
override suspend fun getFile(): File = file
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FileType(
|
class FileType(
|
||||||
@ -86,7 +83,7 @@ suspend fun showFilePicker(types: List<FileType>, multiple: Boolean = false): Li
|
|||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
}).await()
|
}).await()
|
||||||
|
|
||||||
fileHandles.map(::FileHandle)
|
fileHandles.map(FileHandle::Fsaa)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
// Ensure we return null when the user cancels.
|
// Ensure we return null when the user cancels.
|
||||||
if (e.asDynamic().name == "AbortError") {
|
if (e.asDynamic().name == "AbortError") {
|
||||||
@ -103,7 +100,7 @@ suspend fun showFilePicker(types: List<FileType>, multiple: Boolean = false): Li
|
|||||||
el.multiple = multiple
|
el.multiple = multiple
|
||||||
|
|
||||||
el.onchange = {
|
el.onchange = {
|
||||||
cont.resume(el.files!!.asList().map(::FileHandle))
|
cont.resume(el.files!!.asList().map(FileHandle::Simple))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we return null when the user cancels.
|
// Ensure we return null when the user cancels.
|
||||||
|
Loading…
Reference in New Issue
Block a user