Merge branch 'devel' into feature/nextgen
This commit is contained in:
@@ -5,16 +5,18 @@ import android.content.Context
|
||||
import android.os.StrictMode
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.fragment.app.strictmode.FragmentStrictMode
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.acra.ReportField
|
||||
import org.acra.config.dialog
|
||||
import org.acra.config.mailSender
|
||||
import org.acra.data.StringFormat
|
||||
import org.acra.ktx.initAcra
|
||||
import org.koin.android.ext.android.get
|
||||
import org.koin.android.ext.android.getKoin
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.bookmarks.bookmarksModule
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.databaseModule
|
||||
import org.koitharu.kotatsu.core.github.githubModule
|
||||
import org.koitharu.kotatsu.core.network.networkModule
|
||||
@@ -29,7 +31,6 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.local.localModule
|
||||
import org.koitharu.kotatsu.main.mainModule
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.readerModule
|
||||
import org.koitharu.kotatsu.remotelist.remoteListModule
|
||||
@@ -38,7 +39,6 @@ import org.koitharu.kotatsu.search.searchModule
|
||||
import org.koitharu.kotatsu.settings.settingsModule
|
||||
import org.koitharu.kotatsu.suggestions.suggestionsModule
|
||||
import org.koitharu.kotatsu.tracker.trackerModule
|
||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||
|
||||
class KotatsuApp : Application() {
|
||||
@@ -50,11 +50,8 @@ class KotatsuApp : Application() {
|
||||
}
|
||||
initKoin()
|
||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
||||
registerActivityLifecycleCallbacks(get<ActivityRecreationHandle>())
|
||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
||||
widgetUpdater.subscribeToFavourites(get())
|
||||
widgetUpdater.subscribeToHistory(get())
|
||||
setupActivityLifecycleCallbacks()
|
||||
setupDatabaseObservers()
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
@@ -116,6 +113,22 @@ class KotatsuApp : Application() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupDatabaseObservers() {
|
||||
val observers = getKoin().getAll<InvalidationTracker.Observer>()
|
||||
val database = get<MangaDatabase>()
|
||||
val tracker = database.invalidationTracker
|
||||
observers.forEach {
|
||||
tracker.addObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupActivityLifecycleCallbacks() {
|
||||
val callbacks = getKoin().getAll<ActivityLifecycleCallbacks>()
|
||||
callbacks.forEach {
|
||||
registerActivityLifecycleCallbacks(it)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableStrictMode() {
|
||||
StrictMode.setThreadPolicy(
|
||||
StrictMode.ThreadPolicy.Builder()
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
),
|
||||
]
|
||||
)
|
||||
class BookmarkEntity(
|
||||
data class BookmarkEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "page_id", index = true) val pageId: Long,
|
||||
@ColumnInfo(name = "chapter_id") val chapterId: Long,
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.BuildConfig
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.parsers.util.json.JSONIterator
|
||||
import org.koitharu.kotatsu.parsers.util.json.mapJSON
|
||||
|
||||
private const val PAGE_SIZE = 10
|
||||
|
||||
@@ -24,11 +22,11 @@ class BackupRepository(private val db: MangaDatabase) {
|
||||
}
|
||||
offset += history.size
|
||||
for (item in history) {
|
||||
val manga = item.manga.toJson()
|
||||
val manga = JsonSerializer(item.manga).toJson()
|
||||
val tags = JSONArray()
|
||||
item.tags.forEach { tags.put(it.toJson()) }
|
||||
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
|
||||
manga.put("tags", tags)
|
||||
val json = item.history.toJson()
|
||||
val json = JsonSerializer(item.history).toJson()
|
||||
json.put("manga", manga)
|
||||
entry.data.put(json)
|
||||
}
|
||||
@@ -40,7 +38,7 @@ class BackupRepository(private val db: MangaDatabase) {
|
||||
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
|
||||
val categories = db.favouriteCategoriesDao.findAll()
|
||||
for (item in categories) {
|
||||
entry.data.put(item.toJson())
|
||||
entry.data.put(JsonSerializer(item).toJson())
|
||||
}
|
||||
return entry
|
||||
}
|
||||
@@ -55,11 +53,11 @@ class BackupRepository(private val db: MangaDatabase) {
|
||||
}
|
||||
offset += favourites.size
|
||||
for (item in favourites) {
|
||||
val manga = item.manga.toJson()
|
||||
val manga = JsonSerializer(item.manga).toJson()
|
||||
val tags = JSONArray()
|
||||
item.tags.forEach { tags.put(it.toJson()) }
|
||||
item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
|
||||
manga.put("tags", tags)
|
||||
val json = item.favourite.toJson()
|
||||
val json = JsonSerializer(item.favourite).toJson()
|
||||
json.put("manga", manga)
|
||||
entry.data.put(json)
|
||||
}
|
||||
@@ -77,62 +75,54 @@ class BackupRepository(private val db: MangaDatabase) {
|
||||
return entry
|
||||
}
|
||||
|
||||
private fun MangaEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("id", id)
|
||||
jo.put("title", title)
|
||||
jo.put("alt_title", altTitle)
|
||||
jo.put("url", url)
|
||||
jo.put("public_url", publicUrl)
|
||||
jo.put("rating", rating)
|
||||
jo.put("nsfw", isNsfw)
|
||||
jo.put("cover_url", coverUrl)
|
||||
jo.put("large_cover_url", largeCoverUrl)
|
||||
jo.put("state", state)
|
||||
jo.put("author", author)
|
||||
jo.put("source", source)
|
||||
return jo
|
||||
suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
JsonDeserializer(it).toTagEntity()
|
||||
}
|
||||
val history = JsonDeserializer(item).toHistoryEntity()
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.historyDao.upsert(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun TagEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("id", id)
|
||||
jo.put("title", title)
|
||||
jo.put("key", key)
|
||||
jo.put("source", source)
|
||||
return jo
|
||||
suspend fun restoreCategories(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val category = JsonDeserializer(item).toFavouriteCategoryEntity()
|
||||
result += runCatching {
|
||||
db.favouriteCategoriesDao.upsert(category)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun HistoryEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("manga_id", mangaId)
|
||||
jo.put("created_at", createdAt)
|
||||
jo.put("updated_at", updatedAt)
|
||||
jo.put("chapter_id", chapterId)
|
||||
jo.put("page", page)
|
||||
jo.put("scroll", scroll)
|
||||
jo.put("percent", percent)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun FavouriteCategoryEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("category_id", categoryId)
|
||||
jo.put("created_at", createdAt)
|
||||
jo.put("sort_key", sortKey)
|
||||
jo.put("title", title)
|
||||
jo.put("order", order)
|
||||
jo.put("track", track)
|
||||
jo.put("show_in_lib", isVisibleInLibrary)
|
||||
return jo
|
||||
}
|
||||
|
||||
private fun FavouriteEntity.toJson(): JSONObject {
|
||||
val jo = JSONObject()
|
||||
jo.put("manga_id", mangaId)
|
||||
jo.put("category_id", categoryId)
|
||||
jo.put("created_at", createdAt)
|
||||
jo.put("sort_key", sortKey)
|
||||
return jo
|
||||
suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = JsonDeserializer(mangaJson).toMangaEntity()
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
JsonDeserializer(it).toTagEntity()
|
||||
}
|
||||
val favourite = JsonDeserializer(item).toFavouriteEntity()
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.favouritesDao.upsert(favourite)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -1,70 +1,27 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import androidx.room.withTransaction
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.parsers.util.json.*
|
||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||
|
||||
class RestoreRepository(private val db: MangaDatabase) {
|
||||
class JsonDeserializer(private val json: JSONObject) {
|
||||
|
||||
suspend fun upsertHistory(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = parseManga(mangaJson)
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
parseTag(it)
|
||||
}
|
||||
val history = parseHistory(item)
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.historyDao.upsert(history)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
fun toFavouriteEntity() = FavouriteEntity(
|
||||
mangaId = json.getLong("manga_id"),
|
||||
categoryId = json.getLong("category_id"),
|
||||
sortKey = json.getIntOrDefault("sort_key", 0),
|
||||
createdAt = json.getLong("created_at"),
|
||||
)
|
||||
|
||||
suspend fun upsertCategories(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val category = parseCategory(item)
|
||||
result += runCatching {
|
||||
db.favouriteCategoriesDao.upsert(category)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
suspend fun upsertFavourites(entry: BackupEntry): CompositeResult {
|
||||
val result = CompositeResult()
|
||||
for (item in entry.data.JSONIterator()) {
|
||||
val mangaJson = item.getJSONObject("manga")
|
||||
val manga = parseManga(mangaJson)
|
||||
val tags = mangaJson.getJSONArray("tags").mapJSON {
|
||||
parseTag(it)
|
||||
}
|
||||
val favourite = parseFavourite(item)
|
||||
result += runCatching {
|
||||
db.withTransaction {
|
||||
db.tagsDao.upsert(tags)
|
||||
db.mangaDao.upsert(manga, tags)
|
||||
db.favouritesDao.upsert(favourite)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseManga(json: JSONObject) = MangaEntity(
|
||||
fun toMangaEntity() = MangaEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
altTitle = json.getStringOrNull("alt_title"),
|
||||
@@ -79,14 +36,14 @@ class RestoreRepository(private val db: MangaDatabase) {
|
||||
source = json.getString("source")
|
||||
)
|
||||
|
||||
private fun parseTag(json: JSONObject) = TagEntity(
|
||||
fun toTagEntity() = TagEntity(
|
||||
id = json.getLong("id"),
|
||||
title = json.getString("title"),
|
||||
key = json.getString("key"),
|
||||
source = json.getString("source")
|
||||
)
|
||||
|
||||
private fun parseHistory(json: JSONObject) = HistoryEntity(
|
||||
fun toHistoryEntity() = HistoryEntity(
|
||||
mangaId = json.getLong("manga_id"),
|
||||
createdAt = json.getLong("created_at"),
|
||||
updatedAt = json.getLong("updated_at"),
|
||||
@@ -96,7 +53,7 @@ class RestoreRepository(private val db: MangaDatabase) {
|
||||
percent = json.getFloatOrDefault("percent", -1f),
|
||||
)
|
||||
|
||||
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity(
|
||||
fun toFavouriteCategoryEntity() = FavouriteCategoryEntity(
|
||||
categoryId = json.getInt("category_id"),
|
||||
createdAt = json.getLong("created_at"),
|
||||
sortKey = json.getInt("sort_key"),
|
||||
@@ -105,11 +62,4 @@ class RestoreRepository(private val db: MangaDatabase) {
|
||||
track = json.getBooleanOrDefault("track", true),
|
||||
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true),
|
||||
)
|
||||
|
||||
private fun parseFavourite(json: JSONObject) = FavouriteEntity(
|
||||
mangaId = json.getLong("manga_id"),
|
||||
categoryId = json.getLong("category_id"),
|
||||
createdAt = json.getLong("created_at"),
|
||||
sortKey = json.getIntOrDefault("sort_key", 0),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import org.json.JSONObject
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
import org.koitharu.kotatsu.core.db.entity.TagEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
|
||||
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
|
||||
import org.koitharu.kotatsu.history.data.HistoryEntity
|
||||
|
||||
class JsonSerializer private constructor(private val json: JSONObject) {
|
||||
|
||||
constructor(e: FavouriteEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("manga_id", e.mangaId)
|
||||
put("category_id", e.categoryId)
|
||||
put("sort_key", e.sortKey)
|
||||
put("created_at", e.createdAt)
|
||||
}
|
||||
)
|
||||
|
||||
constructor(e: FavouriteCategoryEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("category_id", e.categoryId)
|
||||
put("created_at", e.createdAt)
|
||||
put("sort_key", e.sortKey)
|
||||
put("title", e.title)
|
||||
put("order", e.order)
|
||||
put("track", e.track)
|
||||
put("show_in_lib", e.isVisibleInLibrary)
|
||||
}
|
||||
)
|
||||
|
||||
constructor(e: HistoryEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("manga_id", e.mangaId)
|
||||
put("created_at", e.createdAt)
|
||||
put("updated_at", e.updatedAt)
|
||||
put("chapter_id", e.chapterId)
|
||||
put("page", e.page)
|
||||
put("scroll", e.scroll)
|
||||
put("percent", e.percent)
|
||||
}
|
||||
)
|
||||
|
||||
constructor(e: TagEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("id", e.id)
|
||||
put("title", e.title)
|
||||
put("key", e.key)
|
||||
put("source", e.source)
|
||||
}
|
||||
)
|
||||
|
||||
constructor(e: MangaEntity) : this(
|
||||
JSONObject().apply {
|
||||
put("id", e.id)
|
||||
put("title", e.title)
|
||||
put("alt_title", e.altTitle)
|
||||
put("url", e.url)
|
||||
put("public_url", e.publicUrl)
|
||||
put("rating", e.rating)
|
||||
put("nsfw", e.isNsfw)
|
||||
put("cover_url", e.coverUrl)
|
||||
put("large_cover_url", e.largeCoverUrl)
|
||||
put("state", e.state)
|
||||
put("author", e.author)
|
||||
put("source", e.source)
|
||||
}
|
||||
)
|
||||
|
||||
fun toJson(): JSONObject = json
|
||||
}
|
||||
@@ -29,6 +29,8 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TrackLogEntity
|
||||
import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
|
||||
const val DATABASE_VERSION = 13
|
||||
|
||||
@Database(
|
||||
entities = [
|
||||
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
|
||||
@@ -36,7 +38,7 @@ import org.koitharu.kotatsu.tracker.data.TracksDao
|
||||
TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
|
||||
ScrobblingEntity::class,
|
||||
],
|
||||
version = 13,
|
||||
version = DATABASE_VERSION,
|
||||
)
|
||||
abstract class MangaDatabase : RoomDatabase() {
|
||||
|
||||
|
||||
9
app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt
Normal file
9
app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt
Normal file
@@ -0,0 +1,9 @@
|
||||
package org.koitharu.kotatsu.core.db
|
||||
|
||||
|
||||
const val TABLE_FAVOURITES = "favourites"
|
||||
const val TABLE_MANGA = "manga"
|
||||
const val TABLE_TAGS = "tags"
|
||||
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||
const val TABLE_HISTORY = "history"
|
||||
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||
@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA
|
||||
|
||||
@Entity(tableName = "manga")
|
||||
class MangaEntity(
|
||||
@Entity(tableName = TABLE_MANGA)
|
||||
data class MangaEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@@ -18,44 +19,5 @@ class MangaEntity(
|
||||
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
|
||||
@ColumnInfo(name = "state") val state: String?,
|
||||
@ColumnInfo(name = "author") val author: String?,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as MangaEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
if (altTitle != other.altTitle) return false
|
||||
if (url != other.url) return false
|
||||
if (publicUrl != other.publicUrl) return false
|
||||
if (rating != other.rating) return false
|
||||
if (isNsfw != other.isNsfw) return false
|
||||
if (coverUrl != other.coverUrl) return false
|
||||
if (largeCoverUrl != other.largeCoverUrl) return false
|
||||
if (state != other.state) return false
|
||||
if (author != other.author) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + (altTitle?.hashCode() ?: 0)
|
||||
result = 31 * result + url.hashCode()
|
||||
result = 31 * result + publicUrl.hashCode()
|
||||
result = 31 * result + rating.hashCode()
|
||||
result = 31 * result + isNsfw.hashCode()
|
||||
result = 31 * result + coverUrl.hashCode()
|
||||
result = 31 * result + (largeCoverUrl?.hashCode() ?: 0)
|
||||
result = 31 * result + (state?.hashCode() ?: 0)
|
||||
result = 31 * result + (author?.hashCode() ?: 0)
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ColumnInfo(name = "source") val source: String,
|
||||
)
|
||||
@@ -3,9 +3,11 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
|
||||
|
||||
@Entity(
|
||||
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"],
|
||||
tableName = TABLE_MANGA_TAGS,
|
||||
primaryKeys = ["manga_id", "tag_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
@@ -23,5 +25,5 @@ import androidx.room.ForeignKey
|
||||
)
|
||||
class MangaTagsEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long
|
||||
@ColumnInfo(name = "tag_id", index = true) val tagId: Long,
|
||||
)
|
||||
@@ -3,35 +3,13 @@ package org.koitharu.kotatsu.core.db.entity
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_TAGS
|
||||
|
||||
@Entity(tableName = "tags")
|
||||
class TagEntity(
|
||||
@Entity(tableName = TABLE_TAGS)
|
||||
data class TagEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "tag_id") val id: Long,
|
||||
@ColumnInfo(name = "title") val title: String,
|
||||
@ColumnInfo(name = "key") val key: String,
|
||||
@ColumnInfo(name = "source") val source: String
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as TagEntity
|
||||
|
||||
if (id != other.id) return false
|
||||
if (title != other.title) return false
|
||||
if (key != other.key) return false
|
||||
if (source != other.source) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = id.hashCode()
|
||||
result = 31 * result + title.hashCode()
|
||||
result = 31 * result + key.hashCode()
|
||||
result = 31 * result + source.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ColumnInfo(name = "source") val source: String,
|
||||
)
|
||||
@@ -6,38 +6,41 @@ import android.content.pm.ShortcutManager
|
||||
import android.media.ThumbnailUtils
|
||||
import android.os.Build
|
||||
import android.util.Size
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.room.InvalidationTracker
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.reader.ui.ReaderActivity
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.utils.ext.requireBitmap
|
||||
|
||||
class ShortcutsRepository(
|
||||
class ShortcutsUpdater(
|
||||
private val context: Context,
|
||||
private val coil: ImageLoader,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val mangaRepository: MangaDataRepository,
|
||||
) {
|
||||
) : InvalidationTracker.Observer(TABLE_HISTORY) {
|
||||
|
||||
private val iconSize by lazy {
|
||||
getIconSize(context)
|
||||
}
|
||||
private val iconSize by lazy { getIconSize(context) }
|
||||
private var shortcutsUpdateJob: Job? = null
|
||||
|
||||
suspend fun updateShortcuts() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return
|
||||
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
|
||||
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
|
||||
.filter { x -> x.title.isNotEmpty() }
|
||||
.map { buildShortcutInfo(it).build().toShortcutInfo() }
|
||||
manager.dynamicShortcuts = shortcuts
|
||||
override fun onInvalidated(tables: MutableSet<String>) {
|
||||
val prevJob = shortcutsUpdateJob
|
||||
shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
|
||||
prevJob?.join()
|
||||
updateShortcutsImpl()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun requestPinShortcut(manga: Manga): Boolean {
|
||||
@@ -48,17 +51,28 @@ class ShortcutsRepository(
|
||||
)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
suspend fun await(): Boolean {
|
||||
return shortcutsUpdateJob?.join() != null
|
||||
}
|
||||
|
||||
private suspend fun updateShortcutsImpl() {
|
||||
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager
|
||||
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity)
|
||||
.filter { x -> x.title.isNotEmpty() }
|
||||
.map { buildShortcutInfo(it).build().toShortcutInfo() }
|
||||
manager.dynamicShortcuts = shortcuts
|
||||
}
|
||||
|
||||
private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
|
||||
val icon = runCatching {
|
||||
withContext(Dispatchers.IO) {
|
||||
val bmp = coil.execute(
|
||||
ImageRequest.Builder(context)
|
||||
.data(manga.coverUrl)
|
||||
.size(iconSize.width, iconSize.height)
|
||||
.build()
|
||||
).requireBitmap()
|
||||
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
|
||||
}
|
||||
val bmp = coil.execute(
|
||||
ImageRequest.Builder(context)
|
||||
.data(manga.coverUrl)
|
||||
.size(iconSize.width, iconSize.height)
|
||||
.build()
|
||||
).requireBitmap()
|
||||
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
|
||||
}.fold(
|
||||
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
|
||||
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }
|
||||
@@ -34,7 +34,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.browser.BrowserActivity
|
||||
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
|
||||
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.databinding.ActivityDetailsBinding
|
||||
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
@@ -222,7 +222,7 @@ class DetailsActivity :
|
||||
R.id.action_shortcut -> {
|
||||
viewModel.manga.value?.let {
|
||||
lifecycleScope.launch {
|
||||
if (!get<ShortcutsRepository>().requestPinShortcut(it)) {
|
||||
if (!get<ShortcutsUpdater>().requestPinShortcut(it)) {
|
||||
binding.snackbar.show(getString(R.string.operation_not_supported))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.favourites.data
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
|
||||
|
||||
@Entity(tableName = "favourite_categories")
|
||||
class FavouriteCategoryEntity(
|
||||
@Entity(tableName = TABLE_FAVOURITE_CATEGORIES)
|
||||
data class FavouriteCategoryEntity(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
@ColumnInfo(name = "category_id") val categoryId: Int,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
|
||||
@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.favourites.data
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [
|
||||
tableName = TABLE_FAVOURITES,
|
||||
primaryKeys = ["manga_id", "category_id"],
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
parentColumns = ["manga_id"],
|
||||
@@ -21,32 +24,9 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
)
|
||||
]
|
||||
)
|
||||
class FavouriteEntity(
|
||||
data class FavouriteEntity(
|
||||
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
|
||||
@ColumnInfo(name = "category_id", index = true) val categoryId: Long,
|
||||
@ColumnInfo(name = "sort_key") val sortKey: Int,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
) {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as FavouriteEntity
|
||||
|
||||
if (mangaId != other.mangaId) return false
|
||||
if (categoryId != other.categoryId) return false
|
||||
if (sortKey != other.sortKey) return false
|
||||
if (createdAt != other.createdAt) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = mangaId.hashCode()
|
||||
result = 31 * result + categoryId.hashCode()
|
||||
result = 31 * result + sortKey
|
||||
result = 31 * result + createdAt.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -10,5 +10,5 @@ val historyModule
|
||||
|
||||
single { HistoryRepository(get(), get(), get(), getAll()) }
|
||||
|
||||
viewModel { HistoryListViewModel(get(), get(), get(), get()) }
|
||||
viewModel { HistoryListViewModel(get(), get(), get()) }
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.PrimaryKey
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
|
||||
@Entity(
|
||||
tableName = "history",
|
||||
tableName = TABLE_HISTORY,
|
||||
foreignKeys = [
|
||||
ForeignKey(
|
||||
entity = MangaEntity::class,
|
||||
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
|
||||
)
|
||||
]
|
||||
)
|
||||
class HistoryEntity(
|
||||
data class HistoryEntity(
|
||||
@PrimaryKey(autoGenerate = false)
|
||||
@ColumnInfo(name = "manga_id") val mangaId: Long,
|
||||
@ColumnInfo(name = "created_at") val createdAt: Long,
|
||||
|
||||
@@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.base.domain.plus
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.prefs.observeAsFlow
|
||||
@@ -31,7 +29,6 @@ import java.util.concurrent.TimeUnit
|
||||
class HistoryListViewModel(
|
||||
private val repository: HistoryRepository,
|
||||
private val settings: AppSettings,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
@@ -72,7 +69,6 @@ class HistoryListViewModel(
|
||||
fun clearHistory() {
|
||||
launchLoadingJob {
|
||||
repository.clear()
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,10 +77,7 @@ class HistoryListViewModel(
|
||||
return
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = repository.deleteReversible(ids) + ReversibleHandle {
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
shortcutsRepository.updateShortcuts()
|
||||
val handle = repository.deleteReversible(ids)
|
||||
onItemsRemoved.postCall(handle)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ val libraryModule
|
||||
|
||||
factory { LibraryRepository(get()) }
|
||||
|
||||
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) }
|
||||
viewModel { LibraryViewModel(get(), get(), get(), get()) }
|
||||
viewModel { LibraryCategoriesConfigViewModel(get()) }
|
||||
}
|
||||
@@ -7,12 +7,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.ReversibleHandle
|
||||
import org.koitharu.kotatsu.base.domain.plus
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.model.FavouriteCategory
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.prefs.ListMode
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
@@ -36,7 +33,6 @@ private const val HISTORY_MAX_SEGMENTS = 2
|
||||
class LibraryViewModel(
|
||||
private val repository: LibraryRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
private val trackingRepository: TrackingRepository,
|
||||
private val settings: AppSettings,
|
||||
) : BaseViewModel(), ListExtraProvider {
|
||||
@@ -88,10 +84,7 @@ class LibraryViewModel(
|
||||
return
|
||||
}
|
||||
launchJob(Dispatchers.Default) {
|
||||
val handle = historyRepository.deleteReversible(ids) + ReversibleHandle {
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
shortcutsRepository.updateShortcuts()
|
||||
val handle = historyRepository.deleteReversible(ids)
|
||||
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
|
||||
}
|
||||
}
|
||||
@@ -105,7 +98,6 @@ class LibraryViewModel(
|
||||
historyRepository.deleteAfter(minDate)
|
||||
R.string.removed_from_history
|
||||
}
|
||||
shortcutsRepository.updateShortcuts()
|
||||
onActionDone.postCall(ReversibleAction(stringRes, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,5 +16,5 @@ val localModule
|
||||
|
||||
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
|
||||
|
||||
viewModel { LocalListViewModel(get(), get(), get(), get()) }
|
||||
viewModel { LocalListViewModel(get(), get(), get()) }
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.download.ui.service.DownloadService
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
@@ -32,7 +31,6 @@ class LocalListViewModel(
|
||||
private val repository: LocalMangaRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
settings: AppSettings,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
) : MangaListViewModel(settings) {
|
||||
|
||||
val onMangaRemoved = SingleLiveEvent<Unit>()
|
||||
@@ -103,7 +101,6 @@ class LocalListViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcutsRepository.updateShortcuts()
|
||||
onMangaRemoved.call(Unit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,29 @@
|
||||
package org.koitharu.kotatsu.main
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Build
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.bind
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsUpdater
|
||||
import org.koitharu.kotatsu.main.ui.MainViewModel
|
||||
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
|
||||
import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
|
||||
|
||||
val mainModule
|
||||
get() = module {
|
||||
single { AppProtectHelper(get()) }
|
||||
single { ActivityRecreationHandle() }
|
||||
factory { ShortcutsRepository(androidContext(), get(), get(), get()) }
|
||||
single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class
|
||||
single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
|
||||
single { ShortcutsUpdater(androidContext(), get(), get(), get()) } bind InvalidationTracker.Observer::class
|
||||
} else {
|
||||
factory { ShortcutsUpdater(androidContext(), get(), get(), get()) }
|
||||
}
|
||||
|
||||
viewModel { MainViewModel(get(), get()) }
|
||||
viewModel { ProtectViewModel(get(), get()) }
|
||||
}
|
||||
@@ -23,7 +23,6 @@ val readerModule
|
||||
preselectedBranch = params[2],
|
||||
dataRepository = get(),
|
||||
historyRepository = get(),
|
||||
shortcutsRepository = get(),
|
||||
settings = get(),
|
||||
pageSaveHelper = get(),
|
||||
bookmarksRepository = get(),
|
||||
|
||||
@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.bookmarks.domain.Bookmark
|
||||
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
|
||||
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException
|
||||
import org.koitharu.kotatsu.core.os.ShortcutsRepository
|
||||
import org.koitharu.kotatsu.core.parser.MangaRepository
|
||||
import org.koitharu.kotatsu.core.prefs.*
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
@@ -46,7 +45,6 @@ class ReaderViewModel(
|
||||
private val preselectedBranch: String?,
|
||||
private val dataRepository: MangaDataRepository,
|
||||
private val historyRepository: HistoryRepository,
|
||||
private val shortcutsRepository: ShortcutsRepository,
|
||||
private val bookmarksRepository: BookmarksRepository,
|
||||
private val settings: AppSettings,
|
||||
private val pageSaveHelper: PageSaveHelper,
|
||||
@@ -289,7 +287,6 @@ class ReaderViewModel(
|
||||
currentState.value?.let {
|
||||
val percent = computePercent(it.chapterId, it.page)
|
||||
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
|
||||
shortcutsRepository.updateShortcuts()
|
||||
}
|
||||
|
||||
content.postValue(ReaderContent(pages, currentState.value))
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
package org.koitharu.kotatsu.settings
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.RestoreRepository
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.settings.backup.BackupObserver
|
||||
import org.koitharu.kotatsu.settings.backup.BackupViewModel
|
||||
import org.koitharu.kotatsu.settings.backup.RestoreViewModel
|
||||
import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel
|
||||
@@ -18,8 +20,11 @@ import org.koitharu.kotatsu.settings.tools.ToolsViewModel
|
||||
val settingsModule
|
||||
get() = module {
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
single<InvalidationTracker.Observer> { BackupObserver(androidContext()) }
|
||||
}
|
||||
|
||||
factory { BackupRepository(get()) }
|
||||
factory { RestoreRepository(get()) }
|
||||
single(createdAtStart = true) { AppSettings(androidContext()) }
|
||||
|
||||
viewModel { BackupViewModel(get(), androidContext()) }
|
||||
|
||||
@@ -4,9 +4,14 @@ import android.app.backup.BackupAgent
|
||||
import android.app.backup.BackupDataInput
|
||||
import android.app.backup.BackupDataOutput
|
||||
import android.app.backup.FullBackupDataOutput
|
||||
import android.content.Context
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koitharu.kotatsu.core.backup.*
|
||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.db.MangaDatabase
|
||||
import java.io.*
|
||||
|
||||
@@ -26,7 +31,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
|
||||
override fun onFullBackup(data: FullBackupDataOutput) {
|
||||
super.onFullBackup(data)
|
||||
val file = createBackupFile()
|
||||
val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext)))
|
||||
try {
|
||||
fullBackupFile(file, data)
|
||||
} finally {
|
||||
@@ -43,16 +48,16 @@ class AppBackupAgent : BackupAgent() {
|
||||
mtime: Long
|
||||
) {
|
||||
if (destination?.name?.endsWith(".bk.zip") == true) {
|
||||
restoreBackupFile(data.fileDescriptor, size)
|
||||
restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext)))
|
||||
destination.delete()
|
||||
} else {
|
||||
super.onRestoreFile(data, size, destination, type, mode, mtime)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createBackupFile() = runBlocking {
|
||||
val repository = BackupRepository(MangaDatabase(applicationContext))
|
||||
BackupZipOutput(this@AppBackupAgent).use { backup ->
|
||||
@VisibleForTesting
|
||||
fun createBackupFile(context: Context, repository: BackupRepository) = runBlocking {
|
||||
BackupZipOutput(context).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
@@ -62,8 +67,8 @@ class AppBackupAgent : BackupAgent() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreBackupFile(fd: FileDescriptor, size: Long) {
|
||||
val repository = RestoreRepository(MangaDatabase(applicationContext))
|
||||
@VisibleForTesting
|
||||
fun restoreBackupFile(fd: FileDescriptor, size: Long, repository: BackupRepository) {
|
||||
val tempFile = File.createTempFile("backup_", ".tmp")
|
||||
FileInputStream(fd).use { input ->
|
||||
tempFile.outputStream().use { output ->
|
||||
@@ -73,9 +78,9 @@ class AppBackupAgent : BackupAgent() {
|
||||
val backup = BackupZipInput(tempFile)
|
||||
try {
|
||||
runBlocking {
|
||||
repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
}
|
||||
} finally {
|
||||
backup.close()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.app.backup.BackupManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.M)
|
||||
class BackupObserver(
|
||||
context: Context,
|
||||
) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
|
||||
|
||||
private val backupManager = BackupManager(context)
|
||||
|
||||
override fun onInvalidated(tables: MutableSet<String>) {
|
||||
backupManager.dataChanged()
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.core.backup.BackupEntry
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipInput
|
||||
import org.koitharu.kotatsu.core.backup.CompositeResult
|
||||
import org.koitharu.kotatsu.core.backup.RestoreRepository
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.progress.Progress
|
||||
import java.io.File
|
||||
@@ -17,7 +17,7 @@ import java.io.FileNotFoundException
|
||||
|
||||
class RestoreViewModel(
|
||||
uri: Uri?,
|
||||
private val repository: RestoreRepository,
|
||||
private val repository: BackupRepository,
|
||||
context: Context
|
||||
) : BaseViewModel() {
|
||||
|
||||
@@ -44,13 +44,13 @@ class RestoreViewModel(
|
||||
val result = CompositeResult()
|
||||
|
||||
progress.value = Progress(0, 3)
|
||||
result += repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
|
||||
|
||||
progress.value = Progress(1, 3)
|
||||
result += repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
|
||||
|
||||
progress.value = Progress(2, 3)
|
||||
result += repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
|
||||
|
||||
progress.value = Progress(3, 3)
|
||||
onRestoreDone.call(result)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
package org.koitharu.kotatsu.widget
|
||||
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||
import org.koin.dsl.module
|
||||
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
|
||||
|
||||
val appWidgetModule
|
||||
get() = module {
|
||||
|
||||
single<InvalidationTracker.Observer> { WidgetUpdater(androidContext()) }
|
||||
|
||||
viewModel { ShelfConfigViewModel(get()) }
|
||||
}
|
||||
@@ -4,36 +4,24 @@ import android.appwidget.AppWidgetManager
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.retry
|
||||
import kotlinx.coroutines.plus
|
||||
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
|
||||
import org.koitharu.kotatsu.history.domain.HistoryRepository
|
||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import androidx.room.InvalidationTracker
|
||||
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||
import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
|
||||
import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider
|
||||
|
||||
class WidgetUpdater(private val context: Context) {
|
||||
class WidgetUpdater(private val context: Context) : InvalidationTracker.Observer(TABLE_HISTORY, TABLE_FAVOURITES) {
|
||||
|
||||
fun subscribeToFavourites(repository: FavouritesRepository) {
|
||||
repository.observeAll(SortOrder.NEWEST)
|
||||
.onEach { updateWidget(ShelfWidgetProvider::class.java) }
|
||||
.retry { error -> error !is CancellationException }
|
||||
.launchIn(processLifecycleScope + Dispatchers.Default)
|
||||
override fun onInvalidated(tables: MutableSet<String>) {
|
||||
if (TABLE_HISTORY in tables) {
|
||||
updateWidgets(RecentWidgetProvider::class.java)
|
||||
}
|
||||
if (TABLE_FAVOURITES in tables) {
|
||||
updateWidgets(ShelfWidgetProvider::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun subscribeToHistory(repository: HistoryRepository) {
|
||||
repository.observeAll()
|
||||
.onEach { updateWidget(RecentWidgetProvider::class.java) }
|
||||
.retry { error -> error !is CancellationException }
|
||||
.launchIn(processLifecycleScope + Dispatchers.Default)
|
||||
}
|
||||
|
||||
private fun updateWidget(cls: Class<*>) {
|
||||
private fun updateWidgets(cls: Class<*>) {
|
||||
val intent = Intent(context, cls)
|
||||
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||
val ids = AppWidgetManager.getInstance(context)
|
||||
|
||||
Reference in New Issue
Block a user