diff --git a/app/schemas/org.dueattendant149.bookshelf.data.local.room.BookDatabase/17.json b/app/schemas/org.dueattendant149.bookshelf.data.local.room.BookDatabase/17.json new file mode 100644 index 00000000..b2c4fa3d --- /dev/null +++ b/app/schemas/org.dueattendant149.bookshelf.data.local.room.BookDatabase/17.json @@ -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')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/di/AppModule.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/di/AppModule.kt index 40c47872..3103fcb0 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/di/AppModule.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/di/AppModule.kt @@ -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) diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/AudioFileEntity.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/AudioFileEntity.kt new file mode 100644 index 00000000..d7ca363d --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/AudioFileEntity.kt @@ -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, +) diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/BookEntity.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/BookEntity.kt index 7012f819..be6056a9 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/BookEntity.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/BookEntity.kt @@ -24,5 +24,22 @@ data class BookEntity( val scrollOffset: Int, val progress: Float, val image: String? = null, - @ColumnInfo(defaultValue = "[]") val categories: List + @ColumnInfo(defaultValue = "[]") val categories: List, + // 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, ) \ No newline at end of file diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/CachedFileEntity.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/CachedFileEntity.kt new file mode 100644 index 00000000..4745a9d7 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/CachedFileEntity.kt @@ -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(), +) diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/EbookFileEntity.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/EbookFileEntity.kt new file mode 100644 index 00000000..bbc60a5d --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/dto/EbookFileEntity.kt @@ -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, +) diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/AudioFileDao.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/AudioFileDao.kt new file mode 100644 index 00000000..e3974690 --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/AudioFileDao.kt @@ -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) + + @Query("SELECT * FROM audio_file WHERE bookId = :bookId ORDER BY `order`") + suspend fun getByBookId(bookId: Int): List + + @Query("SELECT * FROM audio_file WHERE bookId = :bookId AND localPath IS NOT NULL ORDER BY `order`") + suspend fun getCachedByBookId(bookId: Int): List + + @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) +} diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDao.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDao.kt index 60a98009..55e71b78 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDao.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDao.kt @@ -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 + @Delete suspend fun deleteBook(book: BookEntity): Int diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDatabase.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDatabase.kt index 4da49fb2..1495842b 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDatabase.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/BookDatabase.kt @@ -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( diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/CachedFileDao.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/CachedFileDao.kt new file mode 100644 index 00000000..f24e165f --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/CachedFileDao.kt @@ -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 + + @Query("SELECT * FROM cached_file WHERE status = :status") + suspend fun getByStatus(status: String): List + + @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) +} diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/EbookFileDao.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/EbookFileDao.kt new file mode 100644 index 00000000..997c19aa --- /dev/null +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/local/room/EbookFileDao.kt @@ -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) +} diff --git a/app/src/main/java/org/dueattendant149/bookshelf/data/mapper/book/BookMapperImpl.kt b/app/src/main/java/org/dueattendant149/bookshelf/data/mapper/book/BookMapperImpl.kt index 1944dad7..395bc2ba 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/data/mapper/book/BookMapperImpl.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/data/mapper/book/BookMapperImpl.kt @@ -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, ) } } \ No newline at end of file diff --git a/app/src/main/java/org/dueattendant149/bookshelf/domain/model/library/Book.kt b/app/src/main/java/org/dueattendant149/bookshelf/domain/model/library/Book.kt index ed0a2277..0993c05c 100644 --- a/app/src/main/java/org/dueattendant149/bookshelf/domain/model/library/Book.kt +++ b/app/src/main/java/org/dueattendant149/bookshelf/domain/model/library/Book.kt @@ -29,7 +29,17 @@ data class Book( val progress: Float, val lastOpened: Long?, - val categories: List + val categories: List, + + // 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, ) } } \ No newline at end of file