initial commit
This commit is contained in:
commit
d84861949f
92 files changed
+8425
No files matched your search
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.app.books.config
|
||||
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.plugins.di.*
|
||||
import net.keinekekse.app.books.db.BookRepository
|
||||
import net.keinekekse.app.books.db.BookRepositoryImpl
|
||||
|
||||
fun Application.configureBookDependencyInjection() {
|
||||
log.info("Configuring book dependency injection")
|
||||
|
||||
val booksDb = configureBooksDatabase()
|
||||
|
||||
dependencies {
|
||||
provide("booksDb") { booksDb }
|
||||
provide<BookRepository> { BookRepositoryImpl(booksDb) }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.keinekekse.app.books.config
|
||||
|
||||
import com.zaxxer.hikari.HikariConfig
|
||||
import com.zaxxer.hikari.HikariDataSource
|
||||
import io.ktor.server.application.*
|
||||
import net.keinekekse.app.core.config.getKeyFromConfiguration
|
||||
import org.jetbrains.exposed.v1.core.DatabaseConfig
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
|
||||
fun Application.configureBooksDatabase(): Database {
|
||||
log.info("Configuring books database")
|
||||
val config = HikariConfig().apply {
|
||||
dataSourceClassName = "org.postgresql.ds.PGSimpleDataSource"
|
||||
username = getKeyFromConfiguration("postgres.books_schema")
|
||||
password = getKeyFromConfiguration("postgres.books_pwd")
|
||||
|
||||
addDataSourceProperty("serverName", getKeyFromConfiguration("postgres.host", "localhost"))
|
||||
addDataSourceProperty("portNumber", getKeyFromConfiguration("postgres.port", "5432"))
|
||||
addDataSourceProperty("databaseName", username)
|
||||
|
||||
maximumPoolSize = 6
|
||||
isReadOnly = false
|
||||
transactionIsolation = "TRANSACTION_SERIALIZABLE"
|
||||
}
|
||||
|
||||
val dataSource = HikariDataSource(config)
|
||||
|
||||
return Database.connect(
|
||||
datasource = dataSource,
|
||||
databaseConfig = DatabaseConfig {
|
||||
// set other parameters here
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package net.keinekekse.app.books.config
|
||||
|
||||
import io.ktor.http.*
|
||||
import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.auth.oidc.*
|
||||
import io.ktor.server.html.*
|
||||
import io.ktor.server.htmx.*
|
||||
import io.ktor.server.plugins.di.annotations.*
|
||||
import io.ktor.server.request.*
|
||||
import io.ktor.server.response.*
|
||||
import io.ktor.server.routing.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.html.div
|
||||
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.scan.getCover
|
||||
import net.keinekekse.app.books.scan.getIsbnFromCalibre
|
||||
import net.keinekekse.app.books.ui.*
|
||||
import net.keinekekse.app.core.auth.UserRoles
|
||||
import net.keinekekse.app.core.config.KeycloakSession
|
||||
import net.keinekekse.app.core.config.onUnauthorized
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
|
||||
var thumbnailUrl: String? = null
|
||||
|
||||
@OptIn(ExperimentalKtorApi::class)
|
||||
fun Application.configureRouting(
|
||||
@Named("booksDb") db: Database,
|
||||
booksRepo: BookRepository,
|
||||
keycloakSession: KeycloakSession,
|
||||
// resultCallback: (String, String) -> Unit,
|
||||
) {
|
||||
log.info("Configuring books routing")
|
||||
|
||||
routing {
|
||||
authenticateWith(
|
||||
keycloakSession,
|
||||
roles = setOf(UserRoles.BOOKS_USER),
|
||||
onUnauthorized = { onUnauthorized(call) }
|
||||
) {
|
||||
// Start
|
||||
get(navBooks.second) {
|
||||
log.info(" ### Books page requested")
|
||||
|
||||
val p = call.principal
|
||||
call.application.log.info("AppPrincipal userId: ${p.userId}")
|
||||
|
||||
val pp = call.principal<OidcToken.Id>()
|
||||
call.application.log.info("OidcToken.Id.Principal userId: ${pp?.userInfo?.preferredUsername}")
|
||||
|
||||
call.respondHtml {
|
||||
testPage(db)
|
||||
}
|
||||
}
|
||||
log.info(" ### Books page done")
|
||||
|
||||
// New/ Import
|
||||
get(navBooksNew.second) {
|
||||
val books = CalibreBookInfoRepositoryImpl(db).readAllBookInfo()
|
||||
call.respondHtml {
|
||||
importPage(books)
|
||||
}
|
||||
}
|
||||
get(navBooksScan.second) {
|
||||
call.respondHtml {
|
||||
scanPage()
|
||||
}
|
||||
}
|
||||
get("${navBooksScanEdit.second}/{id}") {
|
||||
val id = call.parameters["id"]?.toInt() ?: 0
|
||||
val bookInfo = CalibreBookInfoRepositoryImpl(db).readBookInfo(id)
|
||||
call.respondHtml {
|
||||
importEditPage(bookInfo, booksRepo)
|
||||
}
|
||||
}
|
||||
delete("${navBooksScanEdit.second}/{id}") {
|
||||
val id = call.parameters["id"]?.toInt() ?: 0
|
||||
val biRepo = CalibreBookInfoRepositoryImpl(db)
|
||||
val bookInfo = biRepo.readBookInfo(id)
|
||||
if (bookInfo != null) {
|
||||
biRepo.removeBookInfo(bookInfo.id)
|
||||
}
|
||||
val books = biRepo.readAllBookInfo()
|
||||
call.respondHtml {
|
||||
importPage(books)
|
||||
}
|
||||
}
|
||||
|
||||
hx.get("${navBooksScanFetch.second}/{isbn}") {
|
||||
// val isbn = call.request.queryParameters["isbn"] ?: "" // "?isbn=..."
|
||||
val isbn = call.parameters["isbn"] ?: ""
|
||||
call.application.log.info("ISBN lookup for $isbn")
|
||||
val bookInfo = getIsbnFromCalibre(isbn, db)
|
||||
val image = getCover(isbn)
|
||||
val dataUrl = embedImageURL(image)
|
||||
call.respondHtmlPartial(status = HttpStatusCode.OK) {
|
||||
isbnResultCallback(bookInfo, dataUrl)
|
||||
}
|
||||
}
|
||||
|
||||
// Edit
|
||||
get(navBooksEdit.second) {
|
||||
// TODO
|
||||
}
|
||||
hx.post(navBooksEdit.second) {
|
||||
call.application.log.info("Edit book")
|
||||
call.receiveParameters().forEach { param: String, value: List<String> ->
|
||||
call.application.log.info("param: $param, value: $value")
|
||||
}
|
||||
|
||||
call.respondHtmlPartial(HttpStatusCode.OK) {
|
||||
p {
|
||||
+"OK"
|
||||
}
|
||||
}
|
||||
}
|
||||
// hx.get("/isbnlookupreset") {
|
||||
// call.respondHtmlPartial(HttpStatusCode.OK) {
|
||||
// p { +"Bitte Barcode vor die Kamera halten..." }
|
||||
// }
|
||||
// }
|
||||
// get("/testimage/{isbn}") {
|
||||
// val isbn = call.parameters["isbn"] ?: ""
|
||||
// val image = getCover(isbn)
|
||||
// call.application.log.info("/testimage img len ${image.size}")
|
||||
// call.respondBytes(
|
||||
// bytes = image,
|
||||
// contentType = ContentType.Image.JPEG,
|
||||
// status = HttpStatusCode.OK
|
||||
// )
|
||||
// }
|
||||
// "hx." lässt den Aufruf nur per HTMX zu. Der Pfad kann nicht
|
||||
// direkt im Browser aufgerufen werden.
|
||||
hx.get("/books/testdb") {
|
||||
log.info(" ### DB test requested")
|
||||
dbTest(db, booksRepo)
|
||||
call.respondHtmlPartial(HttpStatusCode.OK) {
|
||||
span {
|
||||
+"DB created"
|
||||
}
|
||||
}
|
||||
}
|
||||
hx.post("/books/testform") {
|
||||
log.info(" ### Form test requested")
|
||||
val inp = call.receiveParameters()["testInput"]
|
||||
log.info(" ### Form input: $inp")
|
||||
|
||||
// val formContent = call.receiveParameters()
|
||||
// formContent.forEach { key, value -> // key -> input field name
|
||||
// call.application.log.info(" ### Form key: $key") // == formContent[key]
|
||||
// for (v in value) {
|
||||
// call.application.log.info(" ### Form value(s): $v")
|
||||
// }
|
||||
// }
|
||||
call.respondHtmlPartial(HttpStatusCode.OK) {
|
||||
div {
|
||||
+"form submitted: $inp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test 401
|
||||
authenticateWith(
|
||||
keycloakSession,
|
||||
roles = setOf(UserRoles.BOOKS_USER),
|
||||
// onUnauthorized = { onUnauthorized(call) }
|
||||
) {
|
||||
get("/test") {
|
||||
call.respondText("Test")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.app.books.db
|
||||
|
||||
import net.keinekekse.app.books.model.*
|
||||
|
||||
interface BookRepository {
|
||||
suspend fun allBooks(): List<Book>
|
||||
suspend fun bookById(id: Int): Book?
|
||||
suspend fun booksBySeries(series: Series): List<Book>
|
||||
suspend fun bookByTitle(title: String): List<Book>
|
||||
suspend fun booksByArtist(artist: Artist): List<Book>
|
||||
suspend fun addBook(book: Book)
|
||||
suspend fun removeBook(id: Int): Boolean
|
||||
|
||||
suspend fun allPublishers(): List<Publisher>
|
||||
suspend fun allArtists(): List<Artist>
|
||||
suspend fun allSeries(): List<Series>
|
||||
suspend fun allTags(): List<Tag>
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.keinekekse.app.books.db
|
||||
|
||||
import net.keinekekse.app.books.db.schema.*
|
||||
import net.keinekekse.app.books.model.*
|
||||
import org.jetbrains.exposed.v1.core.and
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.like
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction
|
||||
|
||||
class BookRepositoryImpl(val booksDb: Database) : BookRepository {
|
||||
override suspend fun allBooks(): List<Book> = suspendTransaction(booksDb) {
|
||||
BookDAO.all().map(::bookDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun bookById(id: Int): Book? = suspendTransaction(booksDb) {
|
||||
BookDAO.findById(id)?.let { bookDaoToModel(it) }
|
||||
}
|
||||
|
||||
// TODO noch zu testen
|
||||
override suspend fun booksBySeries(series: Series): List<Book> = suspendTransaction(booksDb) {
|
||||
BookDAO.find { BooksTable.series eq series.id }.map(::bookDaoToModel)
|
||||
}
|
||||
|
||||
// TODO noch zu testen
|
||||
override suspend fun bookByTitle(title: String): List<Book> = suspendTransaction(booksDb) {
|
||||
val comp = "%$title%"
|
||||
BookDAO.find { BooksTable.title like comp }.map(::bookDaoToModel)
|
||||
}
|
||||
|
||||
// TODO noch zu testen
|
||||
override suspend fun booksByArtist(artist: Artist): List<Book> = suspendTransaction(booksDb) {
|
||||
BookDAO.find { (ArtistToBookTable.artistId eq artist.id) and (ArtistToBookTable.bookId eq BooksTable.id) }
|
||||
.map(::bookDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun addBook(book: Book) {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun removeBook(id: Int): Boolean {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
|
||||
override suspend fun allPublishers(): List<Publisher> = suspendTransaction(booksDb) {
|
||||
PublisherDAO.all().map(::publisherDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun allArtists(): List<Artist> = suspendTransaction(booksDb) {
|
||||
ArtistDAO.all().map(::artistDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun allSeries(): List<Series> = suspendTransaction(booksDb) {
|
||||
SeriesDAO.all().map(::seriesDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun allTags(): List<Tag> = suspendTransaction(booksDb) {
|
||||
TagsDAO.all().map(::tagDaoToModel)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package net.keinekekse.app.books.db
|
||||
|
||||
import net.keinekekse.app.books.db.schema.CalibreBookInfoDAO
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
|
||||
interface CalibreBookInfoRepository {
|
||||
suspend fun addBookInfo(bookInfo: CalibreBookInfo): CalibreBookInfoDAO
|
||||
suspend fun readAllBookInfo(): List<CalibreBookInfo>
|
||||
suspend fun readBookInfo(id: Int): CalibreBookInfo?
|
||||
suspend fun removeBookInfo(id: Int)
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package net.keinekekse.app.books.db
|
||||
|
||||
import net.keinekekse.app.books.db.schema.CalibreBookInfoDAO
|
||||
import net.keinekekse.app.books.db.schema.calibreBookInfoDaoToModel
|
||||
import net.keinekekse.app.books.db.schema.modelToCalibreBookInfoDao
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.suspendTransaction
|
||||
|
||||
class CalibreBookInfoRepositoryImpl(val booksDb: Database) : CalibreBookInfoRepository {
|
||||
override suspend fun addBookInfo(bookInfo: CalibreBookInfo) =
|
||||
suspendTransaction(booksDb) {
|
||||
modelToCalibreBookInfoDao(bookInfo)
|
||||
}
|
||||
|
||||
override suspend fun readAllBookInfo(): List<CalibreBookInfo> =
|
||||
suspendTransaction(booksDb) {
|
||||
CalibreBookInfoDAO.all().map(::calibreBookInfoDaoToModel)
|
||||
}
|
||||
|
||||
override suspend fun readBookInfo(id: Int): CalibreBookInfo? =
|
||||
suspendTransaction(booksDb) {
|
||||
CalibreBookInfoDAO.findById(id)?.let { calibreBookInfoDaoToModel(it) }
|
||||
}
|
||||
|
||||
override suspend fun removeBookInfo(id: Int) =
|
||||
suspendTransaction(booksDb) {
|
||||
CalibreBookInfoDAO[id].delete()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.Artist
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object ArtistsTable : IntIdTable("artists") {
|
||||
var name = varchar("name", 100).index()
|
||||
var givenName = varchar("given_name", 100).index()
|
||||
}
|
||||
|
||||
class ArtistDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ArtistDAO>(ArtistsTable)
|
||||
|
||||
var name by ArtistsTable.name
|
||||
var givenName by ArtistsTable.givenName
|
||||
}
|
||||
|
||||
fun artistDaoToModel(dao: ArtistDAO) = Artist(dao.id.value, dao.name, dao.givenName)
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.ArtistRole
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object ArtistRolesTable : IntIdTable("artist_roles") {
|
||||
var role = varchar("role", 50).index()
|
||||
}
|
||||
|
||||
class ArtistRoleDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ArtistRoleDAO>(ArtistRolesTable)
|
||||
|
||||
var role by ArtistRolesTable.role
|
||||
}
|
||||
|
||||
fun artistToRoleDaoToModel(dao: ArtistRoleDAO) = ArtistRole(dao.id.value, dao.role)
|
||||
@@ -0,0 +1,20 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object ArtistToBookTable : IntIdTable("artist_to_book") {
|
||||
val artistId = reference("artist_id", ArtistsTable.id).index()
|
||||
val bookId = reference("book_id", BooksTable.id).index()
|
||||
val role = reference("role_id", ArtistRolesTable.id).index()
|
||||
}
|
||||
|
||||
class ArtistToBookDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<ArtistToBookDAO>(ArtistToBookTable)
|
||||
|
||||
var artist by ArtistDAO referencedOn ArtistToBookTable.artistId
|
||||
var book by BookDAO referencedOn ArtistToBookTable.bookId
|
||||
var role by ArtistRoleDAO referencedOn ArtistToBookTable.role
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.*
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object BooksTable : IntIdTable("books") {
|
||||
var title = varchar("title", 150).index()
|
||||
var subTitle = varchar("sub_title", 150).nullable()
|
||||
var originalTitle = varchar("original_title", 150).nullable()
|
||||
var year = integer("year")
|
||||
var originalYear = integer("original_year").nullable()
|
||||
var language = varchar("language", 2)
|
||||
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 comments = varchar("comments", 255)
|
||||
var parentBook = optReference("parent_book_id", id)
|
||||
val cover = blob("cover").nullable()
|
||||
}
|
||||
|
||||
class BookDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<BookDAO>(BooksTable)
|
||||
|
||||
var title by BooksTable.title
|
||||
var subTitle by BooksTable.subTitle
|
||||
var seriesIndex by BooksTable.seriesIndex
|
||||
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 isbn by BooksTable.isbn
|
||||
var publisher by PublisherDAO.Companion referencedOn BooksTable.publisher
|
||||
var comments by BooksTable.comments
|
||||
var cover by BooksTable.cover
|
||||
|
||||
var series by SeriesDAO.Companion optionalReferencedOn BooksTable.series
|
||||
val artists by ArtistToBookDAO referrersOn ArtistToBookTable.bookId
|
||||
val tags by TagsToBookDAO referrersOn TagsToBookTable.bookId
|
||||
|
||||
// Verweis auf Sammelband (falls dieses Buch ein Teilroman ist)
|
||||
var parentBook by BookDAO optionalReferencedOn BooksTable.parentBook
|
||||
|
||||
// Enthaltene Einzelromane (falls dieses Buch der Sammelband ist)
|
||||
val containedBooks by BookDAO optionalReferrersOn BooksTable.parentBook
|
||||
}
|
||||
|
||||
fun bookDaoToModel(dao: BookDAO, includeChildren: Boolean = true): Book = Book(
|
||||
dao.id.value,
|
||||
dao.title,
|
||||
dao.subTitle,
|
||||
dao.originalTitle,
|
||||
dao.year,
|
||||
dao.originalYear,
|
||||
dao.language,
|
||||
dao.isbn,
|
||||
publisher = Publisher(dao.publisher.id.value, dao.publisher.name),
|
||||
dao.comments,
|
||||
cover = dao.cover?.bytes ?: ByteArray(0),
|
||||
|
||||
series = if (dao.series != null) Series(dao.series!!.id.value, dao.series!!.name) else null,
|
||||
seriesIndex = dao.seriesIndex,
|
||||
|
||||
dao.parentBook?.id?.value,
|
||||
containedBooks = if (includeChildren) {
|
||||
dao.containedBooks.map { bookDaoToModel(it, includeChildren = false) }
|
||||
} else {
|
||||
emptyList()
|
||||
},
|
||||
|
||||
artists = dao.artists.map {
|
||||
ArtistForBook(
|
||||
Artist(it.artist.id.value, it.artist.name, it.artist.givenName),
|
||||
ArtistRole(it.role.id.value, it.role.role)
|
||||
)
|
||||
},
|
||||
tags = dao.tags.map {
|
||||
Tag(it.tag.id.value, it.tag.tag)
|
||||
},
|
||||
)
|
||||
|
||||
fun emptyBook(title: String = "", year: Int = 0, isbn: String = ""): Book =
|
||||
Book(
|
||||
title = title,
|
||||
year = year,
|
||||
isbn = isbn,
|
||||
publisher = Publisher(0, ""),
|
||||
)
|
||||
@@ -0,0 +1,65 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
import net.keinekekse.app.books.model.Languages
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.core.statements.api.ExposedBlob
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object CalibreBookInfoTable : IntIdTable("calibre_book_info") {
|
||||
var title = varchar("title", 150)
|
||||
val authors = varchar("authors", 200)
|
||||
val publisher = varchar("publisher", 100)
|
||||
val tags = varchar("tags", 200)
|
||||
val language = varchar("language", 2)
|
||||
val published = integer("published")
|
||||
val identifiers = varchar("identifiers", 200)
|
||||
val isbn = varchar("isbn", 20)
|
||||
val comments = varchar("comments", 255)
|
||||
val cover = blob("cover")
|
||||
}
|
||||
|
||||
class CalibreBookInfoDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<CalibreBookInfoDAO>(CalibreBookInfoTable)
|
||||
|
||||
var title by CalibreBookInfoTable.title
|
||||
var authors by CalibreBookInfoTable.authors
|
||||
var publisher by CalibreBookInfoTable.publisher
|
||||
var tags by CalibreBookInfoTable.tags
|
||||
var language by CalibreBookInfoTable.language.transform({ it.name }, { Languages.valueOf(it) })
|
||||
var published by CalibreBookInfoTable.published
|
||||
var identifiers by CalibreBookInfoTable.identifiers
|
||||
var isbn by CalibreBookInfoTable.isbn
|
||||
var comments by CalibreBookInfoTable.comments
|
||||
var cover by CalibreBookInfoTable.cover
|
||||
}
|
||||
|
||||
fun calibreBookInfoDaoToModel(dao: CalibreBookInfoDAO): CalibreBookInfo = CalibreBookInfo(
|
||||
dao.id.value,
|
||||
dao.title,
|
||||
dao.authors,
|
||||
dao.publisher,
|
||||
dao.tags,
|
||||
dao.language,
|
||||
dao.published,
|
||||
dao.identifiers,
|
||||
dao.isbn,
|
||||
dao.comments,
|
||||
dao.cover.bytes
|
||||
)
|
||||
|
||||
fun modelToCalibreBookInfoDao(bookInfo: CalibreBookInfo): CalibreBookInfoDAO =
|
||||
CalibreBookInfoDAO.new {
|
||||
title = bookInfo.title.take(150)
|
||||
authors = bookInfo.authors.take(200)
|
||||
publisher = bookInfo.publisher.take(100)
|
||||
tags = bookInfo.tags.take(200)
|
||||
language = bookInfo.language
|
||||
published = bookInfo.published
|
||||
identifiers = bookInfo.identifiers.take(200)
|
||||
isbn = bookInfo.isbn.take(20)
|
||||
comments = bookInfo.comments.take(255)
|
||||
cover = ExposedBlob(bookInfo.cover)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.Publisher
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object PublisherTable : IntIdTable("publisher") {
|
||||
var name = varchar("name", 100).index()
|
||||
}
|
||||
|
||||
class PublisherDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<PublisherDAO>(PublisherTable)
|
||||
|
||||
var name by PublisherTable.name
|
||||
}
|
||||
|
||||
fun publisherDaoToModel(dao: PublisherDAO) = Publisher(dao.id.value, dao.name)
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.Series
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object SeriesTable : IntIdTable("series") {
|
||||
var name = varchar("name", 100).index()
|
||||
}
|
||||
|
||||
class SeriesDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<SeriesDAO>(SeriesTable)
|
||||
|
||||
var name by SeriesTable.name
|
||||
}
|
||||
|
||||
fun seriesDaoToModel(dao: SeriesDAO) = Series(dao.id.value, dao.name)
|
||||
@@ -0,0 +1,19 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import net.keinekekse.app.books.model.Tag
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object TagsTable : IntIdTable("tags") {
|
||||
var tag = varchar("tag", 50).index()
|
||||
}
|
||||
|
||||
class TagsDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<TagsDAO>(TagsTable)
|
||||
|
||||
var tag by TagsTable.tag
|
||||
}
|
||||
|
||||
fun tagDaoToModel(dao: TagsDAO): Tag = Tag(dao.id.value, dao.tag)
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.app.books.db.schema
|
||||
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
|
||||
import org.jetbrains.exposed.v1.dao.IntEntity
|
||||
import org.jetbrains.exposed.v1.dao.IntEntityClass
|
||||
|
||||
object TagsToBookTable : IntIdTable("tag_to_book") {
|
||||
var tagId = reference("tag_id", TagsTable.id).index()
|
||||
var bookId = reference("book_id", BooksTable.id).index()
|
||||
}
|
||||
|
||||
class TagsToBookDAO(id: EntityID<Int>) : IntEntity(id) {
|
||||
companion object : IntEntityClass<TagsToBookDAO>(TagsToBookTable)
|
||||
|
||||
var tag by TagsDAO referencedOn TagsToBookTable.tagId
|
||||
var book by BookDAO referencedOn TagsToBookTable.bookId
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Artist(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
val givenName: String = "",
|
||||
)
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ArtistRole(
|
||||
val id: Int = 0,
|
||||
val role: String = "",
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ArtistForBook(val artist: Artist, val role: ArtistRole)
|
||||
|
||||
@Serializable
|
||||
data class Book(
|
||||
val id: Int = 0,
|
||||
val title: String = "",
|
||||
val subTitle: String? = null,
|
||||
val originalTitle: String? = null,
|
||||
val year: Int = 0,
|
||||
val originalYear: Int? = null,
|
||||
val language: Languages = Languages.DE,
|
||||
val isbn: String = "",
|
||||
val publisher: Publisher,
|
||||
val comments: String = "",
|
||||
val cover: ByteArray = byteArrayOf(0),
|
||||
val series: Series? = null,
|
||||
val seriesIndex: Int? = null,
|
||||
val parentBookId: Int? = null, // ID des Sammelbands
|
||||
|
||||
val containedBooks: List<Book> = emptyList(), // Enthaltene Romane
|
||||
val artists: List<ArtistForBook> = emptyList(),
|
||||
val tags: List<Tag> = emptyList(),
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Book
|
||||
|
||||
if (id != other.id) return false
|
||||
if (year != other.year) return false
|
||||
if (originalYear != other.originalYear) return false
|
||||
if (seriesIndex != other.seriesIndex) return false
|
||||
if (parentBookId != other.parentBookId) return false
|
||||
if (title != other.title) return false
|
||||
if (subTitle != other.subTitle) return false
|
||||
if (originalTitle != other.originalTitle) return false
|
||||
if (language != other.language) return false
|
||||
if (isbn != other.isbn) return false
|
||||
if (publisher != other.publisher) return false
|
||||
if (comments != other.comments) return false
|
||||
if (!cover.contentEquals(other.cover)) return false
|
||||
if (series != other.series) return false
|
||||
if (containedBooks != other.containedBooks) return false
|
||||
if (artists != other.artists) return false
|
||||
if (tags != other.tags) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id
|
||||
result = 31 * result + year
|
||||
result = 31 * result + originalYear.hashCode()
|
||||
result = 31 * result + seriesIndex.hashCode()
|
||||
result = 31 * result + parentBookId.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + subTitle.hashCode()
|
||||
result = 31 * result + originalTitle.hashCode()
|
||||
result = 31 * result + language.hashCode()
|
||||
result = 31 * result + isbn.hashCode()
|
||||
result = 31 * result + publisher.hashCode()
|
||||
result = 31 * result + comments.hashCode()
|
||||
result = 31 * result + cover.contentHashCode()
|
||||
result = 31 * result + series.hashCode()
|
||||
result = 31 * result + containedBooks.hashCode()
|
||||
result = 31 * result + artists.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CalibreBookInfo(
|
||||
val id: Int = 0,
|
||||
val title: String,
|
||||
val authors: String,
|
||||
val publisher: String,
|
||||
val tags: String,
|
||||
val language: Languages,
|
||||
val published: Int,
|
||||
val identifiers: String,
|
||||
val isbn: String,
|
||||
val comments: String,
|
||||
val cover: ByteArray,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
enum class Languages {
|
||||
DE,
|
||||
EN,
|
||||
FR,
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Publisher(val id: Int, val name: String)
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Series(val id: Int, val name: String)
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.keinekekse.app.books.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Tag(val id: Int, val tag: String)
|
||||
@@ -0,0 +1,98 @@
|
||||
package net.keinekekse.app.books.scan
|
||||
|
||||
import io.ktor.util.logging.*
|
||||
import kotlinx.datetime.TimeZone
|
||||
import kotlinx.datetime.toLocalDateTime
|
||||
import net.keinekekse.app.books.db.CalibreBookInfoRepositoryImpl
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
import net.keinekekse.app.books.model.Languages
|
||||
import net.keinekekse.exec.CommandExecutionException
|
||||
import net.keinekekse.exec.ExecutionResult
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import java.io.File
|
||||
import kotlin.time.Instant
|
||||
|
||||
val LOG = KtorSimpleLogger("net.keinekekse.app.books.scan.fetchBookInfo")
|
||||
|
||||
suspend fun getIsbnFromCalibre(isbn: String, db: Database): String {
|
||||
try {
|
||||
val runResult = ExecutionResult(
|
||||
listOf(
|
||||
"fetch-ebook-metadata",
|
||||
"--isbn",
|
||||
isbn,
|
||||
"--cover",
|
||||
"/opt/app/download/$isbn.jpg"
|
||||
)
|
||||
)
|
||||
LOG.info("fetch-ebook-metadata for isbn: $isbn")
|
||||
LOG.info("RC = ${runResult.rc}")
|
||||
LOG.info("Output = ${runResult.stdout}")
|
||||
LOG.info("Error = ${runResult.stderr}")
|
||||
|
||||
val cali = extractData(runResult.stdout, isbn)
|
||||
|
||||
CalibreBookInfoRepositoryImpl(db).addBookInfo(cali)
|
||||
|
||||
LOG.info("CalibreBookInfo.language = ${cali.language}")
|
||||
LOG.info("CalibreBookInfo.published = ${cali.published}")
|
||||
return runResult.stdout
|
||||
} catch (e: CommandExecutionException) {
|
||||
LOG.error("Error running fetch-ebook-metadata for isbn: $isbn", e)
|
||||
return "nothing found for $isbn"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun extractData(input: String, isbn: String): CalibreBookInfo {
|
||||
val title = extractLine(input, "Title")?.trim() ?: ""
|
||||
val authors = extractLine(input, "Author.s.")?.trim() ?: ""
|
||||
val publisher = extractLine(input, "Publisher")?.trim() ?: ""
|
||||
val tags = extractLine(input, "Tags")?.trim() ?: ""
|
||||
val languages = extractLine(input, "Languages")?.trim() ?: ""
|
||||
val published = extractLine(input, "Published")?.trim() ?: ""
|
||||
val identifiers = extractLine(input, "Identifiers")?.trim() ?: ""
|
||||
val comments = extractLine(input, "Comments")?.trim() ?: ""
|
||||
|
||||
// val authorList = authors.split("&").map { it.trim() }
|
||||
val lang = Languages.valueOf(languages.uppercase().slice(0..1))
|
||||
val year = Instant.parse(published).toLocalDateTime(TimeZone.UTC).year
|
||||
|
||||
val cover = getCover(isbn)
|
||||
|
||||
return CalibreBookInfo(
|
||||
0,
|
||||
title,
|
||||
authors,
|
||||
publisher,
|
||||
tags,
|
||||
lang,
|
||||
year,
|
||||
identifiers,
|
||||
isbn,
|
||||
comments,
|
||||
cover
|
||||
)
|
||||
}
|
||||
|
||||
fun extractLine(input: String, text: String): String? {
|
||||
val pat = """$text\s*:\s*(.*)"""
|
||||
val regex = Regex(pat)
|
||||
return regex.find(input)?.groupValues?.get(1)
|
||||
// if (matchResult != null) {
|
||||
// val title = matchResult.groupValues[1]
|
||||
// LOG.info("Title: $title")
|
||||
// }
|
||||
}
|
||||
|
||||
fun getCover(isbn: String): ByteArray {
|
||||
var coverBytes: ByteArray = ByteArray(0)
|
||||
val file = File("/opt/app/download/$isbn.jpg")
|
||||
if (file.exists()) {
|
||||
LOG.info("Cover file exists")
|
||||
coverBytes = file.readBytes()
|
||||
} else {
|
||||
LOG.info("Cover file does not exist")
|
||||
}
|
||||
return coverBytes
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import io.ktor.htmx.html.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.html.*
|
||||
import net.keinekekse.app.books.db.BookRepository
|
||||
import net.keinekekse.app.books.model.*
|
||||
import net.keinekekse.app.books.model.Tag
|
||||
|
||||
fun FlowContent.bookEdit(book: Book, booksRepo: BookRepository) {
|
||||
|
||||
val publishers: List<Publisher> = runBlocking {
|
||||
booksRepo.allPublishers()
|
||||
}
|
||||
val series: List<Series> = runBlocking {
|
||||
booksRepo.allSeries()
|
||||
}
|
||||
val artists: List<Artist> = runBlocking {
|
||||
booksRepo.allArtists()
|
||||
}
|
||||
val tags: List<Tag> = runBlocking {
|
||||
booksRepo.allTags()
|
||||
}
|
||||
|
||||
div(classes = "flex flex-col m-8") {
|
||||
form {
|
||||
attributes.hx {
|
||||
post = navBooksEdit.second
|
||||
target = "#response"
|
||||
swap = "innerHTML"
|
||||
}
|
||||
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30 ") {
|
||||
+"Titel:"
|
||||
}
|
||||
input(classes = "$cssTextInput w-150") {
|
||||
name = "title"
|
||||
tabIndex = "1"
|
||||
value = book.title
|
||||
}
|
||||
|
||||
}
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30 ") {
|
||||
+"Originaltitel:"
|
||||
}
|
||||
input(classes = "$cssTextInput w-150") {
|
||||
name = "originaltitle"
|
||||
tabIndex = "2"
|
||||
value = book.originalTitle ?: ""
|
||||
}
|
||||
|
||||
}
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30 ") {
|
||||
+"Untertitel:"
|
||||
}
|
||||
input(classes = "$cssTextInput w-150") {
|
||||
name = "subtitle"
|
||||
tabIndex = "3"
|
||||
value = book.subTitle ?: ""
|
||||
}
|
||||
}
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30") {
|
||||
+"Sprache"
|
||||
}
|
||||
select(classes = "$cssTextInput w-16") {
|
||||
name = "language"
|
||||
tabIndex = "4"
|
||||
|
||||
for (language in Languages.entries) {
|
||||
option {
|
||||
if (language == book.language)
|
||||
selected = true
|
||||
value = language.ordinal.toString()
|
||||
+language.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div(classes = "ml-9 pr-3 ") {
|
||||
+"Erscheinungsjahr:"
|
||||
}
|
||||
input(classes = "$cssTextInput w-16") {
|
||||
name = "year"
|
||||
tabIndex = "5"
|
||||
value = book.year.toString()
|
||||
}
|
||||
|
||||
div(classes = "ml-8 pr-3") {
|
||||
+"Jahr der Originalausgabe:"
|
||||
}
|
||||
input(classes = "$cssTextInput w-16") {
|
||||
name = "originalyear"
|
||||
tabIndex = "6"
|
||||
value = book.originalYear?.toString() ?: ""
|
||||
}
|
||||
|
||||
}
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30") {
|
||||
+"ISBN"
|
||||
}
|
||||
input(classes = "$cssTextInput w-40") {
|
||||
name = "isbn"
|
||||
tabIndex = "7"
|
||||
value = book.isbn
|
||||
}
|
||||
|
||||
div(classes = "ml-14 pr-3") {
|
||||
+"Verlag"
|
||||
}
|
||||
select(classes = "$cssTextInput w-74") {
|
||||
name = "publisher"
|
||||
tabIndex = "8"
|
||||
|
||||
option {
|
||||
value = "0"
|
||||
+"---"
|
||||
}
|
||||
for (pub in publishers) {
|
||||
option {
|
||||
if (pub == book.publisher)
|
||||
selected = true
|
||||
value = pub.id.toString()
|
||||
+pub.name
|
||||
}
|
||||
}
|
||||
}
|
||||
a(classes = cssFormIcon) {
|
||||
tabIndex = "9"
|
||||
title = "Verlag hinzufügen"
|
||||
materialSymbolFormButton("add_box")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
div(classes = "w-30") {
|
||||
+"Serie"
|
||||
}
|
||||
select(classes = "$cssTextInput w-98") {
|
||||
name = "series"
|
||||
tabIndex = "10"
|
||||
|
||||
option {
|
||||
value = "0"
|
||||
+"---"
|
||||
}
|
||||
for (serie in series) {
|
||||
option {
|
||||
if (serie == book.series)
|
||||
selected = true
|
||||
value = serie.id.toString()
|
||||
+serie.name
|
||||
}
|
||||
}
|
||||
}
|
||||
a(classes = cssFormIcon) {
|
||||
tabIndex = "11"
|
||||
title = "Serie hinzufügen"
|
||||
materialSymbolFormButton("add_box")
|
||||
}
|
||||
|
||||
div(classes = "ml-13 pr-3 ") {
|
||||
+"Index"
|
||||
}
|
||||
input(classes = "$cssTextInput w-16") {
|
||||
name = "seriesindex"
|
||||
tabIndex = "12"
|
||||
value = book.seriesIndex?.toString() ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
// artists tags
|
||||
// | | | |
|
||||
// | | | |
|
||||
|
||||
}
|
||||
|
||||
div(classes = "flex flex-row mb-5 h-9") {
|
||||
// evtl. switch
|
||||
// Teil von Sammelband _____v / enthält (-> Link)
|
||||
// comment
|
||||
|
||||
}
|
||||
|
||||
|
||||
button(classes = "$cssButtonPri block mt-8") {
|
||||
tabIndex = "20"
|
||||
type = ButtonType.submit
|
||||
+"Save"
|
||||
}
|
||||
}
|
||||
div {
|
||||
id = "response"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import kotlinx.html.*
|
||||
import net.keinekekse.app.VERSION_books
|
||||
import net.keinekekse.app.core.ui.cssFullscreen
|
||||
import net.keinekekse.app.core.ui.stdHead
|
||||
|
||||
fun FlowContent.programName() {
|
||||
|
||||
a(classes = "flex") {
|
||||
href = navBooks.second
|
||||
title = "v ${VERSION_books.fullVersion}"
|
||||
|
||||
img(classes = "h-14 my-2 $bgHeaderIcon") {
|
||||
src = "/static/books.png"
|
||||
}
|
||||
div(classes = "self-center text-right ml-3") {
|
||||
div {
|
||||
div(classes = "text-3xl") {
|
||||
+VERSION_books.displayName
|
||||
}
|
||||
div(classes = "text-xs") {
|
||||
+VERSION_books.version
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun HTML.booksPage(title: String, breadcrumbs: Map<String, String>, block: DIV.() -> Unit = {}) {
|
||||
// val booksDb = Dependencies.getBooksDb()
|
||||
LOG.info(" ### building test page")
|
||||
|
||||
// var breadcrumbMap = emptyMap<String, String>()
|
||||
// if (breadcrumbs.isEmpty()) {
|
||||
// breadcrumbMap = breadcrumbs.plus(Pair("Start", ""))
|
||||
// } else {
|
||||
// breadcrumbMap = mapOf(Pair("Start", "/books")).plus(breadcrumbs)
|
||||
// }
|
||||
head {
|
||||
stdHead(title)
|
||||
}
|
||||
|
||||
body {
|
||||
// page
|
||||
div(classes = "$cssFullscreen flex-col") {
|
||||
// header
|
||||
div(classes = "flex flex-row justify-between py-2 px-5 $bgHeader $textHeader") {
|
||||
// header left
|
||||
div(classes = "flex flex-row") {
|
||||
programName()
|
||||
div(classes = "ml-10 flex self-end pr-5 pt-2 border-t-1 items-baseline") {
|
||||
a(classes = cssHeaderLink) {
|
||||
href = "/"
|
||||
materialSymbol("home")
|
||||
// +"Start"
|
||||
}
|
||||
div(classes = "px-3") { +">" }
|
||||
|
||||
breadcrumbs.forEach { (name, url) ->
|
||||
if (url.isNotEmpty()) {
|
||||
a(classes = cssHeaderLink) {
|
||||
href = url
|
||||
+name
|
||||
}
|
||||
div(classes = "px-3") { +">" }
|
||||
} else {
|
||||
div {
|
||||
+name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// header right TODO nötig?
|
||||
// div(classes = "flex") {
|
||||
// div(classes = "self-center text-xl") {
|
||||
// materialSymbol("Settings")
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
div(classes = "flex flex-row flex-auto overflow-hidden") {
|
||||
|
||||
block()
|
||||
|
||||
}
|
||||
|
||||
// footer
|
||||
div(classes = "p-1 text-sm $bgFooter") {
|
||||
div(classes = "col") {
|
||||
+"some footer content"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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.Languages
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.SchemaUtils
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
val logger = KtorSimpleLogger("net.keinekekse.app.books.ui")
|
||||
|
||||
suspend fun dbTest(db: Database, booksRepo: BookRepository) {
|
||||
// val db = Dependencies.getBooksDb()
|
||||
// val repo = Dependencies.getBookRepository()
|
||||
|
||||
// DB-Test
|
||||
transaction(db) {
|
||||
logger.debug("start DB reset - drop")
|
||||
// create if not exists
|
||||
SchemaUtils.drop(
|
||||
PublisherTable,
|
||||
TagsTable,
|
||||
TagsToBookTable,
|
||||
SeriesTable,
|
||||
ArtistToBookTable,
|
||||
ArtistRolesTable,
|
||||
ArtistsTable,
|
||||
BooksTable,
|
||||
CalibreBookInfoTable
|
||||
)
|
||||
logger.debug("create tables")
|
||||
SchemaUtils.create(
|
||||
PublisherTable,
|
||||
TagsTable,
|
||||
TagsToBookTable,
|
||||
SeriesTable,
|
||||
ArtistRolesTable,
|
||||
ArtistsTable,
|
||||
ArtistToBookTable,
|
||||
BooksTable,
|
||||
CalibreBookInfoTable
|
||||
)
|
||||
logger.debug("Tables created")
|
||||
val auth = ArtistRoleDAO.new { role = "Autor" }
|
||||
ArtistRoleDAO.new { role = "Übersetzer" }
|
||||
|
||||
val kia1 = ArtistDAO.new {
|
||||
name = "Aigner"
|
||||
givenName = "Sebastian"
|
||||
}
|
||||
val kia2 = ArtistDAO.new {
|
||||
name = "Elizarov"
|
||||
givenName = "Roman"
|
||||
}
|
||||
val kia3 = ArtistDAO.new {
|
||||
name = "Isakova"
|
||||
givenName = "Svetlana"
|
||||
}
|
||||
val kia4 = ArtistDAO.new {
|
||||
name = "Jemerov"
|
||||
givenName = "Dmitry"
|
||||
}
|
||||
|
||||
val kp1 = ArtistDAO.new {
|
||||
name = "Soshin"
|
||||
givenName = "Alexey"
|
||||
}
|
||||
|
||||
val pub1 = PublisherDAO.new {
|
||||
name = "Manning"
|
||||
}
|
||||
|
||||
val pub2 = PublisherDAO.new {
|
||||
name = "Packt Publishing"
|
||||
}
|
||||
|
||||
val kia = BookDAO.new {
|
||||
title = "Kotlin in Action, 2nd Edition"
|
||||
originalTitle = ""
|
||||
subTitle = ""
|
||||
year = 2024
|
||||
originalYear = 2024
|
||||
seriesIndex = 0
|
||||
language = Languages.EN
|
||||
isbn = "9781617296823"
|
||||
publisher = pub1
|
||||
comments = ""
|
||||
}
|
||||
val kp = BookDAO.new {
|
||||
title = "Kotlin Design Patterns and Best Practices, 3rd Edition"
|
||||
originalTitle = ""
|
||||
subTitle = ""
|
||||
year = 2024
|
||||
originalYear = 2024
|
||||
seriesIndex = 0
|
||||
language = Languages.EN
|
||||
isbn = "9781617298823"
|
||||
publisher = pub2
|
||||
comments = ""
|
||||
}
|
||||
|
||||
ArtistToBookDAO.new {
|
||||
book = kia
|
||||
role = auth
|
||||
artist = kia1
|
||||
}
|
||||
ArtistToBookDAO.new {
|
||||
book = kia
|
||||
role = auth
|
||||
artist = kia2
|
||||
}
|
||||
ArtistToBookDAO.new {
|
||||
book = kia
|
||||
role = auth
|
||||
artist = kia3
|
||||
}
|
||||
ArtistToBookDAO.new {
|
||||
book = kia
|
||||
role = auth
|
||||
artist = kia4
|
||||
}
|
||||
ArtistToBookDAO.new {
|
||||
book = kp
|
||||
role = auth
|
||||
artist = kp1
|
||||
}
|
||||
}
|
||||
val books = booksRepo.allBooks()
|
||||
books.forEach { book ->
|
||||
logger.debug("### ${book.title}:")
|
||||
book.artists.forEach { atb ->
|
||||
logger.debug("### ${atb.role.role}: ${atb.artist.givenName} ${atb.artist.name}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import kotlinx.html.FlowContent
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.span
|
||||
import kotlinx.html.style
|
||||
import net.keinekekse.app.books.isbnScanResultPath
|
||||
import kotlin.io.encoding.Base64
|
||||
|
||||
// * colors *
|
||||
|
||||
const val bgHeader = "bg-orange-500"
|
||||
const val textHeader = "text-orange-100"
|
||||
const val bgHeaderIcon = "bg-orange-100"
|
||||
const val bgFooter = "bg-orange-50"
|
||||
|
||||
const val bgLeftColumn = "bg-slate-200"
|
||||
const val bgRightColumn = "bg-slate-50"
|
||||
const val bgThead = "bg-slate-200"
|
||||
|
||||
const val borderTable = "border-slate-400"
|
||||
|
||||
const val bgStdButton = "bg-taupe-500"
|
||||
const val bgStdButtonHover = "hover:bg-taupe-700"
|
||||
const val bgStdButtonFocus = "focus:bg-taupe-700"
|
||||
const val bgPriButton = "bg-orange-400"
|
||||
const val bgPriButtonHover = "hover:bg-orange-600"
|
||||
const val bgPriButtonFocus = "focus:bg-orange-600"
|
||||
const val bgSecButton = "bg-indigo-400"
|
||||
const val bgSecButtonHover = "hover:bg-indigo-600"
|
||||
const val bgSecButtonFocus = "focus:bg-indigo-600"
|
||||
const val textLink = "text-orange-400"
|
||||
const val textLinkHover = "hover:text-orange-600"
|
||||
const val textLinkFocus = "focus:text-orange-600"
|
||||
const val bgLinkHover = "hover:bg-orange-200"
|
||||
const val bgLinkFocus = "focus:bg-orange-200"
|
||||
|
||||
// * button & link *
|
||||
|
||||
const val cssButton = "cursor-pointer rounded-md px-4 py-2"
|
||||
const val cssButtonStd =
|
||||
"text-white $bgStdButton $bgStdButtonHover $cssButton $bgStdButtonFocus"
|
||||
const val cssButtonPri =
|
||||
"text-white $bgPriButton $bgPriButtonHover $cssButton $bgPriButtonFocus"
|
||||
const val cssButtonSec =
|
||||
"text-white $bgSecButton $bgSecButtonHover $cssButton $bgSecButtonFocus"
|
||||
const val cssHeaderLink =
|
||||
"font-bold px-1 $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md cursor-pointer m-0 no-underline"
|
||||
const val cssLink =
|
||||
"font-bold $textLink $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md cursor-pointer m-0 no-underline"
|
||||
const val cssLinkText =
|
||||
"px-1 $cssLink"
|
||||
const val cssLinkIcon =
|
||||
"px-2 $cssLink "
|
||||
const val cssFormIcon =
|
||||
"font-bold text-slate-400 $textLinkHover $textLinkFocus $bgLinkHover $bgLinkFocus rounded-md " +
|
||||
"cursor-pointer m-0 px-2 pt-1 no-underline "
|
||||
const val cssTextInput =
|
||||
"rounded-sm border-1 border-slate-400 px-2 py-1 focus:border-orange-400 focus:border-4" +
|
||||
" invalid:focus:bg-red-200 invalid:focus:border-red-500 invalid:focus:border-5" +
|
||||
" invalid:bg-red-200 invalid:border-red-500 invalid:border-3 "
|
||||
const val _cssTextInput =
|
||||
"rounded-sm border-b-1 px-2 py-1 focus:border-b-orange-400 focus:border-b-4" +
|
||||
" invalid:focus:bg-red-200 invalid:focus:border-red-500 invalid:focus:border-5" +
|
||||
" invalid:bg-red-200 invalid:border-red-500 invalid:border-3 "
|
||||
// * fixed table header *
|
||||
|
||||
const val cssFixedTHead = "fixed-table-header $bgThead "
|
||||
|
||||
// m-1 -> margin (--spacing * 1) -> --spacing = .25em = 4px => m-1 = 4px, m-2 = 8px
|
||||
|
||||
// Navigation
|
||||
|
||||
val navBooks = "Bücher" to "/books"
|
||||
val navBooksEdit = "Buch bearbeiten" to "/books/edit"
|
||||
val navBooksNew = "Neue Bücher" to "/books/new"
|
||||
val navBooksScan = "ISBN Scan" to "/books/new/scan"
|
||||
val navBooksScanFetch = "" to isbnScanResultPath // has to be shared with :web-Project
|
||||
val navBooksScanEdit = "Scan bearbeiten" to "/books/new/editscan"
|
||||
|
||||
fun FlowContent.materialSymbol(symbol: String, styles: String = "") {
|
||||
// Den Icon via Tailwind formatieren klappt nicht. Daher muss z. B. font-size oder
|
||||
// font-weight per style= gesetzt werden.
|
||||
span(classes = "material-symbols-outlined align-text-bottom") {
|
||||
style = styles
|
||||
+symbol
|
||||
}
|
||||
}
|
||||
|
||||
fun FlowContent.materialSymbolButton(symbol: String, styles: String = "") {
|
||||
div(classes = "$cssLinkIcon inline-block") {
|
||||
materialSymbol(symbol, styles)
|
||||
}
|
||||
}
|
||||
|
||||
fun FlowContent.materialSymbolFormButton(symbol: String, styles: String = "") {
|
||||
div(classes = " inline-block") {
|
||||
materialSymbol(symbol, styles)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a byte array representing an image into a Base64 encoded Data URL string.
|
||||
*
|
||||
* Use in <img src="...">
|
||||
*
|
||||
* @param image A nullable byte array containing the binary data of the image.
|
||||
* When null or empty, an empty string is returned.
|
||||
* @return A Data URL string containing the Base64 encoded image data,
|
||||
* or an empty string if the input is null or empty.
|
||||
*/
|
||||
fun embedImageURL(image: ByteArray?): String =
|
||||
if (image != null && image.isNotEmpty()) {
|
||||
val base64String =
|
||||
Base64.encode(image) // kotlin.io.encoding.Base64
|
||||
val mimeType = "image/jpeg"
|
||||
"data:$mimeType;base64,$base64String"
|
||||
} else ""
|
||||
@@ -0,0 +1,72 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import kotlinx.html.HTML
|
||||
import kotlinx.html.div
|
||||
import kotlinx.html.img
|
||||
import kotlinx.html.span
|
||||
import net.keinekekse.app.books.db.BookRepository
|
||||
import net.keinekekse.app.books.model.Book
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
import net.keinekekse.app.books.model.Languages
|
||||
import net.keinekekse.app.books.model.Publisher
|
||||
|
||||
fun HTML.importEditPage(bookInfo: CalibreBookInfo?, booksRepo: BookRepository) {
|
||||
val dataUrl = embedImageURL(bookInfo?.cover)
|
||||
booksPage(
|
||||
navBooksScanEdit.first,
|
||||
mapOf(navBooks, navBooksNew, Pair(navBooksScanEdit.first, ""))
|
||||
) {
|
||||
div(classes = "flex flex-col flex-auto") {
|
||||
div(classes = "flex flex-row h-60 m-8 w-3/4") {
|
||||
img(classes = "inline-block h-60 pr-6 ") {
|
||||
src = dataUrl
|
||||
}
|
||||
div {
|
||||
div {
|
||||
span(classes = "inline-block w-25 ") { +"ISBN: " }
|
||||
+(bookInfo?.isbn ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Titel: " }
|
||||
+(bookInfo?.title ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Autor(en): " }
|
||||
+(bookInfo?.authors ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Verlag: " }
|
||||
+(bookInfo?.publisher ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Jahr: " }
|
||||
+bookInfo?.published.toString()
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Tags: " }
|
||||
+(bookInfo?.tags ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Identifiers: " }
|
||||
+(bookInfo?.identifiers ?: "")
|
||||
}
|
||||
div {
|
||||
span(classes = "inline-block w-25") { +"Comment: " }
|
||||
+(bookInfo?.comments ?: "")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
bookEdit(
|
||||
Book(
|
||||
title = bookInfo?.title ?: "",
|
||||
year = bookInfo?.published ?: 0,
|
||||
isbn = bookInfo?.isbn ?: "",
|
||||
publisher = Publisher(0, ""),
|
||||
language = bookInfo?.language ?: Languages.DE
|
||||
), booksRepo
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import kotlinx.html.*
|
||||
import net.keinekekse.app.books.model.CalibreBookInfo
|
||||
|
||||
fun HTML.importPage(books: List<CalibreBookInfo>) {
|
||||
// head {
|
||||
// stdHead("ISBN Scannen", false)
|
||||
// }
|
||||
// body {
|
||||
booksPage(navBooksNew.first, mapOf(navBooks, Pair(navBooksNew.first, ""))) {
|
||||
div(classes = "basis-64 px-1 py-2 text-center $bgLeftColumn") {
|
||||
div {
|
||||
+"some content on the left "
|
||||
}
|
||||
div(classes = "my-6") {
|
||||
a(classes = cssButtonPri) {
|
||||
href = navBooksScan.second
|
||||
+"ISBN Scan"
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// content right
|
||||
div(classes = "flex flex-auto $bgRightColumn") {
|
||||
div(classes = "flex flex-col flex-auto") {
|
||||
div {
|
||||
// +"some heading"
|
||||
}
|
||||
importTable(books)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FlowContent.importTable(books: List<CalibreBookInfo>) {
|
||||
|
||||
div(classes = "flex-auto overflow-auto px-1 pb-1") {
|
||||
table {
|
||||
id = "importtable"
|
||||
thead(classes = "$cssFixedTHead ") {
|
||||
tr {
|
||||
th(classes = "py-3 px-2") { +"ISBN" }
|
||||
th(classes = "py-3 px-2") { +"Titel" }
|
||||
th(classes = "py-3 px-2") { +"Autoren" }
|
||||
th(classes = "py-3 px-2") { +"Verlag" }
|
||||
th(classes = "py-3 px-2") { +"Jahr" }
|
||||
th(classes = "py-3 px-2") { +"Sprache" }
|
||||
th(classes = "py-3 px-2") { +" " }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
for ((id, title, authors, publisher, tags, language, published, identifiers, isbn, comment, cover) in books) {
|
||||
tr {
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+isbn
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+title
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+authors
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+publisher
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+published.toString()
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+language.name
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1 text-nowrap") {
|
||||
a {
|
||||
href = "${navBooksScanEdit.second}/$id"
|
||||
materialSymbolButton("Edit")
|
||||
}
|
||||
materialSymbolButton("Delete")
|
||||
}
|
||||
}
|
||||
tr {
|
||||
td {}
|
||||
td {
|
||||
colSpan = "5"
|
||||
details(classes = "mb-2") {
|
||||
attributes["name"] = "comments"
|
||||
summary(classes = "text-sm opacity-50") {
|
||||
+"Tags, Comments"
|
||||
}
|
||||
div(classes = "flex flex-row") {
|
||||
img(classes = "inline-block h-30 pr-6 ") {
|
||||
src = embedImageURL(cover)
|
||||
}
|
||||
div {
|
||||
+tags
|
||||
br {}
|
||||
+identifiers
|
||||
br {}
|
||||
+comment
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import kotlinx.html.*
|
||||
|
||||
fun HTML.scanPage() {
|
||||
// head {
|
||||
// stdHead("ISBN Scannen", false)
|
||||
// }
|
||||
// body {
|
||||
booksPage(
|
||||
navBooksScan.first,
|
||||
mapOf(navBooks, navBooksNew, Pair(navBooksScan.first, ""))
|
||||
) {
|
||||
style {
|
||||
type = "text/css"
|
||||
+"""
|
||||
button {
|
||||
background-color: var(--color-orange-400);
|
||||
color: white;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: var(--color-orange-600);
|
||||
}
|
||||
h3 {
|
||||
font-weight: bold;
|
||||
margin: 10px;
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
div(classes = "m-4 overflow-auto flex-auto") {
|
||||
h3 { +"ISBN / Barcode erfassen" }
|
||||
|
||||
// 1. Kamera-Ausgabebereich
|
||||
div {
|
||||
id = "camera"
|
||||
style = "width: 400px;"
|
||||
}
|
||||
|
||||
// 2. Zielbereich für das Buch-Ergebnis (wird per HTMX befüllt)
|
||||
div {
|
||||
id = "scan-result"
|
||||
// p { +"Bitte Barcode vor die Kamera halten..." }
|
||||
}
|
||||
}
|
||||
// 3. JavaScript zur Initialisierung & HTMX-Bridge
|
||||
script {
|
||||
unsafe {
|
||||
raw(
|
||||
"""
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
renderScanner("camera");
|
||||
});
|
||||
""".trimIndent()
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun TagConsumer<Appendable>.isbnResultCallback(bookInfo: String, dataUrl: String) {
|
||||
|
||||
div {
|
||||
pre {
|
||||
// gefundene Daten anzeigen -> Bearbeitung später
|
||||
+bookInfo
|
||||
}
|
||||
if (dataUrl.isNotEmpty()) {
|
||||
div {
|
||||
// img(src = "/books/testimage/$isbn")
|
||||
img(src = dataUrl) {
|
||||
style = "height: 200px; margin-bottom: 10px;"
|
||||
}
|
||||
}
|
||||
}
|
||||
button {
|
||||
style = "margin-left: 10px; font-size: 16px;"
|
||||
onClick = "renderScanner('camera')"
|
||||
+"Nächtes Buch"
|
||||
}
|
||||
button {
|
||||
style = "margin-left: 10px; font-size: 16px;"
|
||||
type = ButtonType.button
|
||||
onClick = "location.href='${navBooksNew.second}'"
|
||||
+"Fertig"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
@file:OptIn(ExperimentalKtorApi::class)
|
||||
|
||||
package net.keinekekse.app.books.ui
|
||||
|
||||
import io.ktor.htmx.*
|
||||
import io.ktor.htmx.html.*
|
||||
import io.ktor.util.logging.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.html.*
|
||||
import net.keinekekse.app.core.config.LOGOUT_PATH
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
import kotlin.random.Random
|
||||
|
||||
val LOG = KtorSimpleLogger("net.keinekekse.app.books.ui.TestPage")
|
||||
|
||||
//fun FlowContent.programName() {
|
||||
//
|
||||
// a(classes = "flex") {
|
||||
// href = "/"
|
||||
// title = "v ${VERSION_books.fullVersion}"
|
||||
//
|
||||
// img(classes = "h-14 my-2 $bgHeaderIcon") {
|
||||
// src = "/static/books.png"
|
||||
// }
|
||||
// div(classes = "self-center text-right ml-3") {
|
||||
// div {
|
||||
// div(classes = "text-3xl") {
|
||||
// +VERSION_books.displayName
|
||||
// }
|
||||
// div(classes = "text-xs") {
|
||||
// +VERSION_books.version
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//fun HTML.testPage(booksDb: Database) {
|
||||
//// val booksDb = Dependencies.getBooksDb()
|
||||
// LOG.info(" ### building test page")
|
||||
//
|
||||
// head {
|
||||
// stdHead("Test Page")
|
||||
// }
|
||||
//
|
||||
// body {
|
||||
// // page
|
||||
// div(classes = "$cssFullscreen flex-col") {
|
||||
// // header
|
||||
// div(classes = "flex flex-row justify-between py-2 px-5 $bgHeader $textHeader") {
|
||||
// // header left
|
||||
// div(classes = "flex flex-row") {
|
||||
// programName()
|
||||
// div(classes = "ml-10 flex self-center") {
|
||||
// +"MENÜ"
|
||||
// }
|
||||
// }
|
||||
// // header right
|
||||
// div(classes = "flex") {
|
||||
// div(classes = "self-center text-xl") {
|
||||
// materialSymbol("Settings")
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
fun HTML.testPage(booksDb: Database) {
|
||||
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 "
|
||||
}
|
||||
div {
|
||||
button(classes = "$cssButtonStd m-2") {
|
||||
attributes.hx {
|
||||
// TODO
|
||||
get = "/books/testdb"
|
||||
target = "#testdb"
|
||||
swap = HxSwap.innerHtml
|
||||
trigger = "click"
|
||||
}
|
||||
+"DB test"
|
||||
}
|
||||
}
|
||||
div {
|
||||
button(classes = "$cssButtonPri m-2 focus:outline-none focus:outline-orange-800") {
|
||||
+"primary button"
|
||||
}
|
||||
}
|
||||
div {
|
||||
+"text text "
|
||||
button(classes = "$cssLinkText ") { +"click me" }
|
||||
+" test test"
|
||||
}
|
||||
div {
|
||||
button(classes = "$cssButtonSec m-2") { +"secondary button" }
|
||||
}
|
||||
div {
|
||||
+"Database: ${booksDb.vendor} ${booksDb.version}"
|
||||
}
|
||||
div {
|
||||
form(action = LOGOUT_PATH, method = FormMethod.post) {
|
||||
button(classes = cssLinkText) {
|
||||
type = ButtonType.submit
|
||||
+"Logout"
|
||||
}
|
||||
}
|
||||
}
|
||||
div {
|
||||
id = "testdb"
|
||||
}
|
||||
div(classes = "mt-6") {
|
||||
|
||||
// script {
|
||||
// unsafe {
|
||||
// raw(
|
||||
// """
|
||||
// function toggleField(hideObj,showObj){
|
||||
// hideObj.disabled=true;
|
||||
// hideObj.style.display='none';
|
||||
// showObj.disabled=false;
|
||||
// showObj.style.display='inline';
|
||||
// showObj.focus();
|
||||
// }
|
||||
// """.trimIndent()
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// }
|
||||
|
||||
// fkt. nur so lala (onBlur!), lässt sich das hinbiegen?
|
||||
// form {
|
||||
// attributes.hx {
|
||||
// post = "/books/testform"
|
||||
// target = "#testForm"
|
||||
// swap = HxSwap.innerHtml
|
||||
// }
|
||||
// name = "BrowserSurvey"
|
||||
// select {
|
||||
// name = "browser"
|
||||
// onChange = """
|
||||
// if(this.options[this.selectedIndex].value=='customOption'){
|
||||
// toggleField(this,this.nextSibling);
|
||||
// this.selectedIndex='0';
|
||||
// }
|
||||
// """.trimIndent()
|
||||
// label { +"Browser: " }
|
||||
// option {
|
||||
// value = "customOption"
|
||||
// +"[type a custom value]"
|
||||
// }
|
||||
// option {
|
||||
// value = "vChrome"
|
||||
// +"Chrome"
|
||||
// }
|
||||
// option {
|
||||
// value = "vFirefox"
|
||||
// +"Firefox"
|
||||
// }
|
||||
// option {
|
||||
// value = "vInternetExplorer"
|
||||
// +"Internet Explorer"
|
||||
// }
|
||||
// option {
|
||||
// value = "vOpera"
|
||||
// +"Opera"
|
||||
// }
|
||||
// }
|
||||
// input {
|
||||
// type = InputType.text
|
||||
// name = "browser"
|
||||
// style = "display:none;"
|
||||
// disabled = true
|
||||
// onBlur = """
|
||||
// if(this.value==null || this.value==''){toggleField(this,this.previousSibling);}
|
||||
// """.trimIndent()
|
||||
// }
|
||||
// input(type = InputType.submit) {
|
||||
// value = "Submit"
|
||||
// }
|
||||
// }
|
||||
|
||||
// klappt, es fehlt halt die "value" -> alpha search ?!
|
||||
form {
|
||||
attributes.hx {
|
||||
post = "/books/testform"
|
||||
target = "#testForm"
|
||||
swap = HxSwap.innerHtml
|
||||
}
|
||||
input(
|
||||
classes = cssTextInput,
|
||||
type = InputType.text
|
||||
) {
|
||||
name = "testInput"
|
||||
list = "lst"
|
||||
minLength = "2"
|
||||
|
||||
dataList {
|
||||
id = "lst"
|
||||
option(content = "alpha")
|
||||
option(content = "beta")
|
||||
option(content = "gamma")
|
||||
}
|
||||
}
|
||||
input(type = InputType.submit) {
|
||||
value = "Submit"
|
||||
}
|
||||
}
|
||||
div {
|
||||
id = "testForm"
|
||||
}
|
||||
}
|
||||
}
|
||||
// content right
|
||||
div(classes = "flex flex-auto $bgRightColumn") {
|
||||
div(classes = "flex flex-col flex-auto") {
|
||||
// div {
|
||||
// +"some heading"
|
||||
// }
|
||||
testTable()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// // footer
|
||||
// div(classes = "p-1 text-sm $bgFooter") {
|
||||
// div(classes = "col") {
|
||||
// +"some footer content"
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//// }
|
||||
//
|
||||
// }
|
||||
//}
|
||||
|
||||
fun FlowContent.testTable() {
|
||||
div(classes = "flex-auto overflow-auto px-1 pb-1") {
|
||||
table {
|
||||
id = "testtable"
|
||||
thead(classes = "$cssFixedTHead ") {
|
||||
tr {
|
||||
th(classes = "py-3 px-2") { +"#" }
|
||||
th(classes = "py-3 px-2") { +"Alias" }
|
||||
th(classes = "py-3 px-2") { +"Score" }
|
||||
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 (i in 0..100) {
|
||||
tr {
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+i.toString()
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+Random.nextLong().toString()
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1") {
|
||||
+Random.nextLong().toString()
|
||||
}
|
||||
td(classes = "border-t-1 $borderTable px-2 py-1 text-nowrap") {
|
||||
materialSymbolButton("Edit")
|
||||
materialSymbolButton("Delete")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user