diff --git a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt index 8e6ab1e..c71e3e4 100644 --- a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt +++ b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseInstaller.kt @@ -29,8 +29,9 @@ object CourseInstaller { // through this viewer (served same-origin from /pdfjs/web/viewer.html). private const val PDFJS_ASSET = "pdfjs.zip" - // Bumped to 2 to force existing installs to re-extract with the PDF viewer. - private const val INSTALL_VERSION = 2 + // Bumped to 3 to force existing installs to re-extract and regenerate the + // navigation shell with the corrected lesson-item ordering. + private const val INSTALL_VERSION = 3 private const val MARKER = ".installed-v$INSTALL_VERSION" private val installLock = Any() diff --git a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt index 2fc018d..875bfdc 100644 --- a/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt +++ b/ai-literacy-course/src/main/kotlin/org/appdevforall/ailiteracycourse/CourseShell.kt @@ -36,25 +36,25 @@ object CourseShell { private enum class Kind { VIDEO, ACTIVITY, PDF } - private data class Item(val title: String, val href: String, val kind: Kind) + private data class Item(val order: Int, val title: String, val href: String, val kind: Kind) private fun itemsOf(root: File, section: File): List { val entries = section.listFiles() ?: return emptyList() return entries.mapNotNull { entry -> when { entry.isFile && entry.extension.equals("mp4", true) -> - Item(itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.VIDEO) + Item(leadingNumber(entry.nameWithoutExtension), itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.VIDEO) entry.isFile && entry.extension.equals("pdf", true) -> - Item(itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.PDF) + Item(leadingNumber(entry.nameWithoutExtension), itemTitle(entry.nameWithoutExtension), rel(root, entry), Kind.PDF) entry.isDirectory -> findIndexHtml(entry)?.let { index -> - Item(itemTitle(entry.name), rel(root, index), Kind.ACTIVITY) + Item(leadingNumber(entry.name), itemTitle(entry.name), rel(root, index), Kind.ACTIVITY) } else -> null } - }.sortedWith(compareBy({ leadingNumber(it.title) }, { it.title })) + }.sortedWith(compareBy({ it.order }, { it.title })) } /** Shallowest index.html under [dir] (activities nest theirs differently). */ @@ -124,8 +124,14 @@ object CourseShell { private fun itemTitle(raw: String): String = titleCase(raw.replace(Regex("^\\d+-"), "")) - private fun leadingNumber(title: String): Int = - Regex("(\\d+)").find(title)?.groupValues?.get(1)?.toIntOrNull() ?: Int.MAX_VALUE + /** + * The leading "12-" ordering prefix of a raw file/dir name, as an int. + * Anchored to the start so a number later in the name (e.g. the "1" in + * "5-lesson-1-recap-quiz") can't be mistaken for the ordering key. + * Unnumbered entries sort last. + */ + private fun leadingNumber(rawName: String): Int = + Regex("^(\\d+)").find(rawName)?.groupValues?.get(1)?.toIntOrNull() ?: Int.MAX_VALUE private fun titleCase(raw: String): String = raw.split('-', ' ', '_').filter { it.isNotBlank() }.joinToString(" ") { w ->