Update database schema: foreign keys and indices
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="org.koitharu.kotatsu">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
@@ -18,7 +19,8 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
android:usesCleartextTraffic="true"
|
||||
tools:ignore="UnusedAttribute">
|
||||
<activity android:name=".ui.main.MainActivity">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
@@ -54,7 +56,9 @@
|
||||
|
||||
<provider
|
||||
android:name=".ui.search.MangaSuggestionsProvider"
|
||||
android:authorities="${applicationId}.MangaSuggestionsProvider" />
|
||||
android:authorities="${applicationId}.MangaSuggestionsProvider"
|
||||
android:exported="false" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.files"
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.migrations.Migration1To2
|
||||
import org.koitharu.kotatsu.core.local.CbzFetcher
|
||||
import org.koitharu.kotatsu.core.local.PagesCache
|
||||
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
|
||||
@@ -109,5 +110,5 @@ class KotatsuApp : Application() {
|
||||
applicationContext,
|
||||
MangaDatabase::class.java,
|
||||
"kotatsu-db"
|
||||
)
|
||||
).addMigrations(Migration1To2)
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.*
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class
|
||||
], version = 1
|
||||
], version = 2
|
||||
)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
|
||||
@@ -2,10 +2,26 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(tableName = "favourites", primaryKeys = ["manga_id", "category_id"])
|
||||
@Entity(
|
||||
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = FavouriteCategoryEntity::class,
|
||||
parentColumns = ["category_id"],
|
||||
childColumns = ["category_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class FavouriteEntity(
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "category_id") val categoryId: Long,
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long
|
||||
)
|
||||
@@ -2,24 +2,34 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.model.MangaHistory
|
||||
import java.util.*
|
||||
|
||||
@Entity(tableName = "history")
|
||||
@Entity(
|
||||
tableName = "history", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class HistoryEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "page") val page: Int
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long = System.currentTimeMillis(),
|
||||
@ColumnInfo(name = "updated_at") val updatedAt: Long,
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
@ColumnInfo(name = "page") val page: Int
|
||||
) {
|
||||
|
||||
fun toMangaHistory() = MangaHistory(
|
||||
createdAt = Date(createdAt),
|
||||
updatedAt = Date(updatedAt),
|
||||
chapterId = chapterId,
|
||||
page = page
|
||||
)
|
||||
fun toMangaHistory() = MangaHistory(
|
||||
createdAt = Date(createdAt),
|
||||
updatedAt = Date(updatedAt),
|
||||
chapterId = chapterId,
|
||||
page = page
|
||||
)
|
||||
}
|
||||
@@ -2,9 +2,18 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "preferences")
|
||||
@Entity(
|
||||
tableName = "preferences", foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)]
|
||||
)
|
||||
data class MangaPrefsEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
|
||||
@@ -2,9 +2,25 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
|
||||
@Entity(tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"])
|
||||
@Entity(
|
||||
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
childColumns = ["manga_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
),
|
||||
ForeignKey(
|
||||
entity = TagEntity::class,
|
||||
parentColumns = ["tag_id"],
|
||||
childColumns = ["tag_id"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
)
|
||||
]
|
||||
)
|
||||
data class MangaTagsEntity(
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id") val tagId: Long
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
|
||||
)
|
||||
@@ -0,0 +1,54 @@
|
||||
package org.koitharu.kotatsu.core.db.migrations
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
|
||||
object Migration1To2 : Migration(1, 2) {
|
||||
/**
|
||||
* Adding foreign keys
|
||||
*/
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
/* manga_tags */
|
||||
database.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
|
||||
"PRIMARY KEY(manga_id, tag_id), " +
|
||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
|
||||
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||
)
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
|
||||
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
|
||||
database.execSQL("DROP TABLE manga_tags")
|
||||
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
|
||||
/* favourites */
|
||||
database.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
|
||||
"PRIMARY KEY(manga_id, category_id), " +
|
||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
|
||||
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||
)
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
|
||||
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
|
||||
database.execSQL("DROP TABLE favourites")
|
||||
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
|
||||
/* history */
|
||||
database.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
|
||||
"PRIMARY KEY(manga_id), " +
|
||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||
)
|
||||
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
|
||||
database.execSQL("DROP TABLE history")
|
||||
database.execSQL("ALTER TABLE history_tmp RENAME TO history")
|
||||
/* preferences */
|
||||
database.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
|
||||
" PRIMARY KEY(manga_id), " +
|
||||
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
|
||||
)
|
||||
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
|
||||
database.execSQL("DROP TABLE preferences")
|
||||
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import java.util.zip.ZipFile
|
||||
|
||||
class CbzFetcher : Fetcher<Uri> {
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun fetch(
|
||||
pool: BitmapPool,
|
||||
data: Uri,
|
||||
|
||||
@@ -37,7 +37,7 @@ class SetCookieCache : CookieCache {
|
||||
|
||||
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
|
||||
|
||||
private inner class SetCookieCacheIterator internal constructor() : MutableIterator<Cookie> {
|
||||
private inner class SetCookieCacheIterator() : MutableIterator<Cookie> {
|
||||
|
||||
private val iterator = cookies.iterator()
|
||||
|
||||
|
||||
@@ -41,27 +41,24 @@ class SharedPrefsCookiePersistor(private val sharedPreferences: SharedPreference
|
||||
return cookies
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun saveAll(cookies: Collection<Cookie>) {
|
||||
val editor = sharedPreferences.edit()
|
||||
for (cookie in cookies) {
|
||||
editor.putString(createCookieKey(cookie), SerializableCookie().encode(cookie))
|
||||
}
|
||||
editor.commit()
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun removeAll(cookies: Collection<Cookie>) {
|
||||
val editor = sharedPreferences.edit()
|
||||
for (cookie in cookies) {
|
||||
editor.remove(createCookieKey(cookie))
|
||||
}
|
||||
editor.commit()
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
@SuppressLint("ApplySharedPref")
|
||||
override fun clear() {
|
||||
sharedPreferences.edit().clear().commit()
|
||||
sharedPreferences.edit().clear().apply()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
@@ -38,6 +38,7 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
|
||||
getFromFile(Uri.parse(manga.url).toFile())
|
||||
} else manga
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
||||
val file = Uri.parse(chapter.url).toFile()
|
||||
val zip = ZipFile(file)
|
||||
|
||||
@@ -15,7 +15,7 @@ import org.koitharu.kotatsu.ui.common.dialog.TextInputDialog
|
||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class FavouriteCategoriesDialog() : BaseBottomSheet(R.layout.dialog_favorite_categories),
|
||||
class FavouriteCategoriesDialog : BaseBottomSheet(R.layout.dialog_favorite_categories),
|
||||
FavouriteCategoriesView,
|
||||
OnCategoryCheckListener {
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
|
||||
override val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.Main + job
|
||||
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
suspend fun loadFile(url: String, force: Boolean): File {
|
||||
if (!force) {
|
||||
cache[url]?.let {
|
||||
|
||||
@@ -55,7 +55,7 @@ class GroupedList<K, T> {
|
||||
lruGroup = entry.second
|
||||
lruGroupKey = entry.first
|
||||
lruGroupFirstIndex = lastIndex - entry.second.size
|
||||
return entry.second.get(index - lruGroupFirstIndex)
|
||||
return entry.second[index - lruGroupFirstIndex]
|
||||
}
|
||||
lastIndex -= entry.second.size
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.ui.reader.ReaderState
|
||||
import org.koitharu.kotatsu.utils.ext.doOnPageChanged
|
||||
import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
|
||||
class PagerReaderFragment() : AbstractReader(R.layout.fragment_reader_standard) {
|
||||
class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard) {
|
||||
|
||||
private var paginationListener: PagerPaginationListener? = null
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import org.koitharu.kotatsu.utils.ext.withArgs
|
||||
class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
|
||||
|
||||
private val scrollInterpolator = AccelerateDecelerateInterpolator()
|
||||
protected var paginationListener: ListPaginationListener? = null
|
||||
private var paginationListener: ListPaginationListener? = null
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.utils.ext
|
||||
import android.annotation.SuppressLint
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
package org.koitharu.kotatsu.utils.ext
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.core.app.NotificationCompat
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
fun NotificationCompat.Builder.clearActions(): NotificationCompat.Builder {
|
||||
mActions.clear()
|
||||
safe {
|
||||
mActions.clear()
|
||||
}
|
||||
return this
|
||||
}
|
||||
@@ -120,7 +120,8 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
|
||||
@@ -132,7 +132,8 @@
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/chips_tags"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
tools:ignore="UnusedAttribute" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progressBar"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<string name="settings">Настройки</string>
|
||||
<string name="remote_sources">Онлайн каталоги</string>
|
||||
<string name="loading_">Загрузка…</string>
|
||||
<string name="chapter_d_of_d">Глава %d из %d</string>
|
||||
<string name="chapter_d_of_d">Глава %1$d из %2$d</string>
|
||||
<string name="close">Закрыть</string>
|
||||
<string name="try_again">Повторить</string>
|
||||
<string name="clear_history">Очистить историю</string>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<string name="settings">Settings</string>
|
||||
<string name="remote_sources">Remote sources</string>
|
||||
<string name="loading_">Loading…</string>
|
||||
<string name="chapter_d_of_d">Chapter %d of %d</string>
|
||||
<string name="chapter_d_of_d">Chapter %1$d of %2$d</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="clear_history">Clear history</string>
|
||||
|
||||
@@ -13,7 +13,9 @@ object AssertX {
|
||||
cn.connect()
|
||||
when (val code = cn.responseCode) {
|
||||
HttpURLConnection.HTTP_MOVED_PERM,
|
||||
HttpURLConnection.HTTP_MOVED_TEMP -> assertContentType(cn.getHeaderField("Location"), *types)
|
||||
HttpURLConnection.HTTP_MOVED_TEMP -> {
|
||||
assertContentType(cn.getHeaderField("Location"), *types)
|
||||
}
|
||||
HttpURLConnection.HTTP_OK -> {
|
||||
val ct = cn.contentType.substringBeforeLast(';').split("/")
|
||||
Assert.assertTrue(types.any {
|
||||
|
||||
Reference in New Issue
Block a user