Phase 3: data layer — remote BookEntity fields, audio/ebook/cache tables, DAOs, migration 16→17

This commit is contained in:
Atte149 2026-06-15 21:29:26 +03:00
parent 2b63ff2bec
commit 33fddc922b
13 changed files with 845 additions and 7 deletions

View file

@ -0,0 +1,486 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "708de483dd1541f0f65fed40884eadfe",
"entities": [
{
"tableName": "BookEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `author` TEXT NOT NULL, `description` TEXT, `filePath` TEXT NOT NULL, `scrollIndex` INTEGER NOT NULL, `scrollOffset` INTEGER NOT NULL, `progress` REAL NOT NULL, `image` TEXT, `categories` TEXT NOT NULL DEFAULT '[]', `remoteId` TEXT NOT NULL DEFAULT '', `libraryId` TEXT NOT NULL DEFAULT '', `mediaType` TEXT NOT NULL DEFAULT '', `hasAudio` INTEGER NOT NULL DEFAULT 0, `hasEbook` INTEGER NOT NULL DEFAULT 0, `audioDuration` INTEGER NOT NULL DEFAULT 0, `coverUrl` TEXT NOT NULL DEFAULT '', `lastSyncedAt` INTEGER NOT NULL DEFAULT 0)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "author",
"columnName": "author",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "description",
"columnName": "description",
"affinity": "TEXT"
},
{
"fieldPath": "filePath",
"columnName": "filePath",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "scrollIndex",
"columnName": "scrollIndex",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "scrollOffset",
"columnName": "scrollOffset",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "image",
"columnName": "image",
"affinity": "TEXT"
},
{
"fieldPath": "categories",
"columnName": "categories",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'[]'"
},
{
"fieldPath": "remoteId",
"columnName": "remoteId",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "libraryId",
"columnName": "libraryId",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "mediaType",
"columnName": "mediaType",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "hasAudio",
"columnName": "hasAudio",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "hasEbook",
"columnName": "hasEbook",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "audioDuration",
"columnName": "audioDuration",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
},
{
"fieldPath": "coverUrl",
"columnName": "coverUrl",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "''"
},
{
"fieldPath": "lastSyncedAt",
"columnName": "lastSyncedAt",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "0"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "HistoryEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `time` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "time",
"columnName": "time",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "ColorPresetEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `backgroundColor` INTEGER NOT NULL, `fontColor` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `order` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER"
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "backgroundColor",
"columnName": "backgroundColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fontColor",
"columnName": "fontColor",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "isSelected",
"columnName": "isSelected",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "CategoryEntity",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `sortOrder` TEXT NOT NULL DEFAULT 'LAST_READ', `sortOrderDescending` INTEGER NOT NULL DEFAULT 1)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "sortOrder",
"columnName": "sortOrder",
"affinity": "TEXT",
"notNull": true,
"defaultValue": "'LAST_READ'"
},
{
"fieldPath": "sortOrderDescending",
"columnName": "sortOrderDescending",
"affinity": "INTEGER",
"notNull": true,
"defaultValue": "1"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
}
},
{
"tableName": "audio_file",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `fileId` TEXT NOT NULL, `remotePath` TEXT, `localPath` TEXT, `duration` REAL NOT NULL, `order` INTEGER NOT NULL, FOREIGN KEY(`bookId`) REFERENCES `BookEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileId",
"columnName": "fileId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT"
},
{
"fieldPath": "localPath",
"columnName": "localPath",
"affinity": "TEXT"
},
{
"fieldPath": "duration",
"columnName": "duration",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "order",
"columnName": "order",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_audio_file_bookId",
"unique": false,
"columnNames": [
"bookId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_audio_file_bookId` ON `${TABLE_NAME}` (`bookId`)"
}
],
"foreignKeys": [
{
"table": "BookEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bookId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "ebook_file",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `fileId` TEXT NOT NULL, `format` TEXT NOT NULL, `remotePath` TEXT, `localPath` TEXT, FOREIGN KEY(`bookId`) REFERENCES `BookEntity`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "fileId",
"columnName": "fileId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "format",
"columnName": "format",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remotePath",
"columnName": "remotePath",
"affinity": "TEXT"
},
{
"fieldPath": "localPath",
"columnName": "localPath",
"affinity": "TEXT"
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_ebook_file_bookId",
"unique": false,
"columnNames": [
"bookId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_ebook_file_bookId` ON `${TABLE_NAME}` (`bookId`)"
}
],
"foreignKeys": [
{
"table": "BookEntity",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"bookId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "cached_file",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookId` INTEGER NOT NULL, `type` TEXT NOT NULL, `remoteUrl` TEXT NOT NULL, `localPath` TEXT, `status` TEXT NOT NULL, `progress` REAL NOT NULL, `createdAt` INTEGER NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "bookId",
"columnName": "bookId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "type",
"columnName": "type",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "remoteUrl",
"columnName": "remoteUrl",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "localPath",
"columnName": "localPath",
"affinity": "TEXT"
},
{
"fieldPath": "status",
"columnName": "status",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "progress",
"columnName": "progress",
"affinity": "REAL",
"notNull": true
},
{
"fieldPath": "createdAt",
"columnName": "createdAt",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_cached_file_bookId",
"unique": false,
"columnNames": [
"bookId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_cached_file_bookId` ON `${TABLE_NAME}` (`bookId`)"
},
{
"name": "index_cached_file_type",
"unique": false,
"columnNames": [
"type"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_cached_file_type` ON `${TABLE_NAME}` (`type`)"
}
]
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '708de483dd1541f0f65fed40884eadfe')"
]
}
}

View file

@ -60,6 +60,7 @@ object AppModule {
DatabaseHelper.MANUAL_MIGRATION_13_14, // remove nullability from ColorPresetEntity
DatabaseHelper.MANUAL_MIGRATION_14_15, // remove author nullability from BookEntity
DatabaseHelper.MANUAL_MIGRATION_15_16, // merge CategoryEntity and CategorySortEntity
DatabaseHelper.MANUAL_MIGRATION_16_17, // add remote bookshelf fields + audio/ebook/cache tables
).allowMainThreadQueries().build().also { database ->
// Additional Migrations
DatabaseHelper.AUTO_MIGRATION_7_8.removeBooksDir(app)

View file

@ -0,0 +1,35 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.dto
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "audio_file",
foreignKeys = [
ForeignKey(
entity = BookEntity::class,
parentColumns = ["id"],
childColumns = ["bookId"],
onDelete = ForeignKey.CASCADE,
)
],
indices = [Index("bookId")]
)
data class AudioFileEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val bookId: Int,
val fileId: String,
val remotePath: String? = null,
val localPath: String? = null,
val duration: Double = 0.0,
val order: Int = 0,
)

View file

@ -24,5 +24,22 @@ data class BookEntity(
val scrollOffset: Int,
val progress: Float,
val image: String? = null,
@ColumnInfo(defaultValue = "[]") val categories: List<Int>
@ColumnInfo(defaultValue = "[]") val categories: List<Int>,
// Remote / bookshelf fields
@ColumnInfo(defaultValue = "")
val remoteId: String = "",
@ColumnInfo(defaultValue = "")
val libraryId: String = "",
@ColumnInfo(defaultValue = "")
val mediaType: String = "",
@ColumnInfo(defaultValue = "0")
val hasAudio: Boolean = false,
@ColumnInfo(defaultValue = "0")
val hasEbook: Boolean = false,
@ColumnInfo(defaultValue = "0")
val audioDuration: Long = 0L,
@ColumnInfo(defaultValue = "")
val coverUrl: String = "",
@ColumnInfo(defaultValue = "0")
val lastSyncedAt: Long = 0L,
)

View file

@ -0,0 +1,27 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.dto
import androidx.room.Entity
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "cached_file",
indices = [Index("bookId"), Index("type")]
)
data class CachedFileEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val bookId: Int,
val type: String,
val remoteUrl: String,
val localPath: String? = null,
val status: String = "pending",
val progress: Float = 0f,
val createdAt: Long = System.currentTimeMillis(),
)

View file

@ -0,0 +1,34 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.dto
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Index
import androidx.room.PrimaryKey
@Entity(
tableName = "ebook_file",
foreignKeys = [
ForeignKey(
entity = BookEntity::class,
parentColumns = ["id"],
childColumns = ["bookId"],
onDelete = ForeignKey.CASCADE,
)
],
indices = [Index("bookId")]
)
data class EbookFileEntity(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val bookId: Int,
val fileId: String,
val format: String = "",
val remotePath: String? = null,
val localPath: String? = null,
)

View file

@ -0,0 +1,39 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.room
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import org.dueattendant149.bookshelf.data.local.dto.AudioFileEntity
@Dao
interface AudioFileDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(file: AudioFileEntity)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(files: List<AudioFileEntity>)
@Query("SELECT * FROM audio_file WHERE bookId = :bookId ORDER BY `order`")
suspend fun getByBookId(bookId: Int): List<AudioFileEntity>
@Query("SELECT * FROM audio_file WHERE bookId = :bookId AND localPath IS NOT NULL ORDER BY `order`")
suspend fun getCachedByBookId(bookId: Int): List<AudioFileEntity>
@Update
suspend fun update(file: AudioFileEntity)
@Delete
suspend fun delete(file: AudioFileEntity)
@Query("DELETE FROM audio_file WHERE bookId = :bookId")
suspend fun deleteByBookId(bookId: Int)
}

View file

@ -32,6 +32,12 @@ interface BookDao {
@Query("SELECT * FROM bookentity WHERE id=:id")
suspend fun findBookById(id: Int): BookEntity?
@Query("SELECT * FROM bookentity WHERE remoteId=:remoteId LIMIT 1")
suspend fun findBookByRemoteId(remoteId: String): BookEntity?
@Query("SELECT * FROM bookentity WHERE libraryId=:libraryId")
suspend fun findBooksByLibraryId(libraryId: String): List<BookEntity>
@Delete
suspend fun deleteBook(book: BookEntity): Int

View file

@ -15,9 +15,12 @@ import androidx.room.RoomDatabase
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import org.dueattendant149.bookshelf.data.local.dto.AudioFileEntity
import org.dueattendant149.bookshelf.data.local.dto.BookEntity
import org.dueattendant149.bookshelf.data.local.dto.CachedFileEntity
import org.dueattendant149.bookshelf.data.local.dto.CategoryEntity
import org.dueattendant149.bookshelf.data.local.dto.ColorPresetEntity
import org.dueattendant149.bookshelf.data.local.dto.EbookFileEntity
import org.dueattendant149.bookshelf.data.local.dto.HistoryEntity
import java.io.File
@ -26,9 +29,12 @@ import java.io.File
BookEntity::class,
HistoryEntity::class,
ColorPresetEntity::class,
CategoryEntity::class
CategoryEntity::class,
AudioFileEntity::class,
EbookFileEntity::class,
CachedFileEntity::class,
],
version = 16,
version = 17,
autoMigrations = [
AutoMigration(1, 2),
AutoMigration(2, 3),
@ -53,6 +59,9 @@ abstract class BookDatabase : RoomDatabase() {
abstract val historyDao: HistoryDao
abstract val colorPresetDao: ColorPresetDao
abstract val categoryDao: CategoryDao
abstract val audioFileDao: AudioFileDao
abstract val ebookFileDao: EbookFileDao
abstract val cachedFileDao: CachedFileDao
}
@Suppress("ClassName")
@ -194,6 +203,87 @@ object DatabaseHelper {
@DeleteTable("CategorySortEntity")
class AUTO_MIGRATION_15_16 : AutoMigrationSpec
val MANUAL_MIGRATION_16_17 = object : Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
// Add remote / bookshelf columns to BookEntity
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN remoteId TEXT NOT NULL DEFAULT ''"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN libraryId TEXT NOT NULL DEFAULT ''"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN mediaType TEXT NOT NULL DEFAULT ''"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN hasAudio INTEGER NOT NULL DEFAULT 0"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN hasEbook INTEGER NOT NULL DEFAULT 0"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN audioDuration INTEGER NOT NULL DEFAULT 0"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN coverUrl TEXT NOT NULL DEFAULT ''"
)
database.execSQL(
"ALTER TABLE BookEntity ADD COLUMN lastSyncedAt INTEGER NOT NULL DEFAULT 0"
)
// Audio files
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS audio_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
bookId INTEGER NOT NULL,
fileId TEXT NOT NULL,
remotePath TEXT DEFAULT NULL,
localPath TEXT DEFAULT NULL,
duration REAL NOT NULL DEFAULT 0.0,
`order` INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY(bookId) REFERENCES BookEntity(id) ON DELETE CASCADE
)
""".trimIndent()
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_audio_file_bookId ON audio_file(bookId)")
// Ebook files
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS ebook_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
bookId INTEGER NOT NULL,
fileId TEXT NOT NULL,
format TEXT NOT NULL DEFAULT '',
remotePath TEXT DEFAULT NULL,
localPath TEXT DEFAULT NULL,
FOREIGN KEY(bookId) REFERENCES BookEntity(id) ON DELETE CASCADE
)
""".trimIndent()
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_ebook_file_bookId ON ebook_file(bookId)")
// Cached files
database.execSQL(
"""
CREATE TABLE IF NOT EXISTS cached_file (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
bookId INTEGER NOT NULL,
type TEXT NOT NULL,
remoteUrl TEXT NOT NULL,
localPath TEXT DEFAULT NULL,
status TEXT NOT NULL DEFAULT 'pending',
progress REAL NOT NULL DEFAULT 0.0,
createdAt INTEGER NOT NULL DEFAULT 0
)
""".trimIndent()
)
database.execSQL("CREATE INDEX IF NOT EXISTS index_cached_file_bookId ON cached_file(bookId)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_cached_file_type ON cached_file(type)")
}
}
val MANUAL_MIGRATION_15_16 = object : Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL(

View file

@ -0,0 +1,36 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.room
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import org.dueattendant149.bookshelf.data.local.dto.CachedFileEntity
@Dao
interface CachedFileDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(file: CachedFileEntity)
@Query("SELECT * FROM cached_file WHERE bookId = :bookId AND type = :type")
suspend fun getByBookIdAndType(bookId: Int, type: String): List<CachedFileEntity>
@Query("SELECT * FROM cached_file WHERE status = :status")
suspend fun getByStatus(status: String): List<CachedFileEntity>
@Update
suspend fun update(file: CachedFileEntity)
@Delete
suspend fun delete(file: CachedFileEntity)
@Query("DELETE FROM cached_file WHERE bookId = :bookId")
suspend fun deleteByBookId(bookId: Int)
}

View file

@ -0,0 +1,33 @@
/*
* Book's Story free and open-source Material You eBook reader.
* Copyright (C) 2024-2026 Acclorite
* SPDX-License-Identifier: GPL-3.0-only
*/
package org.dueattendant149.bookshelf.data.local.room
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import org.dueattendant149.bookshelf.data.local.dto.EbookFileEntity
@Dao
interface EbookFileDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(file: EbookFileEntity)
@Query("SELECT * FROM ebook_file WHERE bookId = :bookId LIMIT 1")
suspend fun getByBookId(bookId: Int): EbookFileEntity?
@Update
suspend fun update(file: EbookFileEntity)
@Delete
suspend fun delete(file: EbookFileEntity)
@Query("DELETE FROM ebook_file WHERE bookId = :bookId")
suspend fun deleteByBookId(bookId: Int)
}

View file

@ -25,7 +25,15 @@ class BookMapperImpl @Inject constructor() : BookMapper {
author = book.author.getAsString() ?: "",
description = book.description,
image = book.coverImage?.toString(),
categories = book.categories
categories = book.categories,
remoteId = book.remoteId,
libraryId = book.libraryId,
mediaType = book.mediaType,
hasAudio = book.hasAudio,
hasEbook = book.hasEbook,
audioDuration = book.audioDuration,
coverUrl = book.coverUrl,
lastSyncedAt = book.lastSyncedAt,
)
}
@ -44,7 +52,15 @@ class BookMapperImpl @Inject constructor() : BookMapper {
filePath = bookEntity.filePath,
lastOpened = null,
coverImage = bookEntity.image?.toUri(),
categories = bookEntity.categories
categories = bookEntity.categories,
remoteId = bookEntity.remoteId,
libraryId = bookEntity.libraryId,
mediaType = bookEntity.mediaType,
hasAudio = bookEntity.hasAudio,
hasEbook = bookEntity.hasEbook,
audioDuration = bookEntity.audioDuration,
coverUrl = bookEntity.coverUrl,
lastSyncedAt = bookEntity.lastSyncedAt,
)
}
}

View file

@ -29,7 +29,17 @@ data class Book(
val progress: Float,
val lastOpened: Long?,
val categories: List<Int>
val categories: List<Int>,
// Remote / bookshelf fields
val remoteId: String = "",
val libraryId: String = "",
val mediaType: String = "",
val hasAudio: Boolean = false,
val hasEbook: Boolean = false,
val audioDuration: Long = 0L,
val coverUrl: String = "",
val lastSyncedAt: Long = 0L,
) : Parcelable {
companion object {
val default = Book(
@ -43,7 +53,15 @@ data class Book(
scrollOffset = 0,
progress = 0f,
lastOpened = null,
categories = emptyList()
categories = emptyList(),
remoteId = "",
libraryId = "",
mediaType = "",
hasAudio = false,
hasEbook = false,
audioDuration = 0L,
coverUrl = "",
lastSyncedAt = 0L,
)
}
}