From c2e8c3611d8a92eb0d54fefc1585463055d79f33 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Thu, 24 Oct 2024 20:23:53 -0700 Subject: [PATCH] Refactored SceneEditorRepository into Datasource --- .../data/id/datasources/SceneIdDatasource.kt | 3 +- .../synchronizers/ClientSceneSynchronizer.kt | 2 +- .../sceneeditorrepository/SceneDatasource.kt | 429 ++++++++- .../SceneEditorRepository.kt | 789 ++++++++++++---- .../SceneEditorRepositoryOkio.kt | 865 ------------------ .../SceneRepositoryExceptions.kt | 7 + .../sceneeditorrepository/SceneTreeExt.kt | 8 + .../common/dependencyinjection/mainModule.kt | 3 +- .../scenedraft/SceneDraftRepositoryTest.kt | 3 +- .../SceneEditorRepositoryBufferTest.kt | 19 +- .../SceneEditorRepositoryLoadTest.kt | 11 +- .../SceneEditorRepositoryMetadataTest.kt | 4 +- .../SceneEditorRepositoryMoveTest.kt | 6 +- .../SceneEditorRepositoryOtherTest.kt | 13 +- .../SceneEditorRepositoryTestSimple.kt | 37 +- .../synchronizer/SceneSynchronizerTest.kt | 10 +- 16 files changed, 1090 insertions(+), 1119 deletions(-) delete mode 100644 common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepositoryOkio.kt create mode 100644 common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneRepositoryExceptions.kt create mode 100644 common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneTreeExt.kt diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/id/datasources/SceneIdDatasource.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/id/datasources/SceneIdDatasource.kt index 833af3eb..f7eaa199 100644 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/id/datasources/SceneIdDatasource.kt +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/id/datasources/SceneIdDatasource.kt @@ -2,7 +2,6 @@ package com.darkrockstudios.apps.hammer.common.data.id.datasources import com.darkrockstudios.apps.hammer.common.data.ProjectDef import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.filterScenePathsOkio import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath import okio.FileSystem @@ -15,7 +14,7 @@ class SceneIdDatasource( val maxId: Int = fileSystem.listRecursively(sceneDir) .filterScenePathsOkio().maxOfOrNull { path -> - SceneEditorRepository.getSceneIdFromFilename(path.name) + SceneDatasource.getSceneIdFromFilename(path.name) } ?: -1 return maxId diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/projectsync/synchronizers/ClientSceneSynchronizer.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/projectsync/synchronizers/ClientSceneSynchronizer.kt index 59a9cad9..11ca9d86 100644 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/projectsync/synchronizers/ClientSceneSynchronizer.kt +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/projectsync/synchronizers/ClientSceneSynchronizer.kt @@ -133,7 +133,7 @@ class ClientSceneSynchronizer( order = serverEntity.order ) - val scenePath = sceneEditorRepository.getPathFromFilesystem(sceneItem) + val scenePath = sceneEditorRepository.resolveScenePathFromFilesystem(sceneItem.id) ?: throw IllegalStateException("Scene $id has no path") val content = SceneContent(sceneItem, serverEntity.content) diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneDatasource.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneDatasource.kt index eadd5345..616a47d0 100644 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneDatasource.kt +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneDatasource.kt @@ -1,11 +1,19 @@ package com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository import com.darkrockstudios.apps.hammer.common.data.ProjectDef -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository.Companion.SCENE_DIRECTORY +import com.darkrockstudios.apps.hammer.common.data.SceneBuffer +import com.darkrockstudios.apps.hammer.common.data.SceneContent +import com.darkrockstudios.apps.hammer.common.data.SceneItem +import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource.Companion.validateSceneFilename +import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode +import com.darkrockstudios.apps.hammer.common.data.tree.TreeValue import com.darkrockstudios.apps.hammer.common.fileio.HPath import com.darkrockstudios.apps.hammer.common.fileio.okio.toHPath import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath +import io.github.aakira.napier.Napier import okio.FileSystem +import okio.IOException +import okio.Path class SceneDatasource( private val projectDef: ProjectDef, @@ -14,7 +22,410 @@ class SceneDatasource( fun getSceneDirectory(): HPath = getSceneDirectory(projectDef, fileSystem) + fun getSceneIdFromPath(path: HPath): Int { + val fileName = getSceneFilename(path) + return getSceneIdFromFilename(fileName) + } + + fun resolveScenePathFromFilesystem(id: Int, paths: List = getAllScenePaths()): HPath? { + return paths.find { path -> getSceneIdFromPath(path) == id } + } + + fun getPathFromFilesystem(sceneItem: SceneItem): HPath? { + return getAllScenePathsOkio() + .filterScenePathsOkio().firstOrNull { path -> + sceneItem.id == getSceneFromFilename(path).id + } + } + + private fun getAllScenePathsOkio(): List { + val sceneDirPath = getSceneDirectory().toOkioPath() + val scenePaths = fileSystem.listRecursively(sceneDirPath) + .filterScenePathsOkio() + .sortedBy { it.name } + .map { it.toOkioPath() } + .toList() + return scenePaths + } + + fun getAllScenePaths(): List { + val sceneDirPath = getSceneDirectory().toOkioPath() + val scenePaths = fileSystem.listRecursively(sceneDirPath) + .filterScenePathsOkio() + .sortedBy { it.name } + .toList() + return scenePaths + } + + fun getAllScenes(): List { + return getAllScenePathsOkio() + .filterScenePathsOkio() + .map { path -> + getSceneFromFilename(path) + } + } + + private fun getSceneTempFileName(sceneDef: SceneItem): String { + return "${sceneDef.id}.md" + } + + private fun getSceneBufferTempPath(sceneItem: SceneItem): HPath { + val bufferPathSegment = getSceneBufferDirectory().toOkioPath() + val fileName = getSceneTempFileName(sceneItem) + return bufferPathSegment.div(fileName).toHPath() + } + + private fun getSceneIdFromBufferFilename(fileName: String): Int { + val captures = SCENE_BUFFER_FILENAME_PATTERN.matchEntire(fileName) + ?: throw IllegalStateException("Scene filename was bad: $fileName") + + try { + val sceneId = captures.groupValues[1].toInt() + return sceneId + } catch (e: NumberFormatException) { + throw InvalidSceneBufferFilename("Number format exception", fileName) + } catch (e: IllegalStateException) { + throw InvalidSceneBufferFilename("Invalid filename", fileName) + } + } + + fun getSceneTempBufferContents(): List { + val bufferDirectory = getSceneBufferDirectory().toOkioPath() + return fileSystem.list(bufferDirectory) + .filter { fileSystem.metadata(it).isRegularFile } + .mapNotNull { path -> + val sceneId = getSceneIdFromBufferFilename(path.name) + resolveScenePathFromFilesystem(sceneId)?.let { scenePath -> + getSceneFromFilename(scenePath) + } + } + .map { sceneDef -> + val tempPath = getSceneBufferTempPath(sceneDef).toOkioPath() + val content = try { + fileSystem.read(tempPath) { + readUtf8() + } + } catch (e: IOException) { + Napier.e("Failed to load Scene (${sceneDef.name})") + "" + } + SceneContent(sceneDef, content) + } + } + + fun exportStory(path: HPath, allNodes: List>): HPath { + val exportPath = path.toOkioPath() / getExportStoryFileName() + val allPaths = getAllScenePaths() + + fileSystem.write(exportPath) { + writeUtf8("# ${projectDef.name}\n\n") + + allNodes.forEachIndexed { index, chapterNode -> + val scene = chapterNode.value + + val chapterNumber = index + 1 + + writeUtf8("\n## $chapterNumber. ${scene.name}\n\n") + + val scenePath = resolveScenePathFromFilesystem(chapterNode.value.id, allPaths) + ?: error("Could not find Scene for ID ${chapterNode.value.id}") + + if (scene.type == SceneItem.Type.Scene) { + val markdown = loadSceneMarkdownRaw(chapterNode.value, scenePath) + writeUtf8(markdown) + writeUtf8("\n") + } else { + chapterNode.filter { it.value.type == SceneItem.Type.Scene } + .forEach { sceneNode -> + val childScenePath = + resolveScenePathFromFilesystem(sceneNode.value.id, allPaths) + ?: error("Could not find Scene for ID ${sceneNode.value.id}") + + val markdown = loadSceneMarkdownRaw(sceneNode.value, childScenePath) + writeUtf8(markdown) + writeUtf8("\n") + } + } + } + } + + return exportPath.toHPath() + } + + fun loadSceneTree(rootScene: SceneItem): TreeNode { + val sceneDirPath = getSceneDirectory().toOkioPath() + val rootNode = TreeNode(rootScene) + + val childNodes = fileSystem.list(sceneDirPath) + .filterScenePathsOkio() + .map { it.toOkioPath() } + .map { path -> loadSceneTreeNode(path.toHPath()) } + + for (child in childNodes) { + rootNode.addChild(child) + } + + return rootNode + } + + private fun loadSceneTreeNode(root: HPath): TreeNode { + val scene = getSceneFromPath(root) + val node = TreeNode(scene) + + val rootPath = root.toOkioPath() + if (fileSystem.metadata(rootPath).isDirectory) { + val childNodes = fileSystem.list(rootPath) + .filterScenePathsOkio() + .map { path -> loadSceneTreeNode(path) } + + for (child in childNodes) { + node.addChild(child) + } + } + + return node + } + + @Throws(InvalidSceneFilename::class) + fun getSceneFromFilename(path: HPath): SceneItem { + val fileName = getSceneFilename(path) + + val captures = SCENE_FILENAME_PATTERN.matchEntire(fileName) + ?: throw IllegalStateException("Scene filename was bad: $fileName") + + try { + val sceneOrder = captures.groupValues[1].toInt() + val sceneName = captures.groupValues[2] + val sceneId = captures.groupValues[3].toInt() + val isSceneGroup = !(captures.groupValues.size >= 5 + && captures.groupValues[4] == SCENE_FILENAME_EXTENSION) + + val sceneItem = SceneItem( + projectDef = projectDef, + type = if (isSceneGroup) SceneItem.Type.Group else SceneItem.Type.Scene, + id = sceneId, + name = sceneName, + order = sceneOrder, + ) + + return sceneItem + } catch (e: NumberFormatException) { + throw InvalidSceneFilename("Number format exception", fileName) + } catch (e: IllegalStateException) { + throw InvalidSceneFilename("Invalid filename", fileName) + } + } + + private fun getScenePathsFromFilesystem(root: HPath): List { + val scenePaths = fileSystem.list(root.toOkioPath()) + .filterScenePathsOkio() + .map { it.toOkioPath() } + return scenePaths + } + + fun getGroupChildPathsById(root: HPath): Map { + return getScenePathsFromFilesystem(root) + .map { scenePath -> + val sceneId = getSceneIdFromPath(scenePath.toHPath()) + Pair(sceneId, scenePath) + }.associateBy({ it.first }, { it.second.toHPath() }) + } + + fun moveScene(sourcePath: HPath, targetPath: HPath) { + fileSystem.atomicMove(sourcePath.toOkioPath(), targetPath.toOkioPath()) + } + + fun getSceneBufferDirectory(): HPath { + val projOkPath = projectDef.path.toOkioPath() + val sceneDirPath = projOkPath.div(SCENE_DIRECTORY) + val bufferPathSegment = sceneDirPath.div(BUFFER_DIRECTORY) + if (!fileSystem.exists(bufferPathSegment)) { + fileSystem.createDirectory(bufferPathSegment) + } + return bufferPathSegment.toHPath() + } + + fun getSceneFromPath(path: HPath): SceneItem { + val sceneDef = getSceneFromFilename(path) + return sceneDef + } + + fun getSceneFilename(path: HPath) = path.toOkioPath().name + + fun getExportStoryFileName() = "${projectDef.name}.md" + + fun getLastOrderNumber(parentPath: HPath): Int { + val numScenes = fileSystem.list(parentPath.toOkioPath()) + .filterScenePathsOkio() + .count() + return numScenes + } + + fun clearTempScene(sceneItem: SceneItem) { + val path = getSceneBufferTempPath(sceneItem).toOkioPath() + fileSystem.delete(path) + } + + fun loadSceneMarkdownRaw(sceneItem: SceneItem, scenePath: HPath): String { + val content = if (sceneItem.type == SceneItem.Type.Scene) { + try { + fileSystem.read(scenePath.toOkioPath()) { + readUtf8() + } + } catch (e: IOException) { + Napier.e("Failed to load Scene markdown raw (${sceneItem.name})") + "" + } + } else { + "" + } + + return content + } + + suspend fun storeSceneMarkdownRaw(sceneItem: SceneContent, scenePath: HPath): Boolean { + sceneItem.markdown ?: return false + + return try { + fileSystem.write(scenePath.toOkioPath()) { + writeUtf8(sceneItem.markdown) + } + + true + } catch (e: IOException) { + Napier.e("Failed to store Scene markdown raw (${sceneItem.scene.id} - ${sceneItem.scene.name}) because: ${e.message}") + false + } + } + + fun loadSceneBuffer(scenePath: HPath): String { + return try { + fileSystem.read(scenePath.toOkioPath()) { + readUtf8() + } + } catch (e: IOException) { + Napier.e("Failed to load Scene at: $scenePath") + "" + } + } + + fun countScenes(parentPath: HPath): Int { + return fileSystem.list(parentPath.toOkioPath()) + .filterScenePathsOkio() + .count() - 1 + } + + fun storeTempSceneBuffer(buffer: SceneBuffer): Boolean { + val scenePath = getSceneBufferTempPath(buffer.content.scene).toOkioPath() + + return try { + val markdown = buffer.content.coerceMarkdown() + + fileSystem.write(scenePath) { + writeUtf8(markdown) + } + + Napier.d("Stored temp scene: (${buffer.content.scene.name})") + + true + } catch (e: IOException) { + Napier.e("Failed to store temp scene: (${buffer.content.scene.name}) with error: ${e.message}") + false + } + } + + suspend fun storeSceneBuffer(buffer: SceneBuffer, scenePath: HPath): Boolean { + + return try { + val markdown = buffer.content.coerceMarkdown() + + fileSystem.write(scenePath.toOkioPath()) { + writeUtf8(markdown) + } + + true + } catch (e: IOException) { + Napier.e("Failed to store scene: (${buffer.content.scene.name}) with error: ${e.message}") + false + } + } + + fun createNewScene(scenePath: HPath) { + fileSystem.createDirectory(scenePath.toOkioPath(), true) + } + + fun createNewGroup(scenePath: HPath) { + fileSystem.write(scenePath.toOkioPath(), true) { + writeUtf8("") + } + } + + suspend fun deleteScene(scene: SceneItem): Boolean { + val scenePath = resolveScenePathFromFilesystem(scene.id)?.toOkioPath() ?: return false + + return try { + if (!fileSystem.exists(scenePath)) { + Napier.e("Tried to delete Scene, but file did not exist") + false + } else if (!fileSystem.metadata(scenePath).isRegularFile) { + Napier.e("Tried to delete Scene, but file was not File") + false + } else { + fileSystem.delete(scenePath) + true + } + } catch (e: IOException) { + Napier.e("Failed to delete Group ID ${scene.id}: ${e.message}") + false + } + } + + suspend fun deleteGroup(scene: SceneItem): Boolean { + val scenePath = resolveScenePathFromFilesystem(scene.id)?.toOkioPath() ?: return false + return try { + if (!fileSystem.exists(scenePath)) { + Napier.e("Tried to delete Group, but file did not exist") + false + } else if (!fileSystem.metadata(scenePath).isDirectory) { + Napier.e("Tried to delete Group, but file was not Directory") + false + } else if (fileSystem.list(scenePath).isNotEmpty()) { + Napier.w("Tried to delete Group, but was not empty") + false + } else { + fileSystem.delete(scenePath) + true + } + } catch (e: IOException) { + Napier.e("Failed to delete Group ID ${scene.id}: ${e.message}") + false + } + } + companion object { + val SCENE_FILENAME_PATTERN = Regex("""(\d+)-([\d\p{L}+ _']+)-(\d+)(\.md)?(?:\.temp)?""") + val SCENE_BUFFER_FILENAME_PATTERN = Regex("""(\d+)\.md""") + const val SCENE_FILENAME_EXTENSION = ".md" + const val SCENE_DIRECTORY = "scenes" + const val BUFFER_DIRECTORY = ".buffers" + + fun getSceneIdFromFilename(fileName: String): Int { + val captures = SCENE_FILENAME_PATTERN.matchEntire(fileName) + ?: throw IllegalStateException("Scene filename was bad: $fileName") + try { + val sceneId = captures.groupValues[3].toInt() + return sceneId + } catch (e: NumberFormatException) { + throw InvalidSceneFilename("Number format exception", fileName) + } catch (e: IllegalStateException) { + throw InvalidSceneFilename("Invalid filename", fileName) + } + } + + fun validateSceneFilename(fileName: String): Boolean { + return SCENE_FILENAME_PATTERN.matchEntire(fileName) != null + } + fun getSceneDirectory(projectDef: ProjectDef, fileSystem: FileSystem): HPath { val projOkPath = projectDef.path.toOkioPath() val sceneDirPath = projOkPath.div(SCENE_DIRECTORY) @@ -24,4 +435,18 @@ class SceneDatasource( return sceneDirPath.toHPath() } } -} \ No newline at end of file +} + +fun Collection.filterScenePathsOkio() = + map { it.toHPath() }.filterScenePaths() + +fun Sequence.filterScenePathsOkio() = + map { it.toHPath() }.filterScenePaths() + +fun Collection.filterScenePaths() = filter { + validateSceneFilename(it.name) +}.sortedBy { it.name } + +fun Sequence.filterScenePaths() = filter { + validateSceneFilename(it.name) +}.sortedBy { it.name } diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepository.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepository.kt index 298aef2f..c0b42b4d 100644 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepository.kt +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepository.kt @@ -17,7 +17,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetada import com.darkrockstudios.apps.hammer.common.data.projectsrepository.ProjectsRepository import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.projectsync.toApiType -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository.Companion.validateSceneFilename import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadata import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.tree.ImmutableTree @@ -27,6 +26,8 @@ import com.darkrockstudios.apps.hammer.common.dependencyinjection.ProjectDefScop import com.darkrockstudios.apps.hammer.common.dependencyinjection.injectDefaultDispatcher import com.darkrockstudios.apps.hammer.common.dependencyinjection.injectMainDispatcher import com.darkrockstudios.apps.hammer.common.fileio.HPath +import com.darkrockstudios.apps.hammer.common.fileio.okio.toHPath +import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath import com.darkrockstudios.apps.hammer.common.util.debounceUntilQuiescentBy import com.darkrockstudios.apps.hammer.common.util.numDigits import io.github.aakira.napier.Napier @@ -40,20 +41,23 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import okio.IOException +import okio.Path import org.koin.core.component.KoinComponent import org.koin.core.scope.Scope import org.koin.core.scope.ScopeCallback import kotlin.time.Duration.Companion.milliseconds -abstract class SceneEditorRepository( +class SceneEditorRepository( val projectDef: ProjectDef, - protected val idRepository: IdRepository, - protected val projectSynchronizer: ClientProjectSynchronizer, - protected val metadataRepository: ProjectMetadataDatasource, - protected val metadataDatasource: SceneMetadataDatasource, + private val idRepository: IdRepository, + private val projectSynchronizer: ClientProjectSynchronizer, + private val projectMetadataDatasource: ProjectMetadataDatasource, + private val sceneMetadataDatasource: SceneMetadataDatasource, + private val sceneDatasource: SceneDatasource, ) : ScopeCallback, ProjectScoped, KoinComponent { - final override val projectScope = ProjectDefScope(projectDef) + override val projectScope = ProjectDefScope(projectDef) init { projectScope.scope.registerCallback(this) @@ -77,8 +81,8 @@ abstract class SceneEditorRepository( return metadata.first() } - protected val dispatcherMain by injectMainDispatcher() - protected val dispatcherDefault by injectDefaultDispatcher() + private val dispatcherMain by injectMainDispatcher() + private val dispatcherDefault by injectDefaultDispatcher() private val editorScope = CoroutineScope(dispatcherDefault) private val _contentFlow = MutableSharedFlow( @@ -102,15 +106,13 @@ abstract class SceneEditorRepository( ) val sceneListChannel: SharedFlow = _sceneListChannel - protected val sceneTree = Tree() + private val sceneTree = Tree() val rawTree: Tree get() = sceneTree - protected abstract fun loadSceneTree(): TreeNode - - protected suspend fun markForSynchronization(scene: SceneItem) { + private suspend fun markForSynchronization(scene: SceneItem) { if (projectSynchronizer.isServerSynchronized() && !projectSynchronizer.isEntityDirty(scene.id)) { - val metadata = metadataDatasource.loadMetadata(scene.id) + val metadata = sceneMetadataDatasource.loadMetadata(scene.id) val pathSegments = getPathSegments(scene) val content = loadSceneMarkdownRaw(scene) val hash = EntityHasher.hashScene( @@ -183,7 +185,7 @@ abstract class SceneEditorRepository( } } - protected fun getDirtyBufferIds(): Set = sceneBuffers + private fun getDirtyBufferIds(): Set = sceneBuffers .filter { it.value.dirty } .map { it.key } .toSet() @@ -192,7 +194,7 @@ abstract class SceneEditorRepository( * This needs to be called after instantiation */ suspend fun initializeSceneEditor(): SceneEditorRepository { - val root = loadSceneTree() + val root = sceneDatasource.loadSceneTree(rootScene) sceneTree.setRoot(root) cleanupSceneOrder() @@ -200,7 +202,7 @@ abstract class SceneEditorRepository( idRepository.findNextId() // Load any existing temp scenes into buffers - val tempContent = getSceneTempBufferContents() + val tempContent = sceneDatasource.getSceneTempBufferContents() for (content in tempContent) { val buffer = SceneBuffer(content, true, UpdateSource.Repository) updateSceneBuffer(buffer) @@ -208,7 +210,7 @@ abstract class SceneEditorRepository( reloadScenes() - val newMetadata = metadataRepository.loadMetadata(projectDef) + val newMetadata = projectMetadataDatasource.loadMetadata(projectDef) metadata.emit(newMetadata) contentUpdateJob = editorScope.launch { @@ -223,57 +225,6 @@ abstract class SceneEditorRepository( return this } - abstract fun getSceneFilename(path: HPath): String - abstract fun getScenePathSegments(path: HPath): ScenePathSegments - abstract fun getSceneFilePath(sceneId: Int): HPath - abstract fun getSceneDirectory(): HPath - abstract fun getSceneBufferDirectory(): HPath - abstract fun getSceneFilePath(sceneItem: SceneItem, isNewScene: Boolean = false): HPath - abstract fun getSceneBufferTempPath(sceneItem: SceneItem): HPath - abstract suspend fun createScene( - parent: SceneItem?, - sceneName: String, - forceId: Int? = null, - forceOrder: Int? = null - ): SceneItem? - - abstract suspend fun createGroup( - parent: SceneItem?, - groupName: String, - forceId: Int? = null, - forceOrder: Int? = null - ): SceneItem? - - abstract suspend fun deleteScene(scene: SceneItem): Boolean - abstract suspend fun deleteGroup(scene: SceneItem): Boolean - abstract fun getScenes(): List - abstract fun getSceneTree(): ImmutableTree - abstract fun getSceneTempBufferContents(): List - abstract fun getSceneFromPath(path: HPath): SceneItem - abstract fun exportStory(path: HPath): HPath - abstract fun getExportStoryFileName(): String - - /** - * This should only be used for stats and other fire and forget actions where accuracy - * and integrity of the data is not important. - * Anything that wishes to interact with scene content should use `loadSceneBuffer` - * instead. - */ - abstract fun loadSceneMarkdownRaw( - sceneItem: SceneItem, - scenePath: HPath = getSceneFilePath(sceneItem) - ): String - - /** - * This should only be used for server syncing - */ - abstract suspend fun storeSceneMarkdownRaw( - sceneItem: SceneContent, - scenePath: HPath = getSceneFilePath(sceneItem.scene) - ): Boolean - - abstract fun getPathFromFilesystem(sceneItem: SceneItem): HPath? - /** * This should only be used for server syncing */ @@ -281,16 +232,6 @@ abstract class SceneEditorRepository( reloadScenes() } - abstract fun loadSceneBuffer(sceneItem: SceneItem): SceneBuffer - abstract suspend fun storeSceneBuffer(sceneItem: SceneItem): Boolean - abstract fun storeTempSceneBuffer(sceneItem: SceneItem): Boolean - abstract fun clearTempScene(sceneItem: SceneItem) - abstract fun getLastOrderNumber(parentId: Int?): Int - abstract fun getLastOrderNumber(parentPath: HPath): Int - abstract suspend fun updateSceneOrder(parentId: Int) - abstract suspend fun moveScene(moveRequest: MoveRequest) - abstract suspend fun renameScene(sceneItem: SceneItem, newName: String): Boolean - fun getSceneSummaries(): SceneSummary { return SceneSummary( getSceneTree(), @@ -322,7 +263,7 @@ abstract class SceneEditorRepository( } } - protected fun updateSceneBuffer(newBuffer: SceneBuffer) { + private fun updateSceneBuffer(newBuffer: SceneBuffer) { sceneBuffers[newBuffer.content.scene.id] = newBuffer _bufferUpdateFlow.tryEmit(newBuffer) } @@ -330,15 +271,12 @@ abstract class SceneEditorRepository( fun getSceneBuffer(sceneDef: SceneItem): SceneBuffer? = getSceneBuffer(sceneDef.id) fun getSceneBuffer(sceneId: Int): SceneBuffer? = sceneBuffers[sceneId] - protected fun hasSceneBuffer(sceneDef: SceneItem): Boolean = + private fun hasSceneBuffer(sceneDef: SceneItem): Boolean = hasSceneBuffer(sceneDef.id) - fun hasSceneBuffer(sceneId: Int): Boolean = + private fun hasSceneBuffer(sceneId: Int): Boolean = sceneBuffers.containsKey(sceneId) - protected fun hasDirtyBuffer(sceneDef: SceneItem): Boolean = - hasDirtyBuffer(sceneDef.id) - fun hasDirtyBuffer(sceneId: Int): Boolean = getSceneBuffer(sceneId)?.dirty == true @@ -364,7 +302,7 @@ abstract class SceneEditorRepository( return lastOrder.numDigits() < (lastOrder + 1).numDigits() } - fun getSceneFileName( + private fun getSceneFileName( sceneDef: SceneItem, isNewScene: Boolean = false ): String { @@ -377,9 +315,9 @@ abstract class SceneEditorRepository( val parentPath = getSceneFilePath(parentId) val orderDigits = if (isNewScene && willNextSceneIncreaseMagnitude(parentId)) { - getLastOrderNumber(parentPath).numDigits() + 1 + sceneDatasource.getLastOrderNumber(parentPath).numDigits() + 1 } else { - getLastOrderNumber(parentPath).numDigits() + sceneDatasource.getLastOrderNumber(parentPath).numDigits() } val order = sceneDef.order.toString().padStart(orderDigits, '0') @@ -393,64 +331,11 @@ abstract class SceneEditorRepository( return filename } - fun getSceneTempFileName(sceneDef: SceneItem): String { - return "${sceneDef.id}.md" - } - - fun getSceneIdFromBufferFilename(fileName: String): Int { - val captures = SCENE_BUFFER_FILENAME_PATTERN.matchEntire(fileName) - ?: throw IllegalStateException("Scene filename was bad: $fileName") - - try { - val sceneId = captures.groupValues[1].toInt() - return sceneId - } catch (e: NumberFormatException) { - throw InvalidSceneBufferFilename("Number format exception", fileName) - } catch (e: IllegalStateException) { - throw InvalidSceneBufferFilename("Invalid filename", fileName) - } - } - - fun getSceneIdFromPath(path: HPath): Int { - val fileName = getSceneFilename(path) - return getSceneIdFromFilename(fileName) - } - - @Throws(InvalidSceneFilename::class) - fun getSceneFromFilename(path: HPath): SceneItem { - val fileName = getSceneFilename(path) - - val captures = SCENE_FILENAME_PATTERN.matchEntire(fileName) - ?: throw IllegalStateException("Scene filename was bad: $fileName") - - try { - val sceneOrder = captures.groupValues[1].toInt() - val sceneName = captures.groupValues[2] - val sceneId = captures.groupValues[3].toInt() - val isSceneGroup = !(captures.groupValues.size >= 5 - && captures.groupValues[4] == SCENE_FILENAME_EXTENSION) - - val sceneItem = SceneItem( - projectDef = projectDef, - type = if (isSceneGroup) SceneItem.Type.Group else SceneItem.Type.Scene, - id = sceneId, - name = sceneName, - order = sceneOrder, - ) - - return sceneItem - } catch (e: NumberFormatException) { - throw InvalidSceneFilename("Number format exception", fileName) - } catch (e: IllegalStateException) { - throw InvalidSceneFilename("Invalid filename", fileName) - } - } - fun getSceneItemFromId(id: Int): SceneItem? { return sceneTree.findValueOrNull { it.id == id } } - protected fun getSceneNodeFromId(id: Int): TreeNode? { + private fun getSceneNodeFromId(id: Int): TreeNode? { return sceneTree.findOrNull { it.id == id } } @@ -468,7 +353,7 @@ abstract class SceneEditorRepository( } editorScope.cancel("Editor Closed") // During a proper shutdown, we clear any remaining temp buffers that haven't been saved yet - getSceneTempBufferContents().forEach { + sceneDatasource.getSceneTempBufferContents().forEach { clearTempScene(it.scene) } Napier.i("SceneEditorRepository Closed.") @@ -479,64 +364,606 @@ abstract class SceneEditorRepository( return getScenePathSegments(hpath).pathSegments } - abstract fun rationalizeTree() - abstract fun reIdScene(oldId: Int, newId: Int) + fun reIdScene(oldId: Int, newId: Int) { + val oldPath = getSceneFilePath(oldId) + + val oldScene = getSceneItemFromId(oldId) ?: throw IOException("Scene $oldId does not exist") + val newScene = oldScene.copy(id = newId) + val newFileName = getSceneFileName(newScene) + val parent = oldPath.toOkioPath().parent ?: error("Scene ID $oldId path had not parent") + val newPath = (parent / newFileName).toHPath() + + sceneDatasource.moveScene(oldPath, newPath) + + sceneMetadataDatasource.reIdSceneMetadata(oldId = oldId, newId = newId) + + // Update the in-tree representation + val node = getSceneNodeFromId(oldId) ?: error("reIdScene: Failed to get node for ID $oldId") + node.value = node.value.copy( + id = newId + ) + } + + fun getScenePathSegments(path: HPath): ScenePathSegments { + val parentPath = path.toOkioPath() + + val sceneDir = sceneDatasource.getSceneDirectory().toOkioPath() + return if (parentPath != sceneDir) { + val sceneId = sceneDatasource.getSceneIdFromPath(path) + val parentScenes = sceneTree.getBranch(true) { it.id == sceneId } + .map { it.value.id } + ScenePathSegments(pathSegments = parentScenes) + } else { + ScenePathSegments(pathSegments = emptyList()) + } + } suspend fun loadSceneMetadata(sceneId: Int): SceneMetadata { - return metadataDatasource.loadMetadata(sceneId) ?: SceneMetadata() + return sceneMetadataDatasource.loadMetadata(sceneId) ?: SceneMetadata() } suspend fun storeMetadata(metadata: SceneMetadata, sceneId: Int) { val scene = getSceneItemFromId(sceneId) ?: error("storeMetadata: Failed to load scene for id: $sceneId ") - metadataDatasource.storeMetadata(metadata, sceneId) + sceneMetadataDatasource.storeMetadata(metadata, sceneId) markForSynchronization(scene) } - companion object { - val SCENE_FILENAME_PATTERN = Regex("""(\d+)-([\d\p{L}+ _']+)-(\d+)(\.md)?(?:\.temp)?""") - val SCENE_BUFFER_FILENAME_PATTERN = Regex("""(\d+)\.md""") - const val SCENE_FILENAME_EXTENSION = ".md" - const val SCENE_DIRECTORY = "scenes" - const val BUFFER_DIRECTORY = ".buffers" - val BUFFER_COOL_DOWN = 500.milliseconds + fun getSceneFilePath(sceneItem: SceneItem, isNewScene: Boolean = false): HPath { + val scenePathSegment = getSceneDirectory().toOkioPath() + + val pathSegments: MutableList = sceneTree + .getBranch(true) { it.id == sceneItem.id } + .map { node -> node.value } + .filter { scene -> !scene.isRootScene } + .map { scene -> getSceneFileName(scene) } + .toMutableList() + + pathSegments.add(getSceneFileName(sceneItem, isNewScene)) + + var fullPath: Path = scenePathSegment + pathSegments.forEach { segment -> + fullPath = fullPath.div(segment) + } + + return fullPath.toHPath() + } + + fun getSceneFilePath(sceneId: Int): HPath { + val scenePathSegment = getSceneDirectory().toOkioPath() + + val branch = sceneTree.getBranch { it.id == sceneId } + val pathSegments = branch + .map { node -> node.value } + .filter { sceneItem -> !sceneItem.isRootScene } + .map { sceneItem -> getSceneFileName(sceneItem) } + + var fullPath: Path = scenePathSegment + pathSegments.forEach { segment -> + fullPath = fullPath.div(segment) + } + + return fullPath.toHPath() + } - fun getSceneIdFromFilename(fileName: String): Int { - val captures = SCENE_FILENAME_PATTERN.matchEntire(fileName) - ?: throw IllegalStateException("Scene filename was bad: $fileName") - try { - val sceneId = captures.groupValues[3].toInt() - return sceneId - } catch (e: NumberFormatException) { - throw InvalidSceneFilename("Number format exception", fileName) - } catch (e: IllegalStateException) { - throw InvalidSceneFilename("Invalid filename", fileName) + suspend fun renameScene(sceneItem: SceneItem, newName: String): Boolean { + if (validateSceneName(newName).isFailure) return false + + val cleanedNamed = newName.trim() + + val oldPath = getSceneFilePath(sceneItem) + val newDef = sceneItem.copy(name = cleanedNamed) + + val newPath = getSceneFilePath(newDef) + + sceneDatasource.moveScene(oldPath, newPath) + + val node = getSceneNodeFromId(sceneItem.id) + ?: throw IllegalStateException("Failed to get scene for renaming: ${sceneItem.id}") + node.value = newDef + + markForSynchronization(sceneItem) + + reloadScenes() + return true + } + + private suspend fun updateSceneOrderMagnitudeOnly(parentId: Int) { + Napier.d("updateSceneOrderMagnitudeOnly for parentId: $parentId") + + val parent = sceneTree.find { it.id == parentId } + if (parent.value.type == SceneItem.Type.Scene) throw IllegalArgumentException("SceneItem must be Root or Group") + + val parentPath = getSceneFilePath(parent.value.id) + val existingSceneFiles = sceneDatasource.getGroupChildPathsById(parentPath) + + parent.children().forEach { childNode -> + val existingPath = existingSceneFiles[childNode.value.id] + ?: throw IllegalStateException("Scene wasn't present in directory") + val newPath = getSceneFilePath(childNode.value.id) + + if (existingPath != newPath) { + try { + Napier.d("Renaming from: \"${existingPath.name}\" to: \"${newPath.name}\"") + + sceneDatasource.moveScene(sourcePath = existingPath, targetPath = newPath) + } catch (e: IOException) { + throw IOException("existingPath: $existingPath\nnewPath: $newPath\n${e}\n${e.message}") + } } } + } - fun validateSceneFilename(fileName: String): Boolean { - return SCENE_FILENAME_PATTERN.matchEntire(fileName) != null + private suspend fun markForSynchronization(scene: SceneItem, content: String) { + if (projectSynchronizer.isServerSynchronized() && !projectSynchronizer.isEntityDirty(scene.id)) { + val metadata = sceneMetadataDatasource.loadMetadata(scene.id) + val pathSegments = getPathSegments(scene) + val hash = EntityHasher.hashScene( + id = scene.id, + order = scene.order, + path = pathSegments, + name = scene.name, + type = scene.type.toApiType(), + content = content, + outline = metadata?.outline ?: "", + notes = metadata?.notes ?: "", + ) + projectSynchronizer.markEntityAsDirty(scene.id, hash) } } - abstract suspend fun updateSceneOrderMagnitudeOnly(parentId: Int) - abstract fun resolveScenePathFromFilesystem(id: Int): HPath? -} + suspend fun moveScene(moveRequest: MoveRequest) { + val fromNode = sceneTree.find { it.id == moveRequest.id } + val fromParentNode = fromNode.parent + ?: throw IllegalStateException("Item had no parent") + + val toParentNode = sceneTree[moveRequest.toPosition.coords.parentIndex] -fun Collection.filterScenePaths() = filter { - validateSceneFilename(it.name) -}.sortedBy { it.name } + val isMovingParents = (fromParentNode != toParentNode) -fun Sequence.filterScenePaths() = filter { - validateSceneFilename(it.name) -}.sortedBy { it.name } + markForSynchronization(fromNode.value) -open class InvalidSceneFilename(message: String, fileName: String) : - IllegalStateException("$fileName failed to parse because: $message") + // Perform move inside tree + updateSceneTreeForMove(moveRequest) -class InvalidSceneBufferFilename(message: String, fileName: String) : - InvalidSceneFilename(message, fileName) + // Moving from one parent to another + if (isMovingParents) { + // Move the file to its new parent + val toPath = getSceneFilePath(moveRequest.id) -inline fun Tree.findById(scene: SceneItem): TreeNode = findById(scene.id) -inline fun Tree.findById(id: Int): TreeNode = find { it.id == id } \ No newline at end of file + val fromParentPath = getSceneFilePath(fromParentNode.value.id) + val originalFromParentScenePaths = + sceneDatasource.getGroupChildPathsById(fromParentPath) + val originalFromNodePath = originalFromParentScenePaths[fromNode.value.id] + ?: throw IllegalStateException("From node wasn't where it's supposed to be") + + sceneDatasource.moveScene(originalFromNodePath, toPath) + + // Update new parents children + updateSceneOrder(toParentNode.value.id) + + // Update original parents children + updateSceneOrder(fromParentNode.value.id) + } + // Moving inside same parent + else { + updateSceneOrder(toParentNode.value.id) + } + + // Notify listeners of the new state of the tree + val imTree = sceneTree.toImmutableTree() + + val newSummary = SceneSummary( + imTree, + getDirtyBufferIds() + ) + reloadScenes(newSummary) + } + + private suspend fun updateSceneTreeForMove(moveRequest: MoveRequest) { + val fromNode = sceneTree.find { it.id == moveRequest.id } + val toParentNode = sceneTree[moveRequest.toPosition.coords.parentIndex] + val insertIndex = moveRequest.toPosition.coords.childLocalIndex + + Napier.d("Move Scene Item: $moveRequest") + + val fromParent = fromNode.parent + val fromIndex = fromParent?.localIndexOf(fromNode) ?: -1 + val changingParents = (toParentNode != fromParent) + + val finalIndex = if (toParentNode.numChildrenImmedate() == 0) { + 0 + } else { + if (!changingParents) { + if (fromIndex <= insertIndex) { + if (moveRequest.toPosition.before) { + (insertIndex - 1).coerceAtLeast(0) + } else { + insertIndex + } + } else { + if (moveRequest.toPosition.before) { + insertIndex + } else { + insertIndex + 1 + } + } + } else { + if (moveRequest.toPosition.before) { + insertIndex + } else { + insertIndex + 1 + } + } + } + + markForSynchronization(fromNode.value) + + toParentNode.insertChild(finalIndex, fromNode) + + /* + // Move debugging + println("Before Move:") + sceneTree.print() + + println("After Move:") + sceneTree.print() + */ + } + + fun getSceneDirectory() = sceneDatasource.getSceneDirectory() + + private suspend fun updateSceneOrder(parentId: Int) { + val parent = sceneTree.find { it.id == parentId } + if (parent.value.type == SceneItem.Type.Scene) throw IllegalArgumentException("SceneItem must be Root or Group") + + val parentPath = getSceneFilePath(parent.value.id) + val existingSceneFiles = sceneDatasource.getGroupChildPathsById(parentPath) + + // Must grab a copy of the children before they are modified + // we'll need this if we need to calculate their original hash + // down below for markForSynchronization() + val originalChildren = if (projectSynchronizer.isServerSynchronized()) { + parent.children().map { child -> child.value.copy() } + } else { + null + } + + parent.children().forEachIndexed { index, childNode -> + childNode.value = childNode.value.copy(order = index) + + val existingPath = existingSceneFiles[childNode.value.id] + ?: throw IllegalStateException("Scene wasn't present in directory") + val newPath = getSceneFilePath(childNode.value.id) + + if (existingPath != newPath) { + try { + originalChildren?.find { it.id == childNode.value.id }?.let { originalChild -> + val realPath = sceneDatasource.getPathFromFilesystem(childNode.value) + ?: throw IllegalStateException("Could not find Scene on filesystem: ${childNode.value.id}") + + val content = loadSceneMarkdownRaw(childNode.value, realPath) + markForSynchronization(originalChild, content) + } + sceneDatasource.moveScene(sourcePath = existingPath, targetPath = newPath) + } catch (e: IOException) { + throw IOException("existingPath: $existingPath\nnewPath: $newPath\n${e}\n${e.message}") + } + } + } + } + + // Used after a server sync + private fun correctSceneOrders() { + correctSceneOrders(sceneTree.root()) + } + + /** + * Walks the scene tree and makes the order of the children + * in the tree match their internal `order` property. + * + * This is only used when server syncing has changed orders. + */ + private fun correctSceneOrders(node: TreeNode) { + val children = node.children() + val sortedChildren = children.sortedBy { it.value.order } + + for (i in children.indices) { + val child = children.first() + node.removeChild(child) + } + + sortedChildren.forEach { child -> node.addChild(child) } + + children.forEach { child -> + if (child.numChildrenImmedate() > 0) { + correctSceneOrders(child) + } + } + } + + /** + * This looks at the in-memory tree and checks it against the filesystem. + * Any discrepancies it finds on the filesystem will be corrected so that + * it matches the tree. + */ + fun rationalizeTree() { + correctSceneOrders() + + sceneTree.forEach { node -> + if (node.value.type == SceneItem.Type.Root) return@forEach + + val intendedPath = getSceneFilePath(node.value.id) + + val allPaths = sceneDatasource.getAllScenePaths() + val realPath = allPaths.find { path -> + val scene = sceneDatasource.getSceneFromPath(path) + scene.id == node.value.id + } + + if (realPath != null) { + if (realPath != intendedPath) { + Napier.i { "Moving scene to new path: ${intendedPath.path} from old path: ${realPath.path}" } + sceneDatasource.moveScene(realPath, intendedPath) + } else { + // Too chatty, don't need it + //Napier.d { "Scene ${node.value.id} is in the correct location" } + } + } else { + Napier.e { "Scene ${node.value.id} is missing from the filesystem" } + } + } + } + + fun exportStory(path: HPath): HPath { + return sceneDatasource.exportStory(path, getSceneTree().root.children) + } + + suspend fun createScene( + parent: SceneItem?, + sceneName: String, + forceId: Int? = null, + forceOrder: Int? = null, + ): SceneItem? { + return createSceneItem(parent, sceneName, false, forceId, forceOrder) + } + + suspend fun createGroup( + parent: SceneItem?, + groupName: String, + forceId: Int? = null, + forceOrder: Int? = null, + ): SceneItem? { + return createSceneItem(parent, groupName, true, forceId, forceOrder) + } + + private suspend fun createSceneItem( + parent: SceneItem?, + name: String, + isGroup: Boolean, + forceId: Int?, + forceOrder: Int?, + ): SceneItem? { + val cleanedNamed = name.trim() + + return if (validateSceneName(cleanedNamed).isFailure) { + Napier.d("Invalid scene name") + null + } else { + val lastOrder = getLastOrderNumber(parent?.id) + val nextOrder = forceOrder ?: (lastOrder + 1) + val sceneId = forceId ?: idRepository.claimNextId() + val type = if (isGroup) SceneItem.Type.Group else SceneItem.Type.Scene + + val newSceneItem = SceneItem( + projectDef = projectDef, + type = type, + id = sceneId, + name = cleanedNamed, + order = nextOrder, + ) + + val newTreeNode = TreeNode(newSceneItem) + if (parent != null) { + val parentNode = sceneTree.find { it.id == parent.id } + parentNode.addChild(newTreeNode) + } else { + sceneTree.addChild(newTreeNode) + } + + val scenePath = getSceneFilePath(newSceneItem, true) + when (type) { + SceneItem.Type.Scene -> sceneDatasource.createNewGroup(scenePath) + SceneItem.Type.Group -> sceneDatasource.createNewScene(scenePath) + SceneItem.Type.Root -> throw IllegalArgumentException("Cannot create Root") + } + + // Correct order digit paddings when injecting a new scene/group + if (forceOrder != null) { + updateSceneOrderMagnitudeOnly(parent?.id ?: SceneItem.ROOT_ID) + } + // If we need to increase the padding digits, update the file names + else if (lastOrder.numDigits() < nextOrder.numDigits()) { + updateSceneOrder(parent?.id ?: SceneItem.ROOT_ID) + } + + Napier.i("createScene: $cleanedNamed") + + reloadScenes() + + newSceneItem + } + } + + suspend fun deleteScene(scene: SceneItem): Boolean { + val deleted = sceneDatasource.deleteScene(scene) + + return if (deleted) { + val sceneNode = getSceneNodeFromId(scene.id) + + val parent = sceneNode?.parent + if (parent != null) { + val parentId: Int = parent.value.id + parent.removeChild(sceneNode) + + updateSceneOrder(parentId) + Napier.w("Scene ${scene.id} deleted") + + if (projectSynchronizer.isServerSynchronized()) { + projectSynchronizer.recordIdDeletion(scene.id) + } + + reloadScenes() + + true + } else { + Napier.w("Partially failed to delete scene ${scene.id}") + false + } + } else { + deleted + } + } + + suspend fun deleteGroup(scene: SceneItem): Boolean { + val deleted = sceneDatasource.deleteGroup(scene) + + return if (deleted) { + if (projectSynchronizer.isServerSynchronized()) { + projectSynchronizer.recordIdDeletion(scene.id) + } + + val sceneNode = getSceneNodeFromId(scene.id) + + val parent = sceneNode?.parent + if (parent != null) { + val parentId: Int = parent.value.id + parent.removeChild(sceneNode) + + updateSceneOrder(parentId) + Napier.w("Group ${scene.id} deleted") + + reloadScenes() + + true + } else { + Napier.w("Failed to delete group ${scene.id}") + false + } + } else { + deleted + } + } + + fun getScenes(): List = sceneDatasource.getAllScenes() + + fun getSceneTree(): ImmutableTree { + return sceneTree.toImmutableTree() + } + + /** + * This should only be used for stats and other fire and forget actions where accuracy + * and integrity of the data is not important. + * Anything that wishes to interact with scene content should use `loadSceneBuffer` + * instead. + */ + fun loadSceneMarkdownRaw( + sceneItem: SceneItem, + scenePath: HPath = getSceneFilePath(sceneItem) + ): String = + sceneDatasource.loadSceneMarkdownRaw(sceneItem, scenePath) + + /** + * This should only be used for server syncing + */ + suspend fun storeSceneMarkdownRaw( + sceneItem: SceneContent, + scenePath: HPath = getSceneFilePath(sceneItem.scene) + ): Boolean { + val success = sceneDatasource.storeSceneMarkdownRaw(sceneItem, scenePath) + + if (success) { + markForSynchronization(sceneItem.scene) + } + + return success + } + + fun loadSceneBuffer(sceneItem: SceneItem): SceneBuffer { + val cachedBuffer = getSceneBuffer(sceneItem) + return if (cachedBuffer != null) { + cachedBuffer + } else { + val scenePath = getSceneFilePath(sceneItem) + val content = sceneDatasource.loadSceneBuffer(scenePath) + val newBuffer = SceneBuffer( + SceneContent(sceneItem, content), + source = UpdateSource.Repository + ) + + updateSceneBuffer(newBuffer) + + newBuffer + } + } + + suspend fun storeSceneBuffer(sceneItem: SceneItem): Boolean { + val buffer = getSceneBuffer(sceneItem) + if (buffer == null) { + Napier.e { "Failed to store scene: ${sceneItem.id} - ${sceneItem.name}, no buffer present" } + return false + } + val scenePath = getSceneFilePath(sceneItem) + val success = sceneDatasource.storeSceneBuffer(buffer, scenePath) + + if (success) { + markForSynchronization(sceneItem) + + val cleanBuffer = buffer.copy(dirty = false) + updateSceneBuffer(cleanBuffer) + + clearTempScene(sceneItem) + } + + return success + } + + private suspend fun storeTempSceneBuffer(sceneItem: SceneItem): Boolean { + val buffer = getSceneBuffer(sceneItem) + if (buffer == null) { + Napier.e { "Failed to store scene: ${sceneItem.id} - ${sceneItem.name}, no buffer present" } + return false + } + return sceneDatasource.storeTempSceneBuffer(buffer) + } + + private fun clearTempScene(sceneItem: SceneItem) = sceneDatasource.clearTempScene(sceneItem) + + private fun getLastOrderNumber(parentId: Int?): Int { + val parentPath: HPath = if (parentId != null && parentId != 0) { + val parentItem = + getSceneItemFromId(parentId) ?: throw IllegalStateException("Parent not found") + + getSceneFilePath(parentItem) + } else { + getSceneDirectory() + } + + val numScenes = sceneDatasource.countScenes(parentPath) + return numScenes + } + + fun getExportStoryFileName() = sceneDatasource.getExportStoryFileName() + fun getSceneFilename(path: HPath) = sceneDatasource.getSceneFilename(path) + + /** + * This is much slower than using the Scene Tree, but some times you need it. + * It goes right to the source of truth, the disk. + */ + fun resolveScenePathFromFilesystem(id: Int) = sceneDatasource.resolveScenePathFromFilesystem(id) + + companion object { + val BUFFER_COOL_DOWN = 500.milliseconds + } +} \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepositoryOkio.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepositoryOkio.kt deleted file mode 100644 index 12a0bbbb..00000000 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneEditorRepositoryOkio.kt +++ /dev/null @@ -1,865 +0,0 @@ -package com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository - -import com.darkrockstudios.apps.hammer.base.http.synchronizer.EntityHasher -import com.darkrockstudios.apps.hammer.common.data.MoveRequest -import com.darkrockstudios.apps.hammer.common.data.ProjectDef -import com.darkrockstudios.apps.hammer.common.data.SceneBuffer -import com.darkrockstudios.apps.hammer.common.data.SceneContent -import com.darkrockstudios.apps.hammer.common.data.SceneItem -import com.darkrockstudios.apps.hammer.common.data.ScenePathSegments -import com.darkrockstudios.apps.hammer.common.data.SceneSummary -import com.darkrockstudios.apps.hammer.common.data.UpdateSource -import com.darkrockstudios.apps.hammer.common.data.id.IdRepository -import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetadataDatasource -import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer -import com.darkrockstudios.apps.hammer.common.data.projectsync.toApiType -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource -import com.darkrockstudios.apps.hammer.common.data.tree.ImmutableTree -import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode -import com.darkrockstudios.apps.hammer.common.fileio.HPath -import com.darkrockstudios.apps.hammer.common.fileio.okio.toHPath -import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath -import com.darkrockstudios.apps.hammer.common.util.numDigits -import io.github.aakira.napier.Napier -import okio.FileSystem -import okio.IOException -import okio.Path - -class SceneEditorRepositoryOkio( - projectDef: ProjectDef, - idRepository: IdRepository, - projectSynchronizer: ClientProjectSynchronizer, - projectMetadataDatasource: ProjectMetadataDatasource, - sceneMetadataDatasource: SceneMetadataDatasource, - private val sceneDatasource: SceneDatasource, - private val fileSystem: FileSystem, -) : SceneEditorRepository( - projectDef, - idRepository, - projectSynchronizer, - projectMetadataDatasource, - sceneMetadataDatasource -) { - - override fun getSceneFilename(path: HPath) = path.toOkioPath().name - - override fun getScenePathSegments(path: HPath): ScenePathSegments { - val parentPath = path.toOkioPath() - - val sceneDir = getSceneDirectory().toOkioPath() - return if (parentPath != sceneDir) { - val sceneId = getSceneIdFromPath(path) - val parentScenes = sceneTree.getBranch(true) { it.id == sceneId } - .map { it.value.id } - ScenePathSegments(pathSegments = parentScenes) - } else { - ScenePathSegments(pathSegments = emptyList()) - } - } - - // Used after a server sync - private fun correctSceneOrders() { - correctSceneOrders(sceneTree.root()) - } - - /** - * Walks the scene tree and makes the order of the children - * in the tree match their internal `order` property. - * - * This is only used when server syncing has changed orders. - */ - private fun correctSceneOrders(node: TreeNode) { - val children = node.children() - val sortedChildren = children.sortedBy { it.value.order } - - for (i in children.indices) { - val child = children.first() - node.removeChild(child) - } - - sortedChildren.forEach { child -> node.addChild(child) } - - children.forEach { child -> - if (child.numChildrenImmedate() > 0) { - correctSceneOrders(child) - } - } - } - - /** - * This looks at the in-memory tree and checks it against the filesystem. - * Any discrepancies it finds on the filesystem will be corrected so that - * it matches the tree. - */ - override fun rationalizeTree() { - correctSceneOrders() - - sceneTree.forEach { node -> - if (node.value.type == SceneItem.Type.Root) return@forEach - - val intendedPath = getSceneFilePath(node.value.id) - - val allPaths = getAllScenePathsOkio() - val realPath = allPaths.map { it.toHPath() }.find { path -> - val scene = getSceneFromPath(path) - scene.id == node.value.id - } - - if (realPath != null) { - if (realPath != intendedPath) { - Napier.i { "Moving scene to new path: ${intendedPath.path} from old path: ${realPath.path}" } - fileSystem.atomicMove(realPath.toOkioPath(), intendedPath.toOkioPath()) - } else { - // Too chatty, don't need it - //Napier.d { "Scene ${node.value.id} is in the correct location" } - } - } else { - Napier.e { "Scene ${node.value.id} is missing from the filesystem" } - } - } - } - - override fun reIdScene(oldId: Int, newId: Int) { - val oldPath = getSceneFilePath(oldId) - - val oldScene = getSceneItemFromId(oldId) ?: throw IOException("Scene $oldId does not exist") - val newScene = oldScene.copy(id = newId) - val newFileName = getSceneFileName(newScene) - val parent = oldPath.toOkioPath().parent ?: error("Scene ID $oldId path had not parent") - val newPath = parent / newFileName - - fileSystem.atomicMove(oldPath.toOkioPath(), newPath) - - metadataDatasource.reIdSceneMetadata(oldId, newId) - - // Update the in-tree representation - val node = getSceneNodeFromId(oldId) ?: error("reIdScene: Failed to get node for ID $oldId") - node.value = node.value.copy( - id = newId - ) - } - - override fun getSceneDirectory() = sceneDatasource.getSceneDirectory() - - override fun getSceneBufferDirectory(): HPath { - val projOkPath = projectDef.path.toOkioPath() - val sceneDirPath = projOkPath.div(SCENE_DIRECTORY) - val bufferPathSegment = sceneDirPath.div(BUFFER_DIRECTORY) - if (!fileSystem.exists(bufferPathSegment)) { - fileSystem.createDirectory(bufferPathSegment) - } - return bufferPathSegment.toHPath() - } - - override fun getSceneFilePath(sceneItem: SceneItem, isNewScene: Boolean): HPath { - val scenePathSegment = getSceneDirectory().toOkioPath() - - val pathSegments: MutableList = sceneTree - .getBranch(true) { it.id == sceneItem.id } - .map { node -> node.value } - .filter { scene -> !scene.isRootScene } - .map { scene -> getSceneFileName(scene) } - .toMutableList() - - pathSegments.add(getSceneFileName(sceneItem, isNewScene)) - - var fullPath: Path = scenePathSegment - pathSegments.forEach { segment -> - fullPath = fullPath.div(segment) - } - - return fullPath.toHPath() - } - - override fun getSceneFilePath(sceneId: Int): HPath { - val scenePathSegment = getSceneDirectory().toOkioPath() - - val branch = sceneTree.getBranch { it.id == sceneId } - val pathSegments = branch - .map { node -> node.value } - .filter { sceneItem -> !sceneItem.isRootScene } - .map { sceneItem -> getSceneFileName(sceneItem) } - - var fullPath: Path = scenePathSegment - pathSegments.forEach { segment -> - fullPath = fullPath.div(segment) - } - - return fullPath.toHPath() - } - - override fun getSceneBufferTempPath(sceneItem: SceneItem): HPath { - val bufferPathSegment = getSceneBufferDirectory().toOkioPath() - val fileName = getSceneTempFileName(sceneItem) - return bufferPathSegment.div(fileName).toHPath() - } - - override fun getSceneFromPath(path: HPath): SceneItem { - val sceneDef = getSceneFromFilename(path) - return sceneDef - } - - override fun exportStory(path: HPath): HPath { - val exportPath = path.toOkioPath() / getExportStoryFileName() - - fileSystem.write(exportPath) { - writeUtf8("# ${projectDef.name}\n\n") - - getSceneTree().root.children.forEachIndexed { index, chapterNode -> - val scene = chapterNode.value - - val chapterNumber = index + 1 - - writeUtf8("\n## $chapterNumber. ${scene.name}\n\n") - - if (scene.type == SceneItem.Type.Scene) { - val markdown = loadSceneMarkdownRaw(chapterNode.value) - writeUtf8(markdown) - writeUtf8("\n") - } else { - chapterNode.filter { it.value.type == SceneItem.Type.Scene } - .forEach { sceneNode -> - val markdown = loadSceneMarkdownRaw(sceneNode.value) - writeUtf8(markdown) - writeUtf8("\n") - } - } - } - } - - return exportPath.toHPath() - } - - override fun getExportStoryFileName() = "${projectDef.name}.md" - - override fun loadSceneTree(): TreeNode { - val sceneDirPath = getSceneDirectory().toOkioPath() - val rootNode = TreeNode(rootScene) - - val childNodes = fileSystem.list(sceneDirPath) - .filterScenePathsOkio() - .map { it.toOkioPath() } - .map { path -> loadSceneTreeNode(path) } - - for (child in childNodes) { - rootNode.addChild(child) - } - - return rootNode - } - - private fun loadSceneTreeNode(root: Path): TreeNode { - val scene = getSceneFromPath(root.toHPath()) - val node = TreeNode(scene) - - if (fileSystem.metadata(root).isDirectory) { - val childNodes = fileSystem.list(root) - .filterScenePathsOkio() - .map { path -> loadSceneTreeNode(path.toOkioPath()) } - - for (child in childNodes) { - node.addChild(child) - } - } - - return node - } - - private fun getAllScenePathsOkio(): List { - val sceneDirPath = getSceneDirectory().toOkioPath() - val scenePaths = fileSystem.listRecursively(sceneDirPath) - .toList() - .filterScenePathsOkio() - .sortedBy { it.name } - .map { it.toOkioPath() } - return scenePaths - } - - override fun resolveScenePathFromFilesystem(id: Int): HPath? { - return getAllScenePathsOkio() - .map { it.toHPath() } - .find { path -> getSceneIdFromPath(path) == id } - } - - private fun getScenePathsOkio(root: Path): List { - val scenePaths = fileSystem.list(root) - .filterScenePathsOkio() - .map { it.toOkioPath() } - return scenePaths - } - - private fun getGroupChildPathsById(root: Path): Map { - return getScenePathsOkio(root) - .map { scenePath -> - val sceneId = getSceneIdFromPath(scenePath.toHPath()) - Pair(sceneId, scenePath) - }.associateBy({ it.first }, { it.second }) - } - - fun getIndex(node: TreeNode): Int { - return sceneTree.indexOf(node) - } - - fun getIndex(sceneId: Int): Int { - return sceneTree.indexOfFirst { it.value.id == sceneId } - } - - private suspend fun updateSceneTreeForMove(moveRequest: MoveRequest) { - val fromNode = sceneTree.find { it.id == moveRequest.id } - val toParentNode = sceneTree[moveRequest.toPosition.coords.parentIndex] - val insertIndex = moveRequest.toPosition.coords.childLocalIndex - - Napier.d("Move Scene Item: $moveRequest") - - val fromParent = fromNode.parent - val fromIndex = fromParent?.localIndexOf(fromNode) ?: -1 - val changingParents = (toParentNode != fromParent) - - val finalIndex = if (toParentNode.numChildrenImmedate() == 0) { - 0 - } else { - if (!changingParents) { - if (fromIndex <= insertIndex) { - if (moveRequest.toPosition.before) { - (insertIndex - 1).coerceAtLeast(0) - } else { - insertIndex - } - } else { - if (moveRequest.toPosition.before) { - insertIndex - } else { - insertIndex + 1 - } - } - } else { - if (moveRequest.toPosition.before) { - insertIndex - } else { - insertIndex + 1 - } - } - } - - markForSynchronization(fromNode.value) - - toParentNode.insertChild(finalIndex, fromNode) - - /* - // Move debugging - println("Before Move:") - sceneTree.print() - - println("After Move:") - sceneTree.print() - */ - } - - override suspend fun moveScene(moveRequest: MoveRequest) { - - val fromNode = sceneTree.find { it.id == moveRequest.id } - val fromParentNode = fromNode.parent - ?: throw IllegalStateException("Item had no parent") - - val toParentNode = sceneTree[moveRequest.toPosition.coords.parentIndex] - - val isMovingParents = (fromParentNode != toParentNode) - - markForSynchronization(fromNode.value) - - // Perform move inside tree - updateSceneTreeForMove(moveRequest) - - // Moving from one parent to another - if (isMovingParents) { - // Move the file to its new parent - val toPath = getSceneFilePath(moveRequest.id) - - val fromParentPath = getSceneFilePath(fromParentNode.value.id) - val originalFromParentScenePaths = getGroupChildPathsById(fromParentPath.toOkioPath()) - val originalFromNodePath = originalFromParentScenePaths[fromNode.value.id] - ?: throw IllegalStateException("From node wasn't where it's supposed to be") - - fileSystem.atomicMove( - source = originalFromNodePath, - target = toPath.toOkioPath() - ) - - // Update new parents children - updateSceneOrder(toParentNode.value.id) - - // Update original parents children - updateSceneOrder(fromParentNode.value.id) - } - // Moving inside same parent - else { - updateSceneOrder(toParentNode.value.id) - } - - // Notify listeners of the new state of the tree - val imTree = sceneTree.toImmutableTree() - - val newSummary = SceneSummary( - imTree, - getDirtyBufferIds() - ) - reloadScenes(newSummary) - } - - override suspend fun updateSceneOrder(parentId: Int) { - val parent = sceneTree.find { it.id == parentId } - if (parent.value.type == SceneItem.Type.Scene) throw IllegalArgumentException("SceneItem must be Root or Group") - - val parentPath = getSceneFilePath(parent.value.id) - val existingSceneFiles = getGroupChildPathsById(parentPath.toOkioPath()) - - // Must grab a copy of the children before they are modified - // we'll need this if we need to calculate their original hash - // down below for markForSynchronization() - val originalChildren = if (projectSynchronizer.isServerSynchronized()) { - parent.children().map { child -> child.value.copy() } - } else { - null - } - - parent.children().forEachIndexed { index, childNode -> - childNode.value = childNode.value.copy(order = index) - - val existingPath = existingSceneFiles[childNode.value.id] - ?: throw IllegalStateException("Scene wasn't present in directory") - val newPath = getSceneFilePath(childNode.value.id).toOkioPath() - - if (existingPath != newPath) { - try { - originalChildren?.find { it.id == childNode.value.id }?.let { originalChild -> - val realPath = getPathFromFilesystem(childNode.value) - ?: throw IllegalStateException("Could not find Scene on filesystem: ${childNode.value.id}") - - val content = loadSceneMarkdownRaw(childNode.value, realPath) - markForSynchronization(originalChild, content) - } - fileSystem.atomicMove(source = existingPath, target = newPath) - } catch (e: IOException) { - throw IOException("existingPath: $existingPath\nnewPath: $newPath\n${e}\n${e.message}") - } - } - } - } - - override suspend fun updateSceneOrderMagnitudeOnly(parentId: Int) { - Napier.d("updateSceneOrderMagnitudeOnly for parentId: $parentId") - - val parent = sceneTree.find { it.id == parentId } - if (parent.value.type == SceneItem.Type.Scene) throw IllegalArgumentException("SceneItem must be Root or Group") - - val parentPath = getSceneFilePath(parent.value.id) - val existingSceneFiles = getGroupChildPathsById(parentPath.toOkioPath()) - - parent.children().forEach { childNode -> - val existingPath = existingSceneFiles[childNode.value.id] - ?: throw IllegalStateException("Scene wasn't present in directory") - val newPath = getSceneFilePath(childNode.value.id).toOkioPath() - - if (existingPath != newPath) { - try { - Napier.d("Renaming from: \"${existingPath.name}\" to: \"${newPath.name}\"") - - fileSystem.atomicMove(source = existingPath, target = newPath) - } catch (e: IOException) { - throw IOException("existingPath: $existingPath\nnewPath: $newPath\n${e}\n${e.message}") - } - } - } - } - - private suspend fun markForSynchronization(scene: SceneItem, content: String) { - if (projectSynchronizer.isServerSynchronized() && !projectSynchronizer.isEntityDirty(scene.id)) { - val metadata = metadataDatasource.loadMetadata(scene.id) - val pathSegments = getPathSegments(scene) - val hash = EntityHasher.hashScene( - id = scene.id, - order = scene.order, - path = pathSegments, - name = scene.name, - type = scene.type.toApiType(), - content = content, - outline = metadata?.outline ?: "", - notes = metadata?.notes ?: "", - ) - projectSynchronizer.markEntityAsDirty(scene.id, hash) - } - } - - override suspend fun createScene( - parent: SceneItem?, - sceneName: String, - forceId: Int?, - forceOrder: Int? - ): SceneItem? { - return createSceneItem(parent, sceneName, false, forceId, forceOrder) - } - - override suspend fun createGroup( - parent: SceneItem?, - groupName: String, - forceId: Int?, - forceOrder: Int? - ): SceneItem? { - return createSceneItem(parent, groupName, true, forceId, forceOrder) - } - - private suspend fun createSceneItem( - parent: SceneItem?, - name: String, - isGroup: Boolean, - forceId: Int?, - forceOrder: Int? - ): SceneItem? { - val cleanedNamed = name.trim() - - return if (validateSceneName(cleanedNamed).isFailure) { - Napier.d("Invalid scene name") - null - } else { - val lastOrder = getLastOrderNumber(parent?.id) - val nextOrder = forceOrder ?: (lastOrder + 1) - val sceneId = forceId ?: idRepository.claimNextId() - val type = if (isGroup) SceneItem.Type.Group else SceneItem.Type.Scene - - val newSceneItem = SceneItem( - projectDef = projectDef, - type = type, - id = sceneId, - name = cleanedNamed, - order = nextOrder, - ) - - val newTreeNode = TreeNode(newSceneItem) - if (parent != null) { - val parentNode = sceneTree.find { it.id == parent.id } - parentNode.addChild(newTreeNode) - } else { - sceneTree.addChild(newTreeNode) - } - - val scenePath = getSceneFilePath(newSceneItem, true).toOkioPath() - when (type) { - SceneItem.Type.Scene -> fileSystem.write(scenePath, true) { - writeUtf8("") - } - - SceneItem.Type.Group -> fileSystem.createDirectory(scenePath, true) - SceneItem.Type.Root -> throw IllegalArgumentException("Cannot create Root") - } - - // Correct order digit paddings when injecting a new scene/group - if (forceOrder != null) { - updateSceneOrderMagnitudeOnly(parent?.id ?: SceneItem.ROOT_ID) - } - // If we need to increase the padding digits, update the file names - else if (lastOrder.numDigits() < nextOrder.numDigits()) { - updateSceneOrder(parent?.id ?: SceneItem.ROOT_ID) - } - - Napier.i("createScene: $cleanedNamed") - - reloadScenes() - - newSceneItem - } - } - - override suspend fun deleteScene(scene: SceneItem): Boolean { - val scenePath = getSceneFilePath(scene).toOkioPath() - return try { - if (!fileSystem.exists(scenePath)) { - Napier.e("Tried to delete Scene, but file did not exist") - false - } else if (!fileSystem.metadata(scenePath).isRegularFile) { - Napier.e("Tried to delete Scene, but file was not File") - false - } else { - fileSystem.delete(scenePath) - - val sceneNode = getSceneNodeFromId(scene.id) - - val parent = sceneNode?.parent - if (parent != null) { - val parentId: Int = parent.value.id - parent.removeChild(sceneNode) - - updateSceneOrder(parentId) - Napier.w("Scene ${scene.id} deleted") - - if (projectSynchronizer.isServerSynchronized()) { - projectSynchronizer.recordIdDeletion(scene.id) - } - - reloadScenes() - - true - } else { - Napier.w("Failed to delete scene ${scene.id}") - false - } - } - } catch (e: IOException) { - Napier.e("Failed to delete Group ID ${scene.id}: ${e.message}") - false - } - } - - override suspend fun deleteGroup(scene: SceneItem): Boolean { - val scenePath = getSceneFilePath(scene).toOkioPath() - return try { - if (!fileSystem.exists(scenePath)) { - Napier.e("Tried to delete Group, but file did not exist") - false - } else if (!fileSystem.metadata(scenePath).isDirectory) { - Napier.e("Tried to delete Group, but file was not Directory") - false - } else if (fileSystem.list(scenePath).isNotEmpty()) { - Napier.w("Tried to delete Group, but was not empty") - false - } else { - fileSystem.delete(scenePath) - if (projectSynchronizer.isServerSynchronized()) { - projectSynchronizer.recordIdDeletion(scene.id) - } - - val sceneNode = getSceneNodeFromId(scene.id) - - val parent = sceneNode?.parent - if (parent != null) { - val parentId: Int = parent.value.id - parent.removeChild(sceneNode) - - updateSceneOrder(parentId) - Napier.w("Group ${scene.id} deleted") - - reloadScenes() - - true - } else { - Napier.w("Failed to delete group ${scene.id}") - false - } - } - } catch (e: IOException) { - Napier.e("Failed to delete Group ID ${scene.id}: ${e.message}") - false - } - } - - override fun getScenes(): List { - return getAllScenePathsOkio() - .filterScenePathsOkio() - .map { path -> - getSceneFromFilename(path) - } - } - - override fun getSceneTree(): ImmutableTree { - return sceneTree.toImmutableTree() - } - - override fun getSceneTempBufferContents(): List { - val bufferDirectory = getSceneBufferDirectory().toOkioPath() - return fileSystem.list(bufferDirectory) - .filter { fileSystem.metadata(it).isRegularFile } - .mapNotNull { path -> - val id = getSceneIdFromBufferFilename(path.name) - getSceneItemFromId(id) - } - .map { sceneDef -> - val tempPath = getSceneBufferTempPath(sceneDef).toOkioPath() - val content = try { - fileSystem.read(tempPath) { - readUtf8() - } - } catch (e: IOException) { - Napier.e("Failed to load Scene (${sceneDef.name})") - "" - } - SceneContent(sceneDef, content) - } - } - - override fun loadSceneMarkdownRaw(sceneItem: SceneItem, scenePath: HPath): String { - val content = if (sceneItem.type == SceneItem.Type.Scene) { - try { - fileSystem.read(scenePath.toOkioPath()) { - readUtf8() - } - } catch (e: IOException) { - Napier.e("Failed to load Scene markdown raw (${sceneItem.name})") - "" - } - } else { - "" - } - - return content - } - - override fun getPathFromFilesystem(sceneItem: SceneItem): HPath? { - return getAllScenePathsOkio() - .filterScenePathsOkio().firstOrNull { path -> - sceneItem.id == getSceneFromFilename(path).id - } - } - - override suspend fun storeSceneMarkdownRaw(sceneItem: SceneContent, scenePath: HPath): Boolean { - sceneItem.markdown ?: return false - - return try { - fileSystem.write(scenePath.toOkioPath()) { - writeUtf8(sceneItem.markdown) - } - - markForSynchronization(sceneItem.scene) - - true - } catch (e: IOException) { - Napier.e("Failed to store Scene markdown raw (${sceneItem.scene.id} - ${sceneItem.scene.name}) because: ${e.message}") - false - } - } - - override fun loadSceneBuffer(sceneItem: SceneItem): SceneBuffer { - val scenePath = getSceneFilePath(sceneItem).toOkioPath() - - return if (hasSceneBuffer(sceneItem)) { - getSceneBuffer(sceneItem) - ?: throw IllegalStateException("sceneBuffers did not contain buffer for scene: ${sceneItem.id} - ${sceneItem.name}") - } else { - val content = try { - fileSystem.read(scenePath) { - readUtf8() - } - } catch (e: IOException) { - Napier.e("Failed to load Scene (${sceneItem.name})") - "" - } - - val newBuffer = SceneBuffer( - SceneContent(sceneItem, content), - source = UpdateSource.Repository - ) - - updateSceneBuffer(newBuffer) - - newBuffer - } - } - - override suspend fun storeSceneBuffer(sceneItem: SceneItem): Boolean { - val buffer = getSceneBuffer(sceneItem) - if (buffer == null) { - Napier.e { "Failed to store scene: ${sceneItem.id} - ${sceneItem.name}, no buffer present" } - return false - } - - val scenePath = getSceneFilePath(sceneItem).toOkioPath() - - return try { - val markdown = buffer.content.coerceMarkdown() - - markForSynchronization(sceneItem) - - fileSystem.write(scenePath) { - writeUtf8(markdown) - } - - val cleanBuffer = buffer.copy(dirty = false) - updateSceneBuffer(cleanBuffer) - - clearTempScene(sceneItem) - - true - } catch (e: IOException) { - Napier.e("Failed to store scene: (${sceneItem.name}) with error: ${e.message}") - false - } - } - - override fun storeTempSceneBuffer(sceneItem: SceneItem): Boolean { - val buffer = getSceneBuffer(sceneItem) - if (buffer == null) { - Napier.e { "Failed to store scene: ${sceneItem.id} - ${sceneItem.name}, no buffer present" } - return false - } - - val scenePath = getSceneBufferTempPath(sceneItem).toOkioPath() - - return try { - val markdown = buffer.content.coerceMarkdown() - - fileSystem.write(scenePath) { - writeUtf8(markdown) - } - - Napier.d("Stored temp scene: (${sceneItem.name})") - - true - } catch (e: IOException) { - Napier.e("Failed to store temp scene: (${sceneItem.name}) with error: ${e.message}") - false - } - } - - override fun clearTempScene(sceneItem: SceneItem) { - val path = getSceneBufferTempPath(sceneItem).toOkioPath() - fileSystem.delete(path) - } - - override fun getLastOrderNumber(parentPath: HPath): Int { - val numScenes = fileSystem.list(parentPath.toOkioPath()) - .filterScenePathsOkio() - .count() - return numScenes - } - - override fun getLastOrderNumber(parentId: Int?): Int { - val parentPath: HPath = if (parentId != null && parentId != 0) { - val parentItem = - getSceneItemFromId(parentId) ?: throw IllegalStateException("Parent not found") - - getSceneFilePath(parentItem) - } else { - getSceneDirectory() - } - - val numScenes = fileSystem.list(parentPath.toOkioPath()) - .filterScenePathsOkio() - .count() - 1 - return numScenes - } - - override suspend fun renameScene(sceneItem: SceneItem, newName: String): Boolean { - if (validateSceneName(newName).isFailure) return false - - val cleanedNamed = newName.trim() - - val oldPath = getSceneFilePath(sceneItem).toOkioPath() - val newDef = sceneItem.copy(name = cleanedNamed) - - val node = getSceneNodeFromId(sceneItem.id) - ?: throw IllegalStateException("Failed to get scene for renaming: ${sceneItem.id}") - node.value = newDef - - val newPath = getSceneFilePath(newDef).toOkioPath() - - fileSystem.atomicMove(oldPath, newPath) - markForSynchronization(sceneItem) - - reloadScenes() - return true - } -} - -fun Collection.filterScenePathsOkio() = - map { it.toHPath() }.filterScenePaths() - -fun Sequence.filterScenePathsOkio() = - map { it.toHPath() }.filterScenePaths() \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneRepositoryExceptions.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneRepositoryExceptions.kt new file mode 100644 index 00000000..07d9130c --- /dev/null +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneRepositoryExceptions.kt @@ -0,0 +1,7 @@ +package com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository + +open class InvalidSceneFilename(message: String, fileName: String) : + IllegalStateException("$fileName failed to parse because: $message") + +class InvalidSceneBufferFilename(message: String, fileName: String) : + InvalidSceneFilename(message, fileName) diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneTreeExt.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneTreeExt.kt new file mode 100644 index 00000000..bac0a914 --- /dev/null +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/data/sceneeditorrepository/SceneTreeExt.kt @@ -0,0 +1,8 @@ +package com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository + +import com.darkrockstudios.apps.hammer.common.data.SceneItem +import com.darkrockstudios.apps.hammer.common.data.tree.Tree +import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode + +inline fun Tree.findById(scene: SceneItem): TreeNode = findById(scene.id) +inline fun Tree.findById(id: Int): TreeNode = find { it.id == id } \ No newline at end of file diff --git a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/dependencyinjection/mainModule.kt b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/dependencyinjection/mainModule.kt index c777ed5c..b775913d 100644 --- a/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/dependencyinjection/mainModule.kt +++ b/common/src/commonMain/kotlin/com/darkrockstudios/apps/hammer/common/dependencyinjection/mainModule.kt @@ -34,7 +34,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectsync.synchronizers.Cli import com.darkrockstudios.apps.hammer.common.data.projectsync.synchronizers.ClientTimelineSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.timelinerepository.TimeLineDatasource import com.darkrockstudios.apps.hammer.common.data.timelinerepository.TimeLineRepository @@ -113,7 +112,7 @@ val mainModule = module { scoped { get().projectDef } scopedOf(::SceneDatasource) - scopedOf(::SceneEditorRepositoryOkio) bind SceneEditorRepository::class + scopedOf(::SceneEditorRepository) scopedOf(::SceneDraftsDatasource) scopedOf(::SceneDraftRepository) scopedOf(::SceneMetadataDatasource) diff --git a/common/src/desktopTest/kotlin/repositories/scenedraft/SceneDraftRepositoryTest.kt b/common/src/desktopTest/kotlin/repositories/scenedraft/SceneDraftRepositoryTest.kt index 936841df..a6948af9 100644 --- a/common/src/desktopTest/kotlin/repositories/scenedraft/SceneDraftRepositoryTest.kt +++ b/common/src/desktopTest/kotlin/repositories/scenedraft/SceneDraftRepositoryTest.kt @@ -12,7 +12,6 @@ import com.darkrockstudios.apps.hammer.common.data.drafts.SceneDraftsDatasource import com.darkrockstudios.apps.hammer.common.data.id.IdRepository import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.dependencyinjection.createTomlSerializer import com.darkrockstudios.apps.hammer.common.fileio.okio.toOkioPath import createProject @@ -51,7 +50,7 @@ class SceneDraftRepositoryTest : BaseTest() { override fun setup() { super.setup() - sceneEditorRepository = mockk() + sceneEditorRepository = mockk() every { sceneEditorRepository.getSceneDirectory() } answers { SceneDatasource.getSceneDirectory(projectDef, ffs) } diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryBufferTest.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryBufferTest.kt index 92289e18..a56316ab 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryBufferTest.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryBufferTest.kt @@ -12,7 +12,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetada import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.dependencyinjection.createTomlSerializer import com.darkrockstudios.apps.hammer.common.fileio.okio.toHPath @@ -85,10 +84,9 @@ class SceneEditorRepositoryBufferTest : BaseTest() { private fun createRepository(projectDef: ProjectDef): SceneEditorRepository { sceneMetadataDatasource = createDatasource(projectDef) sceneDatasource = createSceneDatasource(projectDef) - return SceneEditorRepositoryOkio( + return SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = projectMetadataDatasource, sceneMetadataDatasource = sceneMetadataDatasource, @@ -251,7 +249,7 @@ class SceneEditorRepositoryBufferTest : BaseTest() { ) val repo = createRepository(projDef) - val scene3Path = repo.getPathFromFilesystem(sceneItem)?.toOkioPath() + val scene3Path = repo.resolveScenePathFromFilesystem(sceneItem.id)?.toOkioPath() assertNotNull(scene3Path) val pathSegments = scene3Path.segments.reversed() @@ -295,8 +293,8 @@ class SceneEditorRepositoryBufferTest : BaseTest() { assertFalse(repo.hasDirtyBuffers()) } - private fun getTempBufferPath(repo: SceneEditorRepository, sceneId: Int): Path { - val bufferDir = repo.getSceneBufferDirectory().toOkioPath() + private fun getTempBufferPath(sceneId: Int): Path { + val bufferDir = sceneDatasource.getSceneBufferDirectory().toOkioPath() ffs.createDirectories(bufferDir) val tempBufPath = bufferDir / "$sceneId.md" @@ -307,7 +305,7 @@ class SceneEditorRepositoryBufferTest : BaseTest() { private fun writeTempBuffer(repo: SceneEditorRepository, sceneId: Int) { val tempContent = content(sceneId) - val tempBufPath = getTempBufferPath(repo, sceneId) + val tempBufPath = getTempBufferPath(sceneId) ffs.write(tempBufPath) { writeUtf8(tempContent) } @@ -352,7 +350,7 @@ class SceneEditorRepositoryBufferTest : BaseTest() { assertFalse(repo.hasDirtyBuffer(1)) - val temp2Path = getTempBufferPath(repo, 1) + val temp2Path = getTempBufferPath(1) assertFalse(ffs.exists(temp2Path)) } @@ -373,10 +371,10 @@ class SceneEditorRepositoryBufferTest : BaseTest() { repo.storeAllBuffers() - val temp1Path = getTempBufferPath(repo, 1) + val temp1Path = getTempBufferPath(1) assertFalse(ffs.exists(temp1Path)) - val temp2Path = getTempBufferPath(repo, 3) + val temp2Path = getTempBufferPath(3) assertFalse(ffs.exists(temp2Path)) assertFalse(repo.hasDirtyBuffers()) @@ -424,6 +422,7 @@ class SceneEditorRepositoryBufferTest : BaseTest() { ffs.read(path.toOkioPath()) { val exported = readUtf8() + println("\"$exported\"") assertEquals(exportedStory1.trim(), exported.trim()) } } diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryLoadTest.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryLoadTest.kt index 8a89a83f..c8dd4158 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryLoadTest.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryLoadTest.kt @@ -11,7 +11,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectsrepository.ProjectsRe import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode import com.darkrockstudios.apps.hammer.common.dependencyinjection.createTomlSerializer @@ -30,7 +29,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import utils.BaseTest -import utils.callPrivate import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -101,10 +99,9 @@ class SceneEditorRepositoryLoadTest : BaseTest() { createProject(ffs, projectName) - repo = SceneEditorRepositoryOkio( + repo = SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = metadataRepository, sceneMetadataDatasource = metadataDatasource, @@ -187,7 +184,7 @@ class SceneEditorRepositoryLoadTest : BaseTest() { }, ) - val tree = repo.callPrivate>("loadSceneTree") + val tree = sceneDatasource.loadSceneTree(repo.rootScene) // +1 to count root assertEquals(expectedNodes.size, tree.numChildrenRecursive() + 1) @@ -269,7 +266,7 @@ class SceneEditorRepositoryLoadTest : BaseTest() { }, ) - val tree = repo.callPrivate>("loadSceneTree") + val tree = sceneDatasource.loadSceneTree(repo.rootScene) // +1 to count root assertEquals(expectedNodes.size, tree.numChildrenRecursive() + 1) @@ -343,7 +340,7 @@ class SceneEditorRepositoryLoadTest : BaseTest() { }, ) - val tree = repo.callPrivate>("loadSceneTree") + val tree = sceneDatasource.loadSceneTree(repo.rootScene) // +1 to count root assertEquals(expectedNodes.size, tree.numChildrenRecursive() + 1) diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMetadataTest.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMetadataTest.kt index 7ca87f47..d3ed51ff 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMetadataTest.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMetadataTest.kt @@ -10,7 +10,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetada import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadata import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.dependencyinjection.createTomlSerializer @@ -68,10 +67,9 @@ class SceneEditorRepositoryMetadataTest : BaseTest() { private fun createRepository(projectDef: ProjectDef): SceneEditorRepository { sceneMetadataDatasource = createDatasource(projectDef) sceneDatasource = createSceneDatasource(projectDef) - return SceneEditorRepositoryOkio( + return SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = projectMetadataDatasource, sceneMetadataDatasource = sceneMetadataDatasource, diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMoveTest.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMoveTest.kt index 2921f3f5..6a817c28 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMoveTest.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryMoveTest.kt @@ -11,7 +11,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectsrepository.ProjectsRe import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.filterScenePathsOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.tree.NodeCoordinates @@ -90,7 +89,7 @@ class SceneEditorRepositoryMoveTest : BaseTest() { ffs.list(scenePath.toOkioPath()) .filterScenePathsOkio() .sortedBy { it.name }.forEach { childPath -> - val sceneItem = repo.getSceneFromPath(childPath) + val sceneItem = sceneDatasource.getSceneFromPath(childPath) val foundItem = nodesById[sceneItem.id] assertNotNull(sceneItem, "File system scene didn't exist in tree") assertEquals(foundItem, sceneItem, "File system scene didn't match tree scene") @@ -134,10 +133,9 @@ class SceneEditorRepositoryMoveTest : BaseTest() { setupKoin() - repo = SceneEditorRepositoryOkio( + repo = SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = metadataRepository, sceneMetadataDatasource = metadataDatasource, diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryOtherTest.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryOtherTest.kt index f176bff8..c48badc7 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryOtherTest.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryOtherTest.kt @@ -17,7 +17,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectsrepository.Validation import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.tree.Tree import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode @@ -41,7 +40,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import utils.BaseTest -import utils.callPrivate import utils.getPrivateProperty import kotlin.test.assertEquals import kotlin.test.assertFalse @@ -80,7 +78,7 @@ class SceneEditorRepositoryOtherTest : BaseTest() { // Verify tree nodes match file system nodes tree.filter { !it.value.isRootScene }.forEach { node -> val path = repo.getSceneFilePath(node.value.id) - val fsScene = repo.getSceneFromPath(path) + val fsScene = sceneDatasource.getSceneFromPath(path) assertEquals(node.value, fsScene) } @@ -157,10 +155,9 @@ class SceneEditorRepositoryOtherTest : BaseTest() { createProject(ffs, projectName) - repo = SceneEditorRepositoryOkio( + repo = SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = metadataRepository, sceneMetadataDatasource = metadataDatasource, @@ -176,11 +173,11 @@ class SceneEditorRepositoryOtherTest : BaseTest() { fun `Cleanup Scene Order`() = runTest { configure(OUT_OF_ORDER_PROJECT_NAME) - val beforeSceneTree: TreeNode = repo.callPrivate("loadSceneTree") + val beforeSceneTree: TreeNode = sceneDatasource.loadSceneTree(repo.rootScene) repo.initializeSceneEditor() - val afterSceneTree: TreeNode = repo.callPrivate("loadSceneTree") + val afterSceneTree: TreeNode = sceneDatasource.loadSceneTree(repo.rootScene) // Make sure the tree was actually changed, initializeProjectEditor() // should clean up this out of order project @@ -450,7 +447,7 @@ class SceneEditorRepositoryOtherTest : BaseTest() { assertEquals(newSceneName, renamedScene?.name) val path = repo.getSceneFilePath(sceneItem.id) - val loadedScene = repo.getSceneFromFilename(path) + val loadedScene = sceneDatasource.getSceneFromFilename(path) assertEquals(newSceneName, loadedScene.name) assertTrue(ffs.exists(path.toOkioPath())) } diff --git a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryTestSimple.kt b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryTestSimple.kt index 273534c1..d0b722f5 100644 --- a/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryTestSimple.kt +++ b/common/src/desktopTest/kotlin/repositories/sceneeditor/SceneEditorRepositoryTestSimple.kt @@ -11,7 +11,6 @@ import com.darkrockstudios.apps.hammer.common.data.projectsrepository.ProjectsRe import com.darkrockstudios.apps.hammer.common.data.projectsync.ClientProjectSynchronizer import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneDatasource import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.scenemetadata.SceneMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode import com.darkrockstudios.apps.hammer.common.dependencyinjection.createTomlSerializer @@ -31,7 +30,6 @@ import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import utils.BaseTest -import utils.callPrivate import kotlin.test.assertEquals class SceneEditorRepositoryTestSimple : BaseTest() { @@ -71,7 +69,7 @@ class SceneEditorRepositoryTestSimple : BaseTest() { private fun populateProject(fs: FakeFileSystem) { fs.createDirectories(projectPath.toOkioPath()) - scenesPath = projectPath.toOkioPath().div(SceneEditorRepository.SCENE_DIRECTORY).toHPath() + scenesPath = projectPath.toOkioPath().div(SceneDatasource.SCENE_DIRECTORY).toHPath() fs.createDirectory(scenesPath.toOkioPath()) @@ -134,37 +132,30 @@ class SceneEditorRepositoryTestSimple : BaseTest() { ffs.checkNoOpenFiles() } - @Test - fun `Get filename`() { - val repo = SceneEditorRepositoryOkio( + private fun createRepository(): SceneEditorRepository { + return SceneEditorRepository( projectDef = projectDef, projectSynchronizer = projectSynchronizer, - fileSystem = ffs, idRepository = idRepository, projectMetadataDatasource = projectMetadataRepository, sceneMetadataDatasource = sceneMetadataDatasource, sceneDatasource = sceneDatasource, ) + } + @Test + fun `Get filename`() { val expectedFilename = sceneFiles.entries.first().key val scenePath = scenePath(expectedFilename) - val sceneFilename = repo.getSceneFilename(scenePath) + val sceneFilename = sceneDatasource.getSceneFilename(scenePath) assertEquals(expectedFilename, sceneFilename) } @Test fun `Load Scene Tree`() { - val repo = SceneEditorRepositoryOkio( - projectDef = projectDef, - projectSynchronizer = projectSynchronizer, - fileSystem = ffs, - idRepository = idRepository, - projectMetadataDatasource = projectMetadataRepository, - sceneMetadataDatasource = sceneMetadataDatasource, - sceneDatasource = sceneDatasource, - ) + val repo = createRepository() - val sceneTree: TreeNode = repo.callPrivate("loadSceneTree") + val sceneTree: TreeNode = sceneDatasource.loadSceneTree(repo.rootScene) assertEquals(3, sceneTree.numChildrenRecursive()) assertEquals(1, sceneTree[0].value.id) @@ -174,15 +165,7 @@ class SceneEditorRepositoryTestSimple : BaseTest() { @Test fun `Init Editor`() = runTest { - val repo = SceneEditorRepositoryOkio( - projectDef = projectDef, - projectSynchronizer = projectSynchronizer, - fileSystem = ffs, - idRepository = idRepository, - projectMetadataDatasource = projectMetadataRepository, - sceneMetadataDatasource = sceneMetadataDatasource, - sceneDatasource = sceneDatasource, - ) + val repo = createRepository() repo.initializeSceneEditor() diff --git a/common/src/desktopTest/kotlin/synchronizer/SceneSynchronizerTest.kt b/common/src/desktopTest/kotlin/synchronizer/SceneSynchronizerTest.kt index 3dba6595..5c1e3a2e 100644 --- a/common/src/desktopTest/kotlin/synchronizer/SceneSynchronizerTest.kt +++ b/common/src/desktopTest/kotlin/synchronizer/SceneSynchronizerTest.kt @@ -10,7 +10,7 @@ import com.darkrockstudios.apps.hammer.common.data.drafts.SceneDraftRepository import com.darkrockstudios.apps.hammer.common.data.projectmetadata.ProjectMetadataDatasource import com.darkrockstudios.apps.hammer.common.data.projectsync.synchronizers.ClientSceneSynchronizer import com.darkrockstudios.apps.hammer.common.data.rootSceneNode -import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepositoryOkio +import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.SceneEditorRepository import com.darkrockstudios.apps.hammer.common.data.sceneeditorrepository.findById import com.darkrockstudios.apps.hammer.common.data.tree.Tree import com.darkrockstudios.apps.hammer.common.data.tree.TreeNode @@ -37,7 +37,7 @@ class SceneSynchronizerTest : BaseTest() { private val def = getProject1Def() @MockK - private lateinit var sceneEditorRepository: SceneEditorRepositoryOkio + private lateinit var sceneEditorRepository: SceneEditorRepository @MockK private lateinit var draftRepository: SceneDraftRepository @@ -110,7 +110,7 @@ class SceneSynchronizerTest : BaseTest() { clientEntity } every { sceneEditorRepository.rawTree } returns tree - every { sceneEditorRepository.getPathFromFilesystem(clientEntity) } returns filePath + every { sceneEditorRepository.resolveScenePathFromFilesystem(clientEntity.id) } returns filePath coEvery { sceneEditorRepository.storeSceneMarkdownRaw(content, filePath) } returns true //////////////////// @@ -160,7 +160,7 @@ class SceneSynchronizerTest : BaseTest() { every { sceneEditorRepository.getSceneItemFromId(ROOT_ID) } returns rootSceneNode(def) every { sceneEditorRepository.getSceneItemFromId(sceneId) } returns clientEntity every { sceneEditorRepository.rawTree } returns tree - every { sceneEditorRepository.getPathFromFilesystem(clientEntity) } returns filePath + every { sceneEditorRepository.resolveScenePathFromFilesystem(clientEntity.id) } returns filePath coEvery { sceneEditorRepository.storeSceneMarkdownRaw(content, filePath) } returns true rootNode.addChild(TreeNode(clientEntity)) @@ -211,7 +211,7 @@ class SceneSynchronizerTest : BaseTest() { every { sceneEditorRepository.getSceneItemFromId(ROOT_ID) } returns rootSceneNode(def) every { sceneEditorRepository.getSceneItemFromId(sceneId) } returns clientSceneEntity every { sceneEditorRepository.rawTree } returns tree - every { sceneEditorRepository.getPathFromFilesystem(clientSceneEntity) } returns filePath + every { sceneEditorRepository.resolveScenePathFromFilesystem(clientSceneEntity.id) } returns filePath coEvery { sceneEditorRepository.storeSceneMarkdownRaw(content, filePath) } returns true val clientGroupEntity = SceneItem(