Phase 3: data layer — remote BookEntity fields, audio/ebook/cache tables, DAOs, migration 16→17
This commit is contained in:
parent
2b63ff2bec
commit
33fddc922b
13 changed files with 845 additions and 7 deletions
|
|
@ -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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(),
|
||||
)
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue