diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b96f737..d65bdb5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,7 +14,7 @@ koinAnnotations = "2.3.1" kotlin = "2.4.20" kotlinx-html = "0.12.0" kspPlugin = "2.3.12" -logback = "1.6.5}" +logback = "1.6.5" postgresql = "42.7.13" [libraries] diff --git a/server/buildnumber.properties b/server/buildnumber.properties index 3ded9e4..3667a5e 100644 --- a/server/buildnumber.properties +++ b/server/buildnumber.properties @@ -1,3 +1,3 @@ #auto generated -#Fri Oct 09 01:13:32 CEST 2026 -build.number=533 +#Sun Oct 11 01:02:04 CEST 2026 +build.number=551 diff --git a/server/src/main/kotlin/Version.kt b/server/src/main/kotlin/Version.kt index 66974b3..09a0b73 100644 --- a/server/src/main/kotlin/Version.kt +++ b/server/src/main/kotlin/Version.kt @@ -4,13 +4,13 @@ import net.keinekekse.version.Version object VERSION: Version { override val version = "0.2.0-alpha" - override val fullVersion = "0.2.0-alpha+build.533" + override val fullVersion = "0.2.0-alpha+build.551" override val major = 0 override val minor = 2 override val patch = 0 override val preRelease = "alpha" - override val buildMetadata = "build.533" - override val buildDate = "2026-10-08T23:13:32.459638Z" + override val buildMetadata = "build.551" + override val buildDate = "2026-10-10T23:02:04.857293Z" override val displayName = "Application Server" override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val name = "server" @@ -18,13 +18,13 @@ object VERSION: Version { } object VERSION_books: Version { override val version = "0.2.0-alpha" - override val fullVersion = "0.2.0-alpha+build.533" + override val fullVersion = "0.2.0-alpha+build.551" override val major = 0 override val minor = 2 override val patch = 0 override val preRelease = "alpha" - override val buildMetadata = "build.533" - override val buildDate = "2026-10-08T23:13:32.459742Z" + override val buildMetadata = "build.551" + override val buildDate = "2026-10-10T23:02:04.857387Z" override val displayName = "Books-DB" override val copyright = "Copyright (c) 2026 Hartmut Bauer" override val name = "books" diff --git a/server/src/main/kotlin/books/config/Routing.kt b/server/src/main/kotlin/books/config/Routing.kt index 59f90a1..23f32c4 100644 --- a/server/src/main/kotlin/books/config/Routing.kt +++ b/server/src/main/kotlin/books/config/Routing.kt @@ -16,6 +16,7 @@ import kotlinx.html.p import kotlinx.html.span import net.keinekekse.app.books.db.BookRepository import net.keinekekse.app.books.db.CalibreBookInfoRepositoryImpl +import net.keinekekse.app.books.db.schema.emptyBook import net.keinekekse.app.books.scan.getCover import net.keinekekse.app.books.scan.getIsbnFromCalibre import net.keinekekse.app.books.ui.* @@ -52,7 +53,7 @@ fun Application.configureRouting( call.application.log.info("OidcToken.Id.Principal userId: ${pp?.userInfo?.preferredUsername}") call.respondHtml { - testPage(db) + booksPage(booksRepo) } } log.info(" ### Books page done") @@ -102,11 +103,14 @@ fun Application.configureRouting( } // Edit - get(navBooksEdit.second) { - // TODO + get("${navBooksEdit.second}/{id}") { + val id = call.parameters["id"]?.toInt() ?: 0 + val book = booksRepo.bookById(id) ?: emptyBook() + call.respondHtml { bookEditPage(book, booksRepo) } } + hx.post(navBooksEdit.second) { - call.application.log.info("Edit book") + call.application.log.info("Edit book form OK") call.receiveParameters().forEach { param: String, value: List -> call.application.log.info("param: $param, value: $value") } @@ -117,6 +121,7 @@ fun Application.configureRouting( } } } + // hx.get("/isbnlookupreset") { // call.respondHtmlPartial(HttpStatusCode.OK) { // p { +"Bitte Barcode vor die Kamera halten..." } @@ -132,6 +137,13 @@ fun Application.configureRouting( // status = HttpStatusCode.OK // ) // } + + // Test-Page + get("/books/test") { + call.respondHtml { + testPage(db) + } + } // "hx." lässt den Aufruf nur per HTMX zu. Der Pfad kann nicht // direkt im Browser aufgerufen werden. hx.get("/books/testdb") { @@ -161,17 +173,17 @@ fun Application.configureRouting( } } } - - // Test 401 - authenticateWith( - keycloakSession, - roles = setOf(UserRoles.BOOKS_USER), + } + // Test 401 + authenticateWith( + keycloakSession, + roles = setOf(UserRoles.BOOKS_USER), // onUnauthorized = { onUnauthorized(call) } - ) { - get("/test") { - call.respondText("Test") - } + ) { + get("/test") { + call.respondText("Test") } } + } } diff --git a/server/src/main/kotlin/books/db/schema/BookDAO.kt b/server/src/main/kotlin/books/db/schema/BookDAO.kt index 2064709..829da11 100644 --- a/server/src/main/kotlin/books/db/schema/BookDAO.kt +++ b/server/src/main/kotlin/books/db/schema/BookDAO.kt @@ -12,11 +12,13 @@ object BooksTable : IntIdTable("books") { var originalTitle = varchar("original_title", 150).nullable() var year = integer("year") var originalYear = integer("original_year").nullable() - var language = varchar("language", 2) + var language = integer("language") var isbn = varchar("isbn", 13).index() var publisher = reference("publisher_id", PublisherTable.id) var series = optReference("series_id", SeriesTable.id) var seriesIndex = integer("seriesIndex").nullable() + var type = integer("type") + var read = bool("read").default(false) var comments = varchar("comments", 255) var parentBook = optReference("parent_book_id", id) val cover = blob("cover").nullable() @@ -31,9 +33,13 @@ class BookDAO(id: EntityID) : IntEntity(id) { var originalTitle by BooksTable.originalTitle var year by BooksTable.year var originalYear by BooksTable.originalYear - var language: Languages by BooksTable.language.transform({ it.name }, { Languages.valueOf(it) }) + var language: Languages by BooksTable.language.transform( + { it.ordinal }, + { Languages.entries[it] }) var isbn by BooksTable.isbn var publisher by PublisherDAO.Companion referencedOn BooksTable.publisher + var type by BooksTable.type.transform({ it.ordinal }, { BookType.entries[it] }) + var read by BooksTable.read var comments by BooksTable.comments var cover by BooksTable.cover @@ -58,6 +64,8 @@ fun bookDaoToModel(dao: BookDAO, includeChildren: Boolean = true): Book = Book( dao.language, dao.isbn, publisher = Publisher(dao.publisher.id.value, dao.publisher.name), + dao.type, + dao.read, dao.comments, cover = dao.cover?.bytes ?: ByteArray(0), diff --git a/server/src/main/kotlin/books/model/Artist.kt b/server/src/main/kotlin/books/model/Artist.kt index 129e3d3..91593b1 100644 --- a/server/src/main/kotlin/books/model/Artist.kt +++ b/server/src/main/kotlin/books/model/Artist.kt @@ -7,4 +7,8 @@ data class Artist( val id: Int = 0, val name: String = "", val givenName: String = "", -) +) { + override fun toString(): String { + return "$name $givenName" + } +} diff --git a/server/src/main/kotlin/books/model/Book.kt b/server/src/main/kotlin/books/model/Book.kt index f17866e..b472a4d 100644 --- a/server/src/main/kotlin/books/model/Book.kt +++ b/server/src/main/kotlin/books/model/Book.kt @@ -16,6 +16,8 @@ data class Book( val language: Languages = Languages.DE, val isbn: String = "", val publisher: Publisher, + val type: BookType = BookType.BUCH, + val read: Boolean = false, val comments: String = "", val cover: ByteArray = byteArrayOf(0), val series: Series? = null, diff --git a/server/src/main/kotlin/books/model/BookType.kt b/server/src/main/kotlin/books/model/BookType.kt new file mode 100644 index 0000000..33e9651 --- /dev/null +++ b/server/src/main/kotlin/books/model/BookType.kt @@ -0,0 +1,8 @@ +package net.keinekekse.app.books.model + +enum class BookType { + BUCH, + COMIC, + `E-BOOK`, + `E-COMIC` +} diff --git a/server/src/main/kotlin/books/ui/BookEdit.kt b/server/src/main/kotlin/books/ui/BookEdit.kt index 2af7436..fd543b9 100644 --- a/server/src/main/kotlin/books/ui/BookEdit.kt +++ b/server/src/main/kotlin/books/ui/BookEdit.kt @@ -22,6 +22,7 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { booksRepo.allTags() } + var tabCount = 1 div(classes = "flex flex-col m-8") { form { attributes.hx { @@ -31,44 +32,62 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30 ") { + div(classes = "w-30 content-center") { + +"Typ" + } + select(classes = "$cssTextInput w-40") { + name = "type" + tabIndex = tabCount++.toString() + + for (type in BookType.entries) { + option { + if (type == book.type) + selected = true + value = type.ordinal.toString() + +type.name + } + } + } + } + div(classes = "flex flex-row mb-5 h-9") { + div(classes = "w-30 content-center") { +"Titel:" } input(classes = "$cssTextInput w-150") { name = "title" - tabIndex = "1" + tabIndex = tabCount++.toString() value = book.title } } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30 ") { + div(classes = "w-30 content-center") { +"Originaltitel:" } input(classes = "$cssTextInput w-150") { name = "originaltitle" - tabIndex = "2" + tabIndex = tabCount++.toString() value = book.originalTitle ?: "" } } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30 ") { + div(classes = "w-30 content-center") { +"Untertitel:" } input(classes = "$cssTextInput w-150") { name = "subtitle" - tabIndex = "3" + tabIndex = tabCount++.toString() value = book.subTitle ?: "" } } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30") { + div(classes = "w-30 content-center") { +"Sprache" } select(classes = "$cssTextInput w-16") { name = "language" - tabIndex = "4" + tabIndex = tabCount++.toString() for (language in Languages.entries) { option { @@ -80,41 +99,41 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { } } - div(classes = "ml-9 pr-3 ") { + div(classes = "ml-9 pr-3 content-center") { +"Erscheinungsjahr:" } input(classes = "$cssTextInput w-16") { name = "year" - tabIndex = "5" + tabIndex = tabCount++.toString() value = book.year.toString() } - div(classes = "ml-8 pr-3") { + div(classes = "ml-8 pr-3 content-center") { +"Jahr der Originalausgabe:" } input(classes = "$cssTextInput w-16") { name = "originalyear" - tabIndex = "6" + tabIndex = tabCount++.toString() value = book.originalYear?.toString() ?: "" } } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30") { + div(classes = "w-30 content-center") { +"ISBN" } input(classes = "$cssTextInput w-40") { name = "isbn" - tabIndex = "7" + tabIndex = tabCount++.toString() value = book.isbn } - div(classes = "ml-14 pr-3") { + div(classes = "ml-14 pr-3 content-center") { +"Verlag" } select(classes = "$cssTextInput w-74") { name = "publisher" - tabIndex = "8" + tabIndex = tabCount++.toString() option { value = "0" @@ -130,7 +149,7 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { } } a(classes = cssFormIcon) { - tabIndex = "9" + tabIndex = tabCount++.toString() title = "Verlag hinzufügen" materialSymbolFormButton("add_box") } @@ -138,12 +157,12 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { } div(classes = "flex flex-row mb-5 h-9") { - div(classes = "w-30") { + div(classes = "w-30 content-center") { +"Serie" } select(classes = "$cssTextInput w-98") { name = "series" - tabIndex = "10" + tabIndex = tabCount++.toString() option { value = "0" @@ -159,22 +178,68 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { } } a(classes = cssFormIcon) { - tabIndex = "11" + tabIndex = tabCount++.toString() title = "Serie hinzufügen" materialSymbolFormButton("add_box") } - div(classes = "ml-13 pr-3 ") { + div(classes = "ml-13 pr-3 content-center") { +"Index" } input(classes = "$cssTextInput w-16") { name = "seriesindex" - tabIndex = "12" + tabIndex = tabCount++.toString() value = book.seriesIndex?.toString() ?: "" } } - div(classes = "flex flex-row mb-5 h-9") { + div(classes = "flex flex-row mb-5") { + div(classes = "w-30 content-center") { + +"Künstler" + } + div(classes = "overflow-auto px-1 pb-1 h-40") { + table { + thead(classes = "$cssFixedTHead ") { + tr { + th(classes = "py-3 px-2") { +"Name" } + th(classes = "py-3 px-2") { +"Rolle" } + th(classes = "py-3 px-2") { + a(classes = cssFormIcon) { + tabIndex = tabCount++.toString() + title = "Künstler hinzufügen" + materialSymbolFormButton("add_box") + } + } + } + } + tbody { + + for ((artist, role) in book.artists) { + tr { + td(classes = "border-t-1 $borderTable px-2 py-1") { + +"${artist.name}, ${artist.givenName}" + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +role.role + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + a(classes = cssFormIcon) { + tabIndex = tabCount++.toString() +// href = "${navBooksEdit.second}/${book.id}" + materialSymbolFormButton("Edit") + } + a(classes = cssFormIcon) { + tabIndex = tabCount++.toString() + materialSymbolFormButton("Delete") + } + } + } + } + } + + } + } + // artists tags // | | | | // | | | | @@ -190,7 +255,7 @@ fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) { button(classes = "$cssButtonPri block mt-8") { - tabIndex = "20" + tabIndex = tabCount++.toString() type = ButtonType.submit +"Save" } diff --git a/server/src/main/kotlin/books/ui/BookEditPage.kt b/server/src/main/kotlin/books/ui/BookEditPage.kt new file mode 100644 index 0000000..b122f10 --- /dev/null +++ b/server/src/main/kotlin/books/ui/BookEditPage.kt @@ -0,0 +1,17 @@ +package net.keinekekse.app.books.ui + +import kotlinx.html.HTML +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.model.Book + +fun HTML.bookEditPage(book: Book, booksRepo: BookRepository) { + + booksPage( + navBooksEdit.first, + mapOf(navBooks, Pair(navBooksEdit.first, "")) + ) { + + bookEdit(book, booksRepo) + } + +} diff --git a/server/src/main/kotlin/books/ui/BooksPage.kt b/server/src/main/kotlin/books/ui/BooksPage.kt new file mode 100644 index 0000000..a1c995e --- /dev/null +++ b/server/src/main/kotlin/books/ui/BooksPage.kt @@ -0,0 +1,112 @@ +package net.keinekekse.app.books.ui + +import kotlinx.coroutines.runBlocking +import kotlinx.html.* +import net.keinekekse.app.books.db.BookRepository +import net.keinekekse.app.books.model.Book + +fun HTML.booksPage(booksRepo: BookRepository) { + booksPage("Books-DB", mapOf(Pair(navBooks.first, ""))) { + // content left + div(classes = "basis-64 px-1 py-2 text-center $bgLeftColumn") { + div { + +"some content on the left " + } + + } + // content right + div(classes = "flex flex-auto $bgRightColumn") { + div(classes = "flex flex-col flex-auto") { +// div { +// +"some heading" +// } + booksTable(booksRepo) + } + } + + } + +} + +fun FlowContent.booksTable(booksRepo: BookRepository) { + val books: List = runBlocking { + booksRepo.allBooks() + } + + div(classes = "flex-auto overflow-auto px-1 pb-1") { + table { + id = "bookstable" + thead(classes = "$cssFixedTHead ") { + tr { + th(classes = "py-3 px-2") { +"Titel" } + th(classes = "py-3 px-2") { +"Autor(en)" } + th(classes = "py-3 px-2") { +"Serie" } + th(classes = "py-3 px-2") { +"Sprache" } + th(classes = "py-3 px-2") { +"Jahr" } + th(classes = "py-3 px-2") { +"Typ" } + th(classes = "py-3 px-2") { +"Gelesen" } + th(classes = "py-3 px-2") { + a { + href = navBooksNew.second + title = navBooksNew.first + materialSymbolButton( + "auto_stories", + "font-weight: bold; font-size: 2em" + ) + // library_add + } + } + } + } + tbody { + for (book in books) { + tr { + td(classes = "border-t-1 $borderTable px-2 py-1") { + +book.title + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +artistsString(book, 40) + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +(book.series?.name ?: "") // TODO add index + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +book.language.name + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +book.year.toString() + } + td(classes = "border-t-1 $borderTable px-2 py-1") { + +book.type.name + } + td(classes = "border-t-1 $borderTable px-2 py-1 text-center text-slate-400") { + title = if (book.read) "gelesen" else "ungelesen" + if (book.read) materialSymbol("check_small") //check_small || check_circle + else materialSymbol("close_small") //cancel || close || block + } + td(classes = "border-t-1 $borderTable px-2 py-1 text-nowrap") { + a { + title = if (book.read) "ungelesen" else "gelesen" + materialSymbolButton("published_with_changes") + } + a { + href = "${navBooksEdit.second}/${book.id}" + materialSymbolButton("Edit") + } + materialSymbolButton("Delete") + } + + } + + } + + } + + } + } +} + +fun artistsString(book: Book, len: Int = 0): String { + val rc = book.artists.joinToString(" & ") { it.artist.toString() } + return if (rc.length > len) rc.slice(0..len) else rc +} diff --git a/server/src/main/kotlin/books/ui/BooksTemplate.kt b/server/src/main/kotlin/books/ui/BooksTemplate.kt index 5d0479b..f1de8c5 100644 --- a/server/src/main/kotlin/books/ui/BooksTemplate.kt +++ b/server/src/main/kotlin/books/ui/BooksTemplate.kt @@ -51,6 +51,7 @@ fun HTML.booksPage(title: String, breadcrumbs: Map, block: DIV.( programName() div(classes = "ml-10 flex self-end pr-5 pt-2 border-t-1 items-baseline") { a(classes = cssHeaderLink) { + tabIndex = "0" href = "/" materialSymbol("home") // +"Start" @@ -60,6 +61,7 @@ fun HTML.booksPage(title: String, breadcrumbs: Map, block: DIV.( breadcrumbs.forEach { (name, url) -> if (url.isNotEmpty()) { a(classes = cssHeaderLink) { + tabIndex = "0" href = url +name } diff --git a/server/src/main/kotlin/books/ui/DbTest.kt b/server/src/main/kotlin/books/ui/DbTest.kt index 369151c..ef98854 100644 --- a/server/src/main/kotlin/books/ui/DbTest.kt +++ b/server/src/main/kotlin/books/ui/DbTest.kt @@ -3,6 +3,7 @@ package net.keinekekse.app.books.ui import io.ktor.util.logging.* import net.keinekekse.app.books.db.BookRepository import net.keinekekse.app.books.db.schema.* +import net.keinekekse.app.books.model.BookType import net.keinekekse.app.books.model.Languages import org.jetbrains.exposed.v1.jdbc.Database import org.jetbrains.exposed.v1.jdbc.SchemaUtils @@ -85,6 +86,8 @@ suspend fun dbTest(db: Database, booksRepo: BookRepository) { language = Languages.EN isbn = "9781617296823" publisher = pub1 + type = BookType.BUCH + read = true comments = "" } val kp = BookDAO.new { @@ -97,6 +100,7 @@ suspend fun dbTest(db: Database, booksRepo: BookRepository) { language = Languages.EN isbn = "9781617298823" publisher = pub2 + type = BookType.BUCH comments = "" } diff --git a/server/src/main/kotlin/core/ui/LandingPage.kt b/server/src/main/kotlin/core/ui/LandingPage.kt index 90d15f1..026900a 100644 --- a/server/src/main/kotlin/core/ui/LandingPage.kt +++ b/server/src/main/kotlin/core/ui/LandingPage.kt @@ -1,5 +1,6 @@ package net.keinekekse.app.core.ui +import io.ktor.htmx.html.* import io.ktor.util.logging.* import io.ktor.utils.io.* import kotlinx.html.* @@ -50,6 +51,18 @@ fun HTML.landingPage(loggedOn: Boolean) { } } div(classes = "m-8") { + button(classes = cssMainLink) { + id = "testDB" + attributes.hx { + get = "/books/testdb" +// target = "#testdb" +// swap = HxSwap.innerHtml +// trigger = "click" + } + +"Create DB" + } + } + div(classes = "m-8") { // a(classes = "p-2 font-bold text-sky-500 hover:text-sky-700 hover:bg-cyan-100 cursor-pointer") { // href = "/oidc/keycloak/login" // +"Login"