diff --git a/.gitignore b/.gitignore index 4abaf04..b16c4b9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ +build +dist .DS_Store .gradle -build +.idea +local.properties yarn.lock -dist diff --git a/CHANELOG.md b/CHANELOG.md new file mode 100644 index 0000000..014ce40 --- /dev/null +++ b/CHANELOG.md @@ -0,0 +1,13 @@ + +# [0.9.0](https://github.com/OGStudio/pskov2/pull/7) +#### 2025-12-27 + +| № | Platform | What has been done | Why | +| --- | --- | --- | --- | +| 1 | Android | Create draft Android version that loads debug page | Research WebView to understand how it feels like, its limitations, and UX | +| 2 | Browser | Display files with accordion and tables | Simplify navigation and search for files | +| 3 | | Implement copying, renaming, and deleting files | Remove the need to use CLI to create new pages | +| 4 | | Add `External` button to open rendered page in a separate browser tab | See how the page looks like in real life outside PSKOV | +| 5 | | Report missing or empty `pskov.cfg` | Helps you notice you specified the wrong path to a project | +| 6 | Node.js | Handle `POST /delete` | Let Browser part delete files | + diff --git a/cld/entities.yml b/cld/entities.yml index 9f9bd10..28f5914 100644 --- a/cld/entities.yml +++ b/cld/entities.yml @@ -11,11 +11,23 @@ AppContext: cfg: [String: String] converterInput: String converterOutput: String + copyFileId: [Int] + deleteFile: String + deleteFileId: [Int] + deleteFileOrigin: String didClickEditorTab: Bool + didClickExternalButton: Bool + didClickFilesCopyOK: Bool + didClickFilesDeleteOK: Bool + didClickFilesRenameOK: Bool didClickFilesTab: Bool didClickRenderTab: Bool didClickSaveBtn: Bool + didCopyFile: Bool + didCopyRenamedFile: Bool + didDeleteFile: Bool didLaunch: Bool + didRenameFile: Bool didResize: Bool didSaveEditedFiles: Bool didSaveFile: Bool @@ -23,18 +35,32 @@ AppContext: editedContents: String editedFileContents: [String: String] editorContents: String + externalURL: String + filesCopyDefaultName: String + filesDeleteName: String + filesRenameName: String header: [String] + hideFileOptions: [Int] inputDirFiles: [Int: [FSFile]] inputDirs: [String] + inputFilesCopy: String + inputFilesRename: String inputMDFiles: [Int: [String]] installEditor: Bool installMDConverter: Bool + isCfgValid: Bool + isExternalButtonVisible: Bool + isFilesCopyDialogVisible: Bool + isFilesDeleteDialogVisible: Bool + isFilesRenameDialogVisible: Bool itemTemplates: [Int: String] listInputDirId: Int page: Page projectPath: String readFile: String readFileContents: String + readFileOrigin: String + renameFileId: [Int] renderedFile: String renderPage: String request: NetRequest @@ -80,6 +106,14 @@ Page: slug: String title: String +PlayContext: + type: context + fields: + didLaunch: Bool + didResetWebView: Bool + isPlaygroundVisible: Bool + url: String + SrvContext: prefix-kotlin: @JsExport type: context @@ -88,6 +122,8 @@ SrvContext: browserDir: String defaultBrowserDir: String defaultHTTPPort: Int + deleteFile: String + didDeleteFile: Bool didLaunch: Bool didWriteFile: Bool dirFiles: [FSFile] diff --git a/kmp/gradle.properties b/kmp/gradle.properties index 573ddf3..d0bd9e1 100644 --- a/kmp/gradle.properties +++ b/kmp/gradle.properties @@ -1,3 +1,5 @@ +# Use Compose for Android +android.useAndroidX=true # Be verbose in console org.gradle.console=verbose # Cache whatever is built diff --git a/kmp/ver-browser/src/commonMain/kotlin/org/opengamestudio/appFun.kt b/kmp/ver-browser/src/commonMain/kotlin/org/opengamestudio/appFun.kt index 7940ac7..09dd285 100644 --- a/kmp/ver-browser/src/commonMain/kotlin/org/opengamestudio/appFun.kt +++ b/kmp/ver-browser/src/commonMain/kotlin/org/opengamestudio/appFun.kt @@ -3,8 +3,11 @@ import kotlin.js.JsExport // +@JsExport val APP_CFG_ERROR_DETAILS = "pskov.cfg is missing or empty" +@JsExport val APP_CFG_ERROR_TITLE = "Project config is invalid" @JsExport val APP_CFG_FILE = "pskov.cfg" @JsExport val APP_CFG_KEY_INPUT = "input" +@JsExport val APP_DELETE_FAILURE_TITLE = "Failed to delete the file" @JsExport val APP_HEADER_KEY_FILE = "File" @JsExport val APP_HEADER_KEY_PROJECT = "Project" @JsExport val APP_ITEM_TEMPLATE_FILE = "item.template" @@ -16,9 +19,71 @@ import kotlin.js.JsExport @JsExport val APP_TAB_FILES_INDEX = 0 @JsExport val APP_TAB_RENDER_INDEX = 2 @JsExport val APP_SPLASH_TIMEOUT = 800 +@JsExport val APP_YEAR_MAX = 2100 // +/* Delete a file from disk + * + * Conditions: + * 1. User selected `OK` to delete the file + * 2. Renamed file has been copied + */ +@JsExport +fun appShouldDeleteFile(c: AppContext): AppContext { + /* 1 */ if ( + c.recentField == "deleteFileOrigin" && + c.deleteFileOrigin == "didClickFilesDeleteOK" + ) { + c.deleteFile = c.filesDeleteName + c.recentField = "deleteFile" + return c + } + + /* 2 */ if ( + c.recentField == "deleteFileOrigin" && + c.deleteFileOrigin == "didCopyRenamedFile" + ) { + c.deleteFile = c.filesRenameName + c.recentField = "deleteFile" + return c + } + + c.recentField = "none" + return c +} + +/* Hide file options' menu + * + * Conditions: + * 1. Did select `Copy` menu item + * 3. Did select `Delete` menu item + * 2. Did select `Rename` menu item + */ +@JsExport +fun appShouldHideFileOptions(c: AppContext): AppContext { + if (c.recentField == "copyFileId") { + c.hideFileOptions = c.copyFileId + c.recentField = "hideFileOptions" + return c + } + + if (c.recentField == "deleteFileId") { + c.hideFileOptions = c.deleteFileId + c.recentField = "hideFileOptions" + return c + } + + if (c.recentField == "renameFileId") { + c.hideFileOptions = c.renameFileId + c.recentField = "hideFileOptions" + return c + } + + c.recentField = "none" + return c +} + /* Hide splash after a delay * * Conditions: @@ -79,10 +144,13 @@ fun appShouldInstallMDConverter(c: AppContext): AppContext { * 4. Read file * 5. Save edited file * 6. Save rendered file + * 7. Save copied file + * 8. Delete file + * 9. Save renamed file */ @JsExport fun appShouldLoad(c: AppContext): AppContext { - if (c.recentField == "didLaunch") { + /* 1 */ if (c.recentField == "didLaunch") { c.request = NetRequest( "", @@ -93,7 +161,7 @@ fun appShouldLoad(c: AppContext): AppContext { return c } - if (c.recentField == "projectPath") { + /* 2 */ if (c.recentField == "projectPath") { c.request = NetRequest( APP_CFG_FILE, @@ -104,7 +172,7 @@ fun appShouldLoad(c: AppContext): AppContext { return c } - if (c.recentField == "listInputDirId") { + /* 3 */ if (c.recentField == "listInputDirId") { val dir = c.inputDirs[c.listInputDirId] c.request = NetRequest( @@ -116,7 +184,7 @@ fun appShouldLoad(c: AppContext): AppContext { return c } - if (c.recentField == "readFile") { + /* 4 */ if (c.recentField == "readFile") { c.request = NetRequest( c.readFile, @@ -127,7 +195,7 @@ fun appShouldLoad(c: AppContext): AppContext { return c } - if (c.recentField == "saveFileId") { + /* 5 */ if (c.recentField == "saveFileId") { val file = c.saveFiles[c.saveFileId] val contents = c.editedFileContents[file]!! val body = fileContentsToJSON(file, contents) @@ -141,7 +209,7 @@ fun appShouldLoad(c: AppContext): AppContext { return c } - if (c.recentField == "renderedFile") { + /* 6 */ if (c.recentField == "renderedFile") { val pageURL = "${c.page.slug}.$CONST_EXT_HTML" val o = c.itemTemplates[c.selectedFileId[0]] ?.replace(APP_PAGE_CONTENTS, c.converterOutput) @@ -160,19 +228,63 @@ fun appShouldLoad(c: AppContext): AppContext { return c } + /* 7 */ if ( + c.recentField == "readFileContents" && + c.readFileOrigin == "didClickFilesCopyOK" + ) { + val body = fileContentsToJSON(c.inputFilesCopy, c.readFileContents) + c.request = + NetRequest( + body, + CONST_HTTP_POST, + appURL(c.baseURL, CONST_API_WRITE), + ) + c.recentField = "request" + return c + } + + /* 8 */ if (c.recentField == "deleteFile") { + c.request = + NetRequest( + c.deleteFile, + CONST_HTTP_POST, + appURL(c.baseURL, CONST_API_DELETE), + ) + c.recentField = "request" + return c + } + + /* 9 */ if ( + c.recentField == "readFileContents" && + c.readFileOrigin == "didClickFilesRenameOK" + ) { + val body = fileContentsToJSON(c.inputFilesRename, c.readFileContents) + c.request = + NetRequest( + body, + CONST_HTTP_POST, + appURL(c.baseURL, CONST_API_WRITE), + ) + c.recentField = "request" + return c + } + c.recentField = "none" return c } -/* Start, continue, or finish listing contents of one of the input dirs +/* Start, continue, or finish listing contents of input dirs * * Conditions: * 1. Input dirs are available and not empty * 2. Input dir files received a new entry + * 3. File has been copied successfully + * 4. File has been deleted successfully + * 5. File has been renamed */ @JsExport fun appShouldListInputDir(c: AppContext): AppContext { - if ( + /* 1 */ if ( c.recentField == "inputDirs" && c.inputDirs.size > 0 ) { @@ -181,7 +293,7 @@ fun appShouldListInputDir(c: AppContext): AppContext { return c } - if ( + /* 2 */ if ( c.recentField == "inputDirFiles" && c.listInputDirId + 1 < c.inputDirs.size ) { @@ -190,6 +302,31 @@ fun appShouldListInputDir(c: AppContext): AppContext { return c } + /* 3 */ if ( + c.recentField == "didCopyFile" && + c.didCopyFile + ) { + c.listInputDirId = 0 + c.recentField = "listInputDirId" + return c + } + + /* 4 */ if ( + c.recentField == "didDeleteFile" && + c.deleteFileOrigin == "didClickFilesDeleteOK" && + c.didDeleteFile + ) { + c.listInputDirId = 0 + c.recentField = "listInputDirId" + return c + } + + /* 5 */ if (c.recentField == "didRenameFile") { + c.listInputDirId = 0 + c.recentField = "listInputDirId" + return c + } + c.recentField = "none" return c } @@ -221,27 +358,47 @@ fun appShouldParseCfg(c: AppContext): AppContext { * Conditions: * 1. User selected a file * 2. Editor has contents of a selected file + * 3. User selected `OK` to make a copy + * 4. User selected `OK` to rename */ @JsExport fun appShouldReadFile(c: AppContext): AppContext { - if ( - c.recentField == "selectedFileName" && - c.editedFileContents[c.selectedFileName] == null + /* 1 */ if ( + c.recentField == "readFileOrigin" && + c.readFileOrigin == "selectedFileName" ) { c.readFile = c.selectedFileName c.recentField = "readFile" return c } - if ( - c.recentField == "editorContents" && - c.itemTemplates[c.selectedFileId[0]] == null + /* 2 */ if ( + c.recentField == "readFileOrigin" && + c.readFileOrigin == "editorContents" ) { c.readFile = c.inputDirs[c.selectedFileId[0]] + "/" + APP_ITEM_TEMPLATE_FILE c.recentField = "readFile" return c } + /* 3 */ if ( + c.recentField == "readFileOrigin" && + c.readFileOrigin == "didClickFilesCopyOK" + ) { + c.readFile = appFileIdToName(c.copyFileId, c.inputDirs, c.inputMDFiles) + c.recentField = "readFile" + return c + } + + /* 4 */ if ( + c.recentField == "readFileOrigin" && + c.readFileOrigin == "didClickFilesRenameOK" + ) { + c.readFile = appFileIdToName(c.renameFileId, c.inputDirs, c.inputMDFiles) + c.recentField = "readFile" + return c + } + c.recentField = "none" return c } @@ -263,6 +420,35 @@ fun appShouldRenderPage(c: AppContext): AppContext { return c } +/* Validate config + * + * Conditions: + * 1. Config has been parsed + * 2. Config is absent + */ +@JsExport +fun appShouldResetCfgValidity(c: AppContext): AppContext { + /* 1 */ if (c.recentField == "cfg") { + c.isCfgValid = !c.cfg.isEmpty() + c.recentField = "isCfgValid" + return c + } + + /* 2 */ if ( + c.recentField == "responseError" && + c.responseError.req.method == CONST_HTTP_POST && + c.responseError.req.url == appURL(c.baseURL, CONST_API_READ) && + c.responseError.req.body == APP_CFG_FILE + ) { + c.isCfgValid = false + c.recentField = "isCfgValid" + return c + } + + c.recentField = "none" + return c +} + /* Convert MD to HTML * * Conditions: @@ -280,6 +466,111 @@ fun appShouldResetConverterInput(c: AppContext): AppContext { return c } +/* Who wants to delete a file + * + * Conditions: + * 1. User selected `OK` to delete the file + * 2. File has been copied for renaming + */ +@JsExport +fun appShouldResetDeleteFileOrigin(c: AppContext): AppContext { + /* 1 */ if (c.recentField == "didClickFilesDeleteOK") { + c.deleteFileOrigin = "didClickFilesDeleteOK" + c.recentField = "deleteFileOrigin" + return c + } + + /* 2 */ if (c.recentField == "didCopyRenamedFile") { + c.deleteFileOrigin = "didCopyRenamedFile" + c.recentField = "deleteFileOrigin" + return c + } + + c.recentField = "none" + return c +} + +/* Mark the end of copying a file + * + * Conditions: + * 1. File has been copied + */ +@JsExport +fun appShouldResetDidCopyFile(c: AppContext): AppContext { + /* 1 */ if ( + c.recentField == "didSaveFile" && + c.readFileOrigin == "didClickFilesCopyOK" + ) { + c.didCopyFile = true + c.recentField = "didCopyFile" + return c + } + + c.recentField = "none" + return c +} + +/* Mark the end of copying a renamed file + * + * Conditions: + * 1. File has been copied as part of renaming + */ +@JsExport +fun appShouldResetDidCopyRenamedFile(c: AppContext): AppContext { + /* 1 */ if ( + c.recentField == "didSaveFile" && + c.readFileOrigin == "didClickFilesRenameOK" + ) { + c.didCopyRenamedFile = true + c.recentField = "didCopyRenamedFile" + return c + } + + c.recentField = "none" + return c +} + +/* Mark the end of deleting a file + * + * Conditions: + * 1. File has been deleted + */ +@JsExport +fun appShouldResetDidDeleteFile(c: AppContext): AppContext { + if ( + c.recentField == "response" && + c.response.req.method == CONST_HTTP_POST && + c.response.req.url == appURL(c.baseURL, CONST_API_DELETE) + ) { + c.didDeleteFile = c.response.contents.isEmpty() + c.recentField = "didDeleteFile" + return c + } + + c.recentField = "none" + return c +} + +/* Mark the end of renaming a file + * + * Conditions: + * 1. File has been deleted + */ +@JsExport +fun appShouldResetDidRenameFile(c: AppContext): AppContext { + /* 1 */ if ( + c.recentField == "didDeleteFile" && + c.deleteFileOrigin == "didCopyRenamedFile" + ) { + c.didRenameFile = true + c.recentField = "didRenameFile" + return c + } + + c.recentField = "none" + return c +} + /* Mark the end of saving edited fiels * * Conditions: @@ -381,7 +672,8 @@ fun appShouldResetEditedFileContents(c: AppContext): AppContext { fun appShouldResetEditorContents(c: AppContext): AppContext { if ( c.recentField == "readFileContents" && - c.readFile.endsWith(CONST_EXT_MD) + c.readFile.endsWith(CONST_EXT_MD) && + c.readFileOrigin == "selectedFileName" ) { c.editorContents = c.readFileContents c.recentField = "editorContents" @@ -401,6 +693,184 @@ fun appShouldResetEditorContents(c: AppContext): AppContext { return c } +/* External button visibility + * + * Conditions: + * 1. User selected non-`Render` tab + * 2. User selected `Render` tab + */ +@JsExport +fun appShouldResetExternalButtonVisibility(c: AppContext): AppContext { + if ( + c.recentField == "selectedTabId" && + c.selectedTabId != APP_TAB_RENDER_INDEX + ) { + c.isExternalButtonVisible = false + c.recentField = "isExternalButtonVisible" + return c + } + + if ( + c.recentField == "selectedTabId" && + c.selectedTabId == APP_TAB_RENDER_INDEX + ) { + c.isExternalButtonVisible = true + c.recentField = "isExternalButtonVisible" + return c + } + + c.recentField = "none" + return c +} + +/* URL to open in a separate browser page + * + * Conditions: + * 1. User did click `External` button + */ +@JsExport +fun appShouldResetExternalURL(c: AppContext): AppContext { + if (c.recentField == "didClickExternalButton") { + c.externalURL = CONST_API_RENDER + "/" + c.renderedFile + c.recentField = "externalURL" + return c + } + + c.recentField = "none" + return c +} + +/* Set default name of a copy + * + * Conditions: + * 1. Did select `Copy` in files options menu + */ +@JsExport +fun appShouldResetFilesCopyDefaultName(c: AppContext): AppContext { + if (c.recentField == "copyFileId") { + val name = appFileIdToName(c.copyFileId, c.inputDirs, c.inputMDFiles) + val delim = "." + CONST_EXT_MD + val parts = name.split(delim) + c.filesCopyDefaultName = parts[0]!! + "_COPY" + delim + c.recentField = "filesCopyDefaultName" + return c + } + + c.recentField = "none" + return c +} + +/* Show / hide files' copy dialog + * + * Conditions: + * 1. Did select `Copy` in files options menu + * 2. Did save file after copying + */ +@JsExport +fun appShouldResetFilesCopyDialogVisibility(c: AppContext): AppContext { + if (c.recentField == "copyFileId") { + c.isFilesCopyDialogVisible = true + c.recentField = "isFilesCopyDialogVisible" + return c + } + + if (c.recentField == "didCopyFile") { + c.isFilesCopyDialogVisible = false + c.recentField = "isFilesCopyDialogVisible" + return c + } + + c.recentField = "none" + return c +} + +/* Show / hide files' delete dialog + * + * Conditions: + * 1. Did select `Delete` in files options menu + * 2. Did delete file + */ +@JsExport +fun appShouldResetFilesDeleteDialogVisibility(c: AppContext): AppContext { + if (c.recentField == "deleteFileId") { + c.isFilesDeleteDialogVisible = true + c.recentField = "isFilesDeleteDialogVisible" + return c + } + + if ( + c.recentField == "didDeleteFile" && + c.deleteFileOrigin == "didClickFilesDeleteOK" + ) { + c.isFilesDeleteDialogVisible = false + c.recentField = "isFilesDeleteDialogVisible" + return c + } + + c.recentField = "none" + return c +} + +/* Set name of deleted file + * + * Conditions: + * 1. Did select `Delete` in files options menu + */ +@JsExport +fun appShouldResetFilesDeleteName(c: AppContext): AppContext { + if (c.recentField == "deleteFileId") { + val name = appFileIdToName(c.deleteFileId, c.inputDirs, c.inputMDFiles) + c.filesDeleteName = name + c.recentField = "filesDeleteName" + return c + } + + c.recentField = "none" + return c +} + +/* Show / hide files' rename dialog + * + * Conditions: + * 1. Did select `Rename` in files options menu + * 2. Did save file after renaming + */ +@JsExport +fun appShouldResetFilesRenameDialogVisibility(c: AppContext): AppContext { + if (c.recentField == "renameFileId") { + c.isFilesRenameDialogVisible = true + c.recentField = "isFilesRenameDialogVisible" + return c + } + + if (c.recentField == "didRenameFile") { + c.isFilesRenameDialogVisible = false + c.recentField = "isFilesRenameDialogVisible" + return c + } + + c.recentField = "none" + return c +} + +/* Set name of renaming + * + * Conditions: + * 1. Did select `Rename` in files options menu + */ +@JsExport +fun appShouldResetFilesRenameName(c: AppContext): AppContext { + if (c.recentField == "renameFileId") { + val name = appFileIdToName(c.renameFileId, c.inputDirs, c.inputMDFiles) + c.filesRenameName = name + c.recentField = "filesRenameName" + return c + } + + c.recentField = "none" + return c +} + /* Set header contents * * Conditions: @@ -585,6 +1055,50 @@ fun appShouldResetReadFileContents(c: AppContext): AppContext { return c } +/* Who wants to read a file + * + * Conditions: + * 1. User selected a file + * 2. Editor has contents of a selected file + * 3. User selected `OK` to make a copy + * 4. User selected `OK` to rename + */ +@JsExport +fun appShouldResetReadFileOrigin(c: AppContext): AppContext { + /* 1 */ if ( + c.recentField == "selectedFileName" && + c.editedFileContents[c.selectedFileName] == null + ) { + c.readFileOrigin = "selectedFileName" + c.recentField = "readFileOrigin" + return c + } + + /* 2 */ if ( + c.recentField == "editorContents" && + c.itemTemplates[c.selectedFileId[0]] == null + ) { + c.readFileOrigin = "editorContents" + c.recentField = "readFileOrigin" + return c + } + + /* 3 */ if (c.recentField == "didClickFilesCopyOK") { + c.readFileOrigin = "didClickFilesCopyOK" + c.recentField = "readFileOrigin" + return c + } + + /* 4 */ if (c.recentField == "didClickFilesRenameOK") { + c.readFileOrigin = "didClickFilesRenameOK" + c.recentField = "readFileOrigin" + return c + } + + c.recentField = "none" + return c +} + /* Detect when editor needs to be resized * * Conditions: @@ -718,11 +1232,7 @@ fun appShouldSaveRenderedFile(c: AppContext): AppContext { @JsExport fun appShouldSelectFileName(c: AppContext): AppContext { if (c.recentField == "selectedFileId") { - val inputDirId = c.selectedFileId[0] - val mdFileId = c.selectedFileId[1] - val dir = c.inputDirs[inputDirId]!! - val file = c.inputMDFiles[inputDirId]!![mdFileId]!! - c.selectedFileName = "$dir/$file" + c.selectedFileName = appFileIdToName(c.selectedFileId, c.inputDirs, c.inputMDFiles) c.recentField = "selectedFileName" return c } @@ -778,6 +1288,19 @@ fun appShouldSelectTab(c: AppContext): AppContext { // +@JsExport +fun appFileIdToName( + fileId: Array, + inputDirs: Array, + inputMDFiles: Map> +): String { + val inputDirId = fileId[0] + val mdFileId = fileId[1] + val dir = inputDirs[inputDirId]!! + val file = inputMDFiles[inputDirId]!![mdFileId]!! + return "$dir/$file" +} + @JsExport fun appURL( baseURL: String, @@ -785,3 +1308,32 @@ fun appURL( ): String { return "$baseURL$api" } + +@JsExport +fun appYear(value: String): String { + val parts = value.split("-") + + // 1. Make sure there was a split + if (parts.size < 2) { + return "" + } + + // 2. Make sure the first part is a digit + var year: Int = 0 + try { + year = parts[0]?.toInt() ?: 0 + } catch (e: Exception) { + return "" + } + + // 3. Make sure digit makes sense as a year + if ( + year < 1970 || + year > APP_YEAR_MAX + ) { + return "" + } + + return "$year" +} + diff --git a/kmp/ver-browser/src/jsMain/resources/app.js b/kmp/ver-browser/src/jsMain/resources/app.js index 1a92f23..bdba475 100644 --- a/kmp/ver-browser/src/jsMain/resources/app.js +++ b/kmp/ver-browser/src/jsMain/resources/app.js @@ -6,27 +6,73 @@ function appCtrl() { // +let APP_FILE_OPTIONS = ` + + + + + + +
+ Make a copy +
+ Rename +
+ Delete +
+`; +let APP_FILE_OPTIONS_ID_T = "fileOptions-%ID%"; let APP_FILES_ID = "files"; +let APP_FILES_CONTENTS_ID = "filesContents"; +let APP_FILES_COPY_DIALOG_ID = "filesCopyDialog"; +let APP_FILES_COPY_INPUT_ID = "filesCopyInput"; +let APP_FILES_DELETE_DIALOG_ID = "filesDeleteDialog"; +let APP_FILES_DELETE_NAME_ID = "filesDeleteName"; +let APP_FILES_RENAME_DIALOG_ID = "filesRenameDialog"; +let APP_FILES_RENAME_INPUT_ID = "filesRenameInput"; let APP_HEADER_KEY_ID = "headerKey"; +let APP_HEADER_EXTERNAL_ID = "headerExternal"; let APP_HEADER_VALUE_ID = "headerValue"; let APP_INPUT_DIR_FILE_T = ` -
-
-

%NAME%

-

TODO-Date

-

TODO-Title

+ + %YEAR% + + + + %NAME% + + + + +
+ %OPTIONS%
-
+ + `; let APP_INPUT_DIR_SECTION_ID_T = "input-dir-%I%"; let APP_INPUT_DIR_SECTION_T = ` -
- %NUM% - %NAME% -
-
-
+
  • + + + %NAME% + + + + + + + + + + + + +
    YearFileOptions
    +
  • `; let APP_EDITOR_ID = "editor"; @@ -58,13 +104,26 @@ function AppComponent() { this.setupEffects = function() { let oneliners = [ "converterInput", (c) => { appResetConverterInput(this, c.converterInput) }, + "didCopyFile", (c) => { reportSuccessIf(c.didCopyFile, ' 👌') }, + "didDeleteFile", (c) => { reportFailureIf(!c.didDeleteFile, KT.APP_DELETE_FAILURE_TITLE, c.deleteFile) }, + "didDeleteFile", (c) => { reportSuccessIf(c.didDeleteFile, ' 👌') }, "didSaveEditedFiles", (c) => { reportSuccess("💾 👌") }, "editorContents", (c) => { appResetEditorContents(this, c.editorContents) }, + "externalURL", (c) => { open(c.externalURL, "_blank") }, + "filesCopyDefaultName", (c) => { setUIInputValue(APP_FILES_COPY_INPUT_ID, c.filesCopyDefaultName) }, + "filesDeleteName", (c) => { setUIText(APP_FILES_DELETE_NAME_ID, c.filesDeleteName) }, + "filesRenameName", (c) => { setUIInputValue(APP_FILES_RENAME_INPUT_ID, c.filesRenameName) }, "header", (c) => { appResetHeader(c.header) }, + "hideFileOptions", (c) => { appHideFileOptions(c.hideFileOptions) }, "inputDirs", (c) => { appDisplayInputDirSections(c.inputDirs) }, "inputMDFiles", (c) => { appDisplayInputMDFiles(c.inputMDFiles) }, "installEditor", (c) => { appInstallEditor(this) }, "installMDConverter", (c) => { appInstallMDConverter(this) }, + "isCfgValid", (c) => { reportFailureIf(!c.isCfgValid, KT.APP_CFG_ERROR_TITLE, KT.APP_CFG_ERROR_DETAILS) }, + "isFilesCopyDialogVisible", (c) => { setUIModalVisibility(APP_FILES_COPY_DIALOG_ID, c.isFilesCopyDialogVisible) }, + "isFilesDeleteDialogVisible", (c) => { setUIModalVisibility(APP_FILES_DELETE_DIALOG_ID, c.isFilesDeleteDialogVisible) }, + "isFilesRenameDialogVisible", (c) => { setUIModalVisibility(APP_FILES_RENAME_DIALOG_ID, c.isFilesRenameDialogVisible) }, + "isExternalButtonVisible", (c) => { setUIVisibility(APP_HEADER_EXTERNAL_ID, c.isExternalButtonVisible) }, "renderPage", (c) => { appRenderPage(c.renderPage) }, "request", (c) => { appLoad(c.request) }, "resizeEditor", (c) => { appResizeEditor() }, @@ -86,6 +145,8 @@ function AppComponent() { this.setupShoulds = function() { [ + KT.appShouldDeleteFile, + KT.appShouldHideFileOptions, KT.appShouldHideSplash, KT.appShouldInstallEditor, KT.appShouldInstallMDConverter, @@ -94,12 +155,26 @@ function AppComponent() { KT.appShouldParseCfg, KT.appShouldReadFile, KT.appShouldRenderPage, + KT.appShouldResetCfgValidity, KT.appShouldResetConverterInput, + KT.appShouldResetDeleteFileOrigin, + KT.appShouldResetDidCopyFile, + KT.appShouldResetDidCopyRenamedFile, + KT.appShouldResetDidDeleteFile, + KT.appShouldResetDidRenameFile, KT.appShouldResetDidSaveEditedFiles, KT.appShouldResetDidSaveFile, KT.appShouldResetDidSaveRenderedFile, KT.appShouldResetEditedFileContents, KT.appShouldResetEditorContents, + KT.appShouldResetExternalButtonVisibility, + KT.appShouldResetExternalURL, + KT.appShouldResetFilesCopyDefaultName, + KT.appShouldResetFilesCopyDialogVisibility, + KT.appShouldResetFilesDeleteDialogVisibility, + KT.appShouldResetFilesDeleteName, + KT.appShouldResetFilesRenameDialogVisibility, + KT.appShouldResetFilesRenameName, KT.appShouldResetHeader, KT.appShouldResetInputDirFiles, KT.appShouldResetInputDirs, @@ -108,6 +183,7 @@ function AppComponent() { KT.appShouldResetPage, KT.appShouldResetProjectPath, KT.appShouldResetReadFileContents, + KT.appShouldResetReadFileOrigin, KT.appShouldResizeEditor, KT.appShouldResizeRenderer, KT.appShouldSaveFileId, @@ -125,6 +201,12 @@ function AppComponent() { // +function appHideFileOptions(id) { + let menuId = APP_FILE_OPTIONS_ID_T.replaceAll("%ID%", id); + let el = deId(menuId); + UIkit.dropdown(el).hide(false); +} + function appDisplayInputDirSections(items) { var html = ""; for (let i in items) { @@ -135,20 +217,40 @@ function appDisplayInputDirSections(items) { .replaceAll("%NAME%", item) .replaceAll("%NUM%", Number(i) + 1); } - setUIText(APP_FILES_ID, html); + setUIText(APP_FILES_CONTENTS_ID, html); } function appDisplayInputMDFiles(d) { // For each section KT.forKIntVArrayString(d, (id, files) => { var html = ""; + + // Find out if this is a list of year sorted files + let isYearSorted = (KT.appYear(files[0]) != 0); + // Reverse year sorted files + if (isYearSorted) { + files.reverse() + } + var lastYear = ""; // For each file for (let i in files) { let name = files[i]; let pageId = [id, i]; + // Leave only differing years + let year = KT.appYear(name); + if (year != lastYear) { + lastYear = year; + } else { + year = ""; + } + let optionsId = APP_FILE_OPTIONS_ID_T.replaceAll("%ID%", pageId) + html += APP_INPUT_DIR_FILE_T .replaceAll("%NAME%", name) - .replaceAll("%PAGE_ID%", pageId); + .replaceAll("%OPTIONS%", APP_FILE_OPTIONS) + .replaceAll("%OPTIONS_ID%", optionsId) + .replaceAll("%PAGE_ID%", pageId) + .replaceAll("%YEAR%", year); } let sectionId = APP_INPUT_DIR_SECTION_ID_T.replaceAll("%I%", id); setUIText(sectionId, html); diff --git a/kmp/ver-browser/src/jsMain/resources/index.html b/kmp/ver-browser/src/jsMain/resources/index.html index 90e4dd0..f4df73d 100644 --- a/kmp/ver-browser/src/jsMain/resources/index.html +++ b/kmp/ver-browser/src/jsMain/resources/index.html @@ -31,9 +31,14 @@
    TODO-Header-key: TODO-Header-value - +
    + + +
    @@ -47,7 +52,59 @@ -