Merge branch 'devel' into feature/nextgen

This commit is contained in:
Koitharu
2022-07-18 13:33:42 +03:00
41 changed files with 710 additions and 363 deletions

View File

@@ -68,8 +68,11 @@ android {
disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged' disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged'
} }
testOptions { testOptions {
unitTests.includeAndroidResources = true unitTests.includeAndroidResources true
unitTests.returnDefaultValues = false unitTests.returnDefaultValues false
kotlinOptions {
freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi']
}
} }
} }
afterEvaluate { afterEvaluate {
@@ -128,6 +131,7 @@ dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1'
testImplementation 'junit:junit:4.13.2' testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20220320'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4'
androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0'

View File

@@ -0,0 +1,8 @@
{
"id": 4,
"title": "Read later",
"sortKey": 1,
"order": "NEWEST",
"createdAt": 1335906000000,
"isTrackingEnabled": true
}

View File

@@ -0,0 +1,35 @@
{
"id": -2096681732556647985,
"title": "Странствия Эманон",
"url": "/stranstviia_emanon",
"publicUrl": "https://readmanga.io/stranstviia_emanon",
"rating": 0.9400894,
"isNsfw": true,
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
"tags": [
{
"title": "Сверхъестественное",
"key": "supernatural",
"source": "READMANGA_RU"
},
{
"title": "Сэйнэн",
"key": "seinen",
"source": "READMANGA_RU"
},
{
"title": "Повседневность",
"key": "slice_of_life",
"source": "READMANGA_RU"
},
{
"title": "Приключения",
"key": "adventure",
"source": "READMANGA_RU"
}
],
"state": "FINISHED",
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
"description": null,
"source": "READMANGA_RU"
}

View File

@@ -0,0 +1,9 @@
package org.koitharu.kotatsu
import android.app.Instrumentation
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
waitForIdle { cont.resume(Unit) }
}

View File

@@ -0,0 +1,54 @@
package org.koitharu.kotatsu
import androidx.test.platform.app.InstrumentationRegistry
import com.squareup.moshi.*
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okio.buffer
import okio.source
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.parsers.model.Manga
import java.util.*
import kotlin.reflect.KClass
object SampleData {
private val moshi = Moshi.Builder()
.add(DateAdapter())
.add(KotlinJsonAdapterFactory())
.build()
val manga: Manga = loadAsset("manga/header.json", Manga::class)
val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class)
val tag = mangaDetails.tags.elementAt(2)
val chapter = checkNotNull(mangaDetails.chapters)[2]
val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class)
fun <T : Any> loadAsset(name: String, cls: KClass<T>): T {
val assets = InstrumentationRegistry.getInstrumentation().context.assets
return assets.open(name).use {
moshi.adapter(cls.java).fromJson(it.source().buffer())
} ?: throw RuntimeException("Cannot read asset from json \"$name\"")
}
private class DateAdapter : JsonAdapter<Date>() {
@FromJson
override fun fromJson(reader: JsonReader): Date? {
val ms = reader.nextLong()
return if (ms == 0L) {
null
} else {
Date(ms)
}
}
@ToJson
override fun toJson(writer: JsonWriter, value: Date?) {
writer.value(value?.time ?: 0L)
}
}
}

View File

@@ -3,11 +3,12 @@ package org.koitharu.kotatsu.core.db
import androidx.room.testing.MigrationTestHelper import androidx.room.testing.MigrationTestHelper
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry
import java.io.IOException
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koitharu.kotatsu.core.db.migrations.* import org.koitharu.kotatsu.core.db.migrations.*
import java.io.IOException
import kotlin.test.assertEquals
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MangaDatabaseTest { class MangaDatabaseTest {
@@ -21,17 +22,15 @@ class MangaDatabaseTest {
@Test @Test
@Throws(IOException::class) @Throws(IOException::class)
fun migrateAll() { fun migrateAll() {
helper.createDatabase(TEST_DB, 1).apply { assertEquals(DATABASE_VERSION, migrations.last().endVersion)
// TODO execSQL("") helper.createDatabase(TEST_DB, 1).close()
close()
}
for (migration in migrations) { for (migration in migrations) {
helper.runMigrationsAndValidate( helper.runMigrationsAndValidate(
TEST_DB, TEST_DB,
migration.endVersion, migration.endVersion,
true, true,
migration migration
) ).close()
} }
} }
@@ -50,6 +49,8 @@ class MangaDatabaseTest {
Migration8To9(), Migration8To9(),
Migration9To10(), Migration9To10(),
Migration10To11(), Migration10To11(),
Migration11To12(),
Migration12To13(),
) )
} }
} }

View File

@@ -0,0 +1,65 @@
package org.koitharu.kotatsu.core.os
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import androidx.core.content.getSystemService
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.inject
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.awaitForIdle
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.history.domain.HistoryRepository
import kotlin.test.assertEquals
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class)
class ShortcutsUpdaterTest : KoinTest {
private val historyRepository by inject<HistoryRepository>()
private val shortcutsUpdater by inject<ShortcutsUpdater>()
private val database by inject<MangaDatabase>()
@Before
fun setUp() {
database.clearAllTables()
}
@Test
fun testUpdateShortcuts() = runTest {
awaitUpdate()
assertTrue(getShortcuts().isEmpty())
historyRepository.addOrUpdate(
manga = SampleData.manga,
chapterId = SampleData.chapter.id,
page = 4,
scroll = 2,
percent = 0.3f
)
awaitUpdate()
val shortcuts = getShortcuts()
assertEquals(1, shortcuts.size)
}
private fun getShortcuts(): List<ShortcutInfo> {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val manager = checkNotNull(context.getSystemService<ShortcutManager>())
return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" }
}
private suspend fun awaitUpdate() {
val instrumentation = InstrumentationRegistry.getInstrumentation()
while (true) {
instrumentation.awaitForIdle()
if (shortcutsUpdater.await()) {
return
}
}
}
}

View File

@@ -0,0 +1,67 @@
package org.koitharu.kotatsu.settings.backup
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.koin.test.KoinTest
import org.koin.test.get
import org.koin.test.inject
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.toMangaTags
import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import kotlin.test.*
@RunWith(AndroidJUnit4::class)
class AppBackupAgentTest : KoinTest {
private val historyRepository by inject<HistoryRepository>()
private val favouritesRepository by inject<FavouritesRepository>()
private val backupRepository by inject<BackupRepository>()
private val database by inject<MangaDatabase>()
@Before
fun setUp() {
database.clearAllTables()
}
@Test
fun testBackupRestore() = runTest {
val category = favouritesRepository.createCategory(
title = SampleData.favouriteCategory.title,
sortOrder = SampleData.favouriteCategory.order,
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
)
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
historyRepository.addOrUpdate(
manga = SampleData.mangaDetails,
chapterId = SampleData.mangaDetails.chapters!![2].id,
page = 3,
scroll = 40,
percent = 0.2f,
)
val history = checkNotNull(historyRepository.getOne(SampleData.manga))
val agent = AppBackupAgent()
val backup = agent.createBackupFile(get(), backupRepository)
database.clearAllTables()
assertTrue(favouritesRepository.getAllManga().isEmpty())
assertNull(historyRepository.getLastOrNull())
backup.inputStream().use {
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
}
assertEquals(category, favouritesRepository.getCategory(category.id))
assertEquals(history, historyRepository.getOne(SampleData.manga))
assertContentEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags()
assertContains(allTags, SampleData.tag)
}
}

View File

@@ -1,29 +1,21 @@
package org.koitharu.kotatsu.tracker.domain package org.koitharu.kotatsu.tracker.domain
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import okio.buffer
import okio.source
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.koin.test.KoinTest import org.koin.test.KoinTest
import org.koin.test.inject import org.koin.test.inject
import org.koitharu.kotatsu.SampleData
import org.koitharu.kotatsu.base.domain.MangaDataRepository import org.koitharu.kotatsu.base.domain.MangaDataRepository
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class TrackerTest : KoinTest { class TrackerTest : KoinTest {
private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
private val mangaAdapter = moshi.adapter(Manga::class.java)
private val historyRegistry by inject<HistoryRepository>()
private val repository by inject<TrackingRepository>() private val repository by inject<TrackingRepository>()
private val dataRepository by inject<MangaDataRepository>() private val dataRepository by inject<MangaDataRepository>()
private val tracker by inject<Tracker>() private val tracker by inject<Tracker>()
@@ -178,10 +170,7 @@ class TrackerTest : KoinTest {
} }
private suspend fun loadManga(name: String): Manga { private suspend fun loadManga(name: String): Manga {
val assets = InstrumentationRegistry.getInstrumentation().context.assets val manga = SampleData.loadAsset("manga/$name", Manga::class)
val manga = assets.open("manga/$name").use {
mangaAdapter.fromJson(it.source().buffer())
} ?: throw RuntimeException("Cannot read manga from json \"$name\"")
dataRepository.storeManga(manga) dataRepository.storeManga(manga)
return manga return manga
} }

View File

@@ -5,16 +5,18 @@ import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.room.InvalidationTracker
import org.acra.ReportField import org.acra.ReportField
import org.acra.config.dialog import org.acra.config.dialog
import org.acra.config.mailSender import org.acra.config.mailSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.bookmarks.bookmarksModule 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.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule 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.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule 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.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.suggestions.suggestionsModule
import org.koitharu.kotatsu.tracker.trackerModule import org.koitharu.kotatsu.tracker.trackerModule
import org.koitharu.kotatsu.widget.WidgetUpdater
import org.koitharu.kotatsu.widget.appWidgetModule import org.koitharu.kotatsu.widget.appWidgetModule
class KotatsuApp : Application() { class KotatsuApp : Application() {
@@ -50,11 +50,8 @@ class KotatsuApp : Application() {
} }
initKoin() initKoin()
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme) AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>()) setupActivityLifecycleCallbacks()
registerActivityLifecycleCallbacks(get<ActivityRecreationHandle>()) setupDatabaseObservers()
val widgetUpdater = WidgetUpdater(applicationContext)
widgetUpdater.subscribeToFavourites(get())
widgetUpdater.subscribeToHistory(get())
} }
private fun initKoin() { 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() { private fun enableStrictMode() {
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder() StrictMode.ThreadPolicy.Builder()

View File

@@ -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 = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "page_id", index = true) val pageId: Long, @ColumnInfo(name = "page_id", index = true) val pageId: Long,
@ColumnInfo(name = "chapter_id") val chapterId: Long, @ColumnInfo(name = "chapter_id") val chapterId: Long,

View File

@@ -1,14 +1,12 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONObject import org.json.JSONObject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.parsers.util.json.JSONIterator
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.parsers.util.json.mapJSON
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity
private const val PAGE_SIZE = 10 private const val PAGE_SIZE = 10
@@ -24,11 +22,11 @@ class BackupRepository(private val db: MangaDatabase) {
} }
offset += history.size offset += history.size
for (item in history) { for (item in history) {
val manga = item.manga.toJson() val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray() val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) } item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags) manga.put("tags", tags)
val json = item.history.toJson() val json = JsonSerializer(item.history).toJson()
json.put("manga", manga) json.put("manga", manga)
entry.data.put(json) entry.data.put(json)
} }
@@ -40,7 +38,7 @@ class BackupRepository(private val db: MangaDatabase) {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray()) val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val categories = db.favouriteCategoriesDao.findAll() val categories = db.favouriteCategoriesDao.findAll()
for (item in categories) { for (item in categories) {
entry.data.put(item.toJson()) entry.data.put(JsonSerializer(item).toJson())
} }
return entry return entry
} }
@@ -55,11 +53,11 @@ class BackupRepository(private val db: MangaDatabase) {
} }
offset += favourites.size offset += favourites.size
for (item in favourites) { for (item in favourites) {
val manga = item.manga.toJson() val manga = JsonSerializer(item.manga).toJson()
val tags = JSONArray() val tags = JSONArray()
item.tags.forEach { tags.put(it.toJson()) } item.tags.forEach { tags.put(JsonSerializer(it).toJson()) }
manga.put("tags", tags) manga.put("tags", tags)
val json = item.favourite.toJson() val json = JsonSerializer(item.favourite).toJson()
json.put("manga", manga) json.put("manga", manga)
entry.data.put(json) entry.data.put(json)
} }
@@ -77,62 +75,54 @@ class BackupRepository(private val db: MangaDatabase) {
return entry return entry
} }
private fun MangaEntity.toJson(): JSONObject { suspend fun restoreHistory(entry: BackupEntry): CompositeResult {
val jo = JSONObject() val result = CompositeResult()
jo.put("id", id) for (item in entry.data.JSONIterator()) {
jo.put("title", title) val mangaJson = item.getJSONObject("manga")
jo.put("alt_title", altTitle) val manga = JsonDeserializer(mangaJson).toMangaEntity()
jo.put("url", url) val tags = mangaJson.getJSONArray("tags").mapJSON {
jo.put("public_url", publicUrl) JsonDeserializer(it).toTagEntity()
jo.put("rating", rating) }
jo.put("nsfw", isNsfw) val history = JsonDeserializer(item).toHistoryEntity()
jo.put("cover_url", coverUrl) result += runCatching {
jo.put("large_cover_url", largeCoverUrl) db.withTransaction {
jo.put("state", state) db.tagsDao.upsert(tags)
jo.put("author", author) db.mangaDao.upsert(manga, tags)
jo.put("source", source) db.historyDao.upsert(history)
return jo }
}
}
return result
} }
private fun TagEntity.toJson(): JSONObject { suspend fun restoreCategories(entry: BackupEntry): CompositeResult {
val jo = JSONObject() val result = CompositeResult()
jo.put("id", id) for (item in entry.data.JSONIterator()) {
jo.put("title", title) val category = JsonDeserializer(item).toFavouriteCategoryEntity()
jo.put("key", key) result += runCatching {
jo.put("source", source) db.favouriteCategoriesDao.upsert(category)
return jo }
}
return result
} }
private fun HistoryEntity.toJson(): JSONObject { suspend fun restoreFavourites(entry: BackupEntry): CompositeResult {
val jo = JSONObject() val result = CompositeResult()
jo.put("manga_id", mangaId) for (item in entry.data.JSONIterator()) {
jo.put("created_at", createdAt) val mangaJson = item.getJSONObject("manga")
jo.put("updated_at", updatedAt) val manga = JsonDeserializer(mangaJson).toMangaEntity()
jo.put("chapter_id", chapterId) val tags = mangaJson.getJSONArray("tags").mapJSON {
jo.put("page", page) JsonDeserializer(it).toTagEntity()
jo.put("scroll", scroll) }
jo.put("percent", percent) val favourite = JsonDeserializer(item).toFavouriteEntity()
return jo result += runCatching {
} db.withTransaction {
db.tagsDao.upsert(tags)
private fun FavouriteCategoryEntity.toJson(): JSONObject { db.mangaDao.upsert(manga, tags)
val jo = JSONObject() db.favouritesDao.upsert(favourite)
jo.put("category_id", categoryId) }
jo.put("created_at", createdAt) }
jo.put("sort_key", sortKey) }
jo.put("title", title) return result
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
} }
} }

View File

@@ -1,70 +1,27 @@
package org.koitharu.kotatsu.core.backup package org.koitharu.kotatsu.core.backup
import androidx.room.withTransaction
import org.json.JSONObject 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.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity
import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity
import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.history.data.HistoryEntity
import org.koitharu.kotatsu.parsers.model.SortOrder 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 { fun toFavouriteEntity() = FavouriteEntity(
val result = CompositeResult() mangaId = json.getLong("manga_id"),
for (item in entry.data.JSONIterator()) { categoryId = json.getLong("category_id"),
val mangaJson = item.getJSONObject("manga") sortKey = json.getIntOrDefault("sort_key", 0),
val manga = parseManga(mangaJson) createdAt = json.getLong("created_at"),
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
}
suspend fun upsertCategories(entry: BackupEntry): CompositeResult { fun toMangaEntity() = MangaEntity(
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(
id = json.getLong("id"), id = json.getLong("id"),
title = json.getString("title"), title = json.getString("title"),
altTitle = json.getStringOrNull("alt_title"), altTitle = json.getStringOrNull("alt_title"),
@@ -79,14 +36,14 @@ class RestoreRepository(private val db: MangaDatabase) {
source = json.getString("source") source = json.getString("source")
) )
private fun parseTag(json: JSONObject) = TagEntity( fun toTagEntity() = TagEntity(
id = json.getLong("id"), id = json.getLong("id"),
title = json.getString("title"), title = json.getString("title"),
key = json.getString("key"), key = json.getString("key"),
source = json.getString("source") source = json.getString("source")
) )
private fun parseHistory(json: JSONObject) = HistoryEntity( fun toHistoryEntity() = HistoryEntity(
mangaId = json.getLong("manga_id"), mangaId = json.getLong("manga_id"),
createdAt = json.getLong("created_at"), createdAt = json.getLong("created_at"),
updatedAt = json.getLong("updated_at"), updatedAt = json.getLong("updated_at"),
@@ -96,7 +53,7 @@ class RestoreRepository(private val db: MangaDatabase) {
percent = json.getFloatOrDefault("percent", -1f), percent = json.getFloatOrDefault("percent", -1f),
) )
private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity( fun toFavouriteCategoryEntity() = FavouriteCategoryEntity(
categoryId = json.getInt("category_id"), categoryId = json.getInt("category_id"),
createdAt = json.getLong("created_at"), createdAt = json.getLong("created_at"),
sortKey = json.getInt("sort_key"), sortKey = json.getInt("sort_key"),
@@ -105,11 +62,4 @@ class RestoreRepository(private val db: MangaDatabase) {
track = json.getBooleanOrDefault("track", true), track = json.getBooleanOrDefault("track", true),
isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", 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),
)
} }

View File

@@ -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
}

View File

@@ -29,6 +29,8 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity
import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity
import org.koitharu.kotatsu.tracker.data.TracksDao import org.koitharu.kotatsu.tracker.data.TracksDao
const val DATABASE_VERSION = 13
@Database( @Database(
entities = [ entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, 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, TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class,
ScrobblingEntity::class, ScrobblingEntity::class,
], ],
version = 13, version = DATABASE_VERSION,
) )
abstract class MangaDatabase : RoomDatabase() { abstract class MangaDatabase : RoomDatabase() {

View 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"

View File

@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_MANGA
@Entity(tableName = "manga") @Entity(tableName = TABLE_MANGA)
class MangaEntity( data class MangaEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "manga_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,
@@ -18,44 +19,5 @@ class MangaEntity(
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?, @ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?, @ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "source") val source: 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
}
}

View File

@@ -3,9 +3,11 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
@Entity( @Entity(
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], tableName = TABLE_MANGA_TAGS,
primaryKeys = ["manga_id", "tag_id"],
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,
@@ -23,5 +25,5 @@ import androidx.room.ForeignKey
) )
class MangaTagsEntity( class MangaTagsEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @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,
) )

View File

@@ -3,35 +3,13 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_TAGS
@Entity(tableName = "tags") @Entity(tableName = TABLE_TAGS)
class TagEntity( data class TagEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long, @ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "key") val key: String, @ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "source") val source: 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
}
}

View File

@@ -6,38 +6,41 @@ import android.content.pm.ShortcutManager
import android.media.ThumbnailUtils import android.media.ThumbnailUtils
import android.os.Build import android.os.Build
import android.util.Size import android.util.Size
import androidx.annotation.VisibleForTesting
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import androidx.room.InvalidationTracker
import coil.ImageLoader import coil.ImageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers 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.R
import org.koitharu.kotatsu.base.domain.MangaDataRepository 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.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderActivity
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import org.koitharu.kotatsu.utils.ext.requireBitmap import org.koitharu.kotatsu.utils.ext.requireBitmap
class ShortcutsRepository( class ShortcutsUpdater(
private val context: Context, private val context: Context,
private val coil: ImageLoader, private val coil: ImageLoader,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val mangaRepository: MangaDataRepository, private val mangaRepository: MangaDataRepository,
) { ) : InvalidationTracker.Observer(TABLE_HISTORY) {
private val iconSize by lazy { private val iconSize by lazy { getIconSize(context) }
getIconSize(context) private var shortcutsUpdateJob: Job? = null
}
suspend fun updateShortcuts() { override fun onInvalidated(tables: MutableSet<String>) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return val prevJob = shortcutsUpdateJob
val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) {
val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) prevJob?.join()
.filter { x -> x.title.isNotEmpty() } updateShortcutsImpl()
.map { buildShortcutInfo(it).build().toShortcutInfo() } }
manager.dynamicShortcuts = shortcuts
} }
suspend fun requestPinShortcut(manga: Manga): Boolean { 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 { private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder {
val icon = runCatching { val icon = runCatching {
withContext(Dispatchers.IO) { val bmp = coil.execute(
val bmp = coil.execute( ImageRequest.Builder(context)
ImageRequest.Builder(context) .data(manga.coverUrl)
.data(manga.coverUrl) .size(iconSize.width, iconSize.height)
.size(iconSize.width, iconSize.height) .build()
.build() ).requireBitmap()
).requireBitmap() ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0)
}
}.fold( }.fold(
onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onSuccess = { IconCompat.createWithAdaptiveBitmap(it) },
onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) }

View File

@@ -34,7 +34,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity
import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.browser.BrowserActivity
import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver
import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga 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.databinding.ActivityDetailsBinding
import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
@@ -222,7 +222,7 @@ class DetailsActivity :
R.id.action_shortcut -> { R.id.action_shortcut -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
lifecycleScope.launch { lifecycleScope.launch {
if (!get<ShortcutsRepository>().requestPinShortcut(it)) { if (!get<ShortcutsUpdater>().requestPinShortcut(it)) {
binding.snackbar.show(getString(R.string.operation_not_supported)) binding.snackbar.show(getString(R.string.operation_not_supported))
} }
} }

View File

@@ -3,9 +3,10 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
@Entity(tableName = "favourite_categories") @Entity(tableName = TABLE_FAVOURITE_CATEGORIES)
class FavouriteCategoryEntity( data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "created_at") val createdAt: Long,

View File

@@ -3,10 +3,13 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [ tableName = TABLE_FAVOURITES,
primaryKeys = ["manga_id", "category_id"],
foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,
parentColumns = ["manga_id"], 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 = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "sort_key") val sortKey: Int, @ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "created_at") val createdAt: Long, @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
}
}

View File

@@ -10,5 +10,5 @@ val historyModule
single { HistoryRepository(get(), get(), get(), getAll()) } single { HistoryRepository(get(), get(), get(), getAll()) }
viewModel { HistoryListViewModel(get(), get(), get(), get()) } viewModel { HistoryListViewModel(get(), get(), get()) }
} }

View File

@@ -4,10 +4,11 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "history", tableName = TABLE_HISTORY,
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,
@@ -17,7 +18,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
) )
] ]
) )
class HistoryEntity( data class HistoryEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "created_at") val createdAt: Long,

View File

@@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.onStart
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.domain.ReversibleHandle 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.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.prefs.observeAsFlow import org.koitharu.kotatsu.core.prefs.observeAsFlow
@@ -31,7 +29,6 @@ import java.util.concurrent.TimeUnit
class HistoryListViewModel( class HistoryListViewModel(
private val repository: HistoryRepository, private val repository: HistoryRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val shortcutsRepository: ShortcutsRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
@@ -72,7 +69,6 @@ class HistoryListViewModel(
fun clearHistory() { fun clearHistory() {
launchLoadingJob { launchLoadingJob {
repository.clear() repository.clear()
shortcutsRepository.updateShortcuts()
} }
} }
@@ -81,10 +77,7 @@ class HistoryListViewModel(
return return
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = repository.deleteReversible(ids) + ReversibleHandle { val handle = repository.deleteReversible(ids)
shortcutsRepository.updateShortcuts()
}
shortcutsRepository.updateShortcuts()
onItemsRemoved.postCall(handle) onItemsRemoved.postCall(handle)
} }
} }

View File

@@ -11,6 +11,6 @@ val libraryModule
factory { LibraryRepository(get()) } factory { LibraryRepository(get()) }
viewModel { LibraryViewModel(get(), get(), get(), get(), get()) } viewModel { LibraryViewModel(get(), get(), get(), get()) }
viewModel { LibraryCategoriesConfigViewModel(get()) } viewModel { LibraryCategoriesConfigViewModel(get()) }
} }

View File

@@ -7,12 +7,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R 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.BaseViewModel
import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.base.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.model.FavouriteCategory 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.AppSettings
import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.ListMode
import org.koitharu.kotatsu.core.ui.DateTimeAgo import org.koitharu.kotatsu.core.ui.DateTimeAgo
@@ -36,7 +33,6 @@ private const val HISTORY_MAX_SEGMENTS = 2
class LibraryViewModel( class LibraryViewModel(
private val repository: LibraryRepository, private val repository: LibraryRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
private val trackingRepository: TrackingRepository, private val trackingRepository: TrackingRepository,
private val settings: AppSettings, private val settings: AppSettings,
) : BaseViewModel(), ListExtraProvider { ) : BaseViewModel(), ListExtraProvider {
@@ -88,10 +84,7 @@ class LibraryViewModel(
return return
} }
launchJob(Dispatchers.Default) { launchJob(Dispatchers.Default) {
val handle = historyRepository.deleteReversible(ids) + ReversibleHandle { val handle = historyRepository.deleteReversible(ids)
shortcutsRepository.updateShortcuts()
}
shortcutsRepository.updateShortcuts()
onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle)) onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle))
} }
} }
@@ -105,7 +98,6 @@ class LibraryViewModel(
historyRepository.deleteAfter(minDate) historyRepository.deleteAfter(minDate)
R.string.removed_from_history R.string.removed_from_history
} }
shortcutsRepository.updateShortcuts()
onActionDone.postCall(ReversibleAction(stringRes, null)) onActionDone.postCall(ReversibleAction(stringRes, null))
} }
} }

View File

@@ -16,5 +16,5 @@ val localModule
factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) }
viewModel { LocalListViewModel(get(), get(), get(), get()) } viewModel { LocalListViewModel(get(), get(), get()) }
} }

View File

@@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.os.ShortcutsRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.download.ui.service.DownloadService
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@@ -32,7 +31,6 @@ class LocalListViewModel(
private val repository: LocalMangaRepository, private val repository: LocalMangaRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
settings: AppSettings, settings: AppSettings,
private val shortcutsRepository: ShortcutsRepository,
) : MangaListViewModel(settings) { ) : MangaListViewModel(settings) {
val onMangaRemoved = SingleLiveEvent<Unit>() val onMangaRemoved = SingleLiveEvent<Unit>()
@@ -103,7 +101,6 @@ class LocalListViewModel(
} }
} }
} }
shortcutsRepository.updateShortcuts()
onMangaRemoved.call(Unit) onMangaRemoved.call(Unit)
} }
} }

View File

@@ -1,19 +1,29 @@
package org.koitharu.kotatsu.main 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.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.bind
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle 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.MainViewModel
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
val mainModule val mainModule
get() = module { get() = module {
single { AppProtectHelper(get()) } single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class
single { ActivityRecreationHandle() } single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class
factory { ShortcutsRepository(androidContext(), get(), get(), get()) }
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 { MainViewModel(get(), get()) }
viewModel { ProtectViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) }
} }

View File

@@ -23,7 +23,6 @@ val readerModule
preselectedBranch = params[2], preselectedBranch = params[2],
dataRepository = get(), dataRepository = get(),
historyRepository = get(), historyRepository = get(),
shortcutsRepository = get(),
settings = get(), settings = get(),
pageSaveHelper = get(), pageSaveHelper = get(),
bookmarksRepository = get(), bookmarksRepository = get(),

View File

@@ -17,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.Bookmark
import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository
import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException 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.parser.MangaRepository
import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.core.prefs.*
import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.history.domain.HistoryRepository
@@ -46,7 +45,6 @@ class ReaderViewModel(
private val preselectedBranch: String?, private val preselectedBranch: String?,
private val dataRepository: MangaDataRepository, private val dataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository, private val historyRepository: HistoryRepository,
private val shortcutsRepository: ShortcutsRepository,
private val bookmarksRepository: BookmarksRepository, private val bookmarksRepository: BookmarksRepository,
private val settings: AppSettings, private val settings: AppSettings,
private val pageSaveHelper: PageSaveHelper, private val pageSaveHelper: PageSaveHelper,
@@ -289,7 +287,6 @@ class ReaderViewModel(
currentState.value?.let { currentState.value?.let {
val percent = computePercent(it.chapterId, it.page) val percent = computePercent(it.chapterId, it.page)
historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent)
shortcutsRepository.updateShortcuts()
} }
content.postValue(ReaderContent(pages, currentState.value)) content.postValue(ReaderContent(pages, currentState.value))

View File

@@ -1,12 +1,14 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.net.Uri import android.net.Uri
import android.os.Build
import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.backup.BackupRepository 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.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel
@@ -18,8 +20,11 @@ import org.koitharu.kotatsu.settings.tools.ToolsViewModel
val settingsModule val settingsModule
get() = module { get() = module {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
single<InvalidationTracker.Observer> { BackupObserver(androidContext()) }
}
factory { BackupRepository(get()) } factory { BackupRepository(get()) }
factory { RestoreRepository(get()) }
single(createdAtStart = true) { AppSettings(androidContext()) } single(createdAtStart = true) { AppSettings(androidContext()) }
viewModel { BackupViewModel(get(), androidContext()) } viewModel { BackupViewModel(get(), androidContext()) }

View File

@@ -4,9 +4,14 @@ import android.app.backup.BackupAgent
import android.app.backup.BackupDataInput import android.app.backup.BackupDataInput
import android.app.backup.BackupDataOutput import android.app.backup.BackupDataOutput
import android.app.backup.FullBackupDataOutput import android.app.backup.FullBackupDataOutput
import android.content.Context
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.runBlocking 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 org.koitharu.kotatsu.core.db.MangaDatabase
import java.io.* import java.io.*
@@ -26,7 +31,7 @@ class AppBackupAgent : BackupAgent() {
override fun onFullBackup(data: FullBackupDataOutput) { override fun onFullBackup(data: FullBackupDataOutput) {
super.onFullBackup(data) super.onFullBackup(data)
val file = createBackupFile() val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext)))
try { try {
fullBackupFile(file, data) fullBackupFile(file, data)
} finally { } finally {
@@ -43,16 +48,16 @@ class AppBackupAgent : BackupAgent() {
mtime: Long mtime: Long
) { ) {
if (destination?.name?.endsWith(".bk.zip") == true) { if (destination?.name?.endsWith(".bk.zip") == true) {
restoreBackupFile(data.fileDescriptor, size) restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext)))
destination.delete() destination.delete()
} else { } else {
super.onRestoreFile(data, size, destination, type, mode, mtime) super.onRestoreFile(data, size, destination, type, mode, mtime)
} }
} }
private fun createBackupFile() = runBlocking { @VisibleForTesting
val repository = BackupRepository(MangaDatabase(applicationContext)) fun createBackupFile(context: Context, repository: BackupRepository) = runBlocking {
BackupZipOutput(this@AppBackupAgent).use { backup -> BackupZipOutput(context).use { backup ->
backup.put(repository.createIndex()) backup.put(repository.createIndex())
backup.put(repository.dumpHistory()) backup.put(repository.dumpHistory())
backup.put(repository.dumpCategories()) backup.put(repository.dumpCategories())
@@ -62,8 +67,8 @@ class AppBackupAgent : BackupAgent() {
} }
} }
private fun restoreBackupFile(fd: FileDescriptor, size: Long) { @VisibleForTesting
val repository = RestoreRepository(MangaDatabase(applicationContext)) fun restoreBackupFile(fd: FileDescriptor, size: Long, repository: BackupRepository) {
val tempFile = File.createTempFile("backup_", ".tmp") val tempFile = File.createTempFile("backup_", ".tmp")
FileInputStream(fd).use { input -> FileInputStream(fd).use { input ->
tempFile.outputStream().use { output -> tempFile.outputStream().use { output ->
@@ -73,9 +78,9 @@ class AppBackupAgent : BackupAgent() {
val backup = BackupZipInput(tempFile) val backup = BackupZipInput(tempFile)
try { try {
runBlocking { runBlocking {
repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY)) repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES)) repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES)) repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
} }
} finally { } finally {
backup.close() backup.close()

View File

@@ -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()
}
}

View File

@@ -7,9 +7,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.BaseViewModel
import org.koitharu.kotatsu.core.backup.BackupEntry 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.BackupZipInput
import org.koitharu.kotatsu.core.backup.CompositeResult 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.SingleLiveEvent
import org.koitharu.kotatsu.utils.progress.Progress import org.koitharu.kotatsu.utils.progress.Progress
import java.io.File import java.io.File
@@ -17,7 +17,7 @@ import java.io.FileNotFoundException
class RestoreViewModel( class RestoreViewModel(
uri: Uri?, uri: Uri?,
private val repository: RestoreRepository, private val repository: BackupRepository,
context: Context context: Context
) : BaseViewModel() { ) : BaseViewModel() {
@@ -44,13 +44,13 @@ class RestoreViewModel(
val result = CompositeResult() val result = CompositeResult()
progress.value = Progress(0, 3) progress.value = Progress(0, 3)
result += repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY)) result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY))
progress.value = Progress(1, 3) progress.value = Progress(1, 3)
result += repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES)) result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES))
progress.value = Progress(2, 3) progress.value = Progress(2, 3)
result += repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES)) result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES))
progress.value = Progress(3, 3) progress.value = Progress(3, 3)
onRestoreDone.call(result) onRestoreDone.call(result)

View File

@@ -1,10 +1,15 @@
package org.koitharu.kotatsu.widget 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.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
val appWidgetModule val appWidgetModule
get() = module { get() = module {
single<InvalidationTracker.Observer> { WidgetUpdater(androidContext()) }
viewModel { ShelfConfigViewModel(get()) } viewModel { ShelfConfigViewModel(get()) }
} }

View File

@@ -4,36 +4,24 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import kotlinx.coroutines.CancellationException import androidx.room.InvalidationTracker
import kotlinx.coroutines.Dispatchers import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import kotlinx.coroutines.flow.launchIn import org.koitharu.kotatsu.core.db.TABLE_HISTORY
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 org.koitharu.kotatsu.widget.recent.RecentWidgetProvider import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider 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) { override fun onInvalidated(tables: MutableSet<String>) {
repository.observeAll(SortOrder.NEWEST) if (TABLE_HISTORY in tables) {
.onEach { updateWidget(ShelfWidgetProvider::class.java) } updateWidgets(RecentWidgetProvider::class.java)
.retry { error -> error !is CancellationException } }
.launchIn(processLifecycleScope + Dispatchers.Default) if (TABLE_FAVOURITES in tables) {
updateWidgets(ShelfWidgetProvider::class.java)
}
} }
fun subscribeToHistory(repository: HistoryRepository) { private fun updateWidgets(cls: Class<*>) {
repository.observeAll()
.onEach { updateWidget(RecentWidgetProvider::class.java) }
.retry { error -> error !is CancellationException }
.launchIn(processLifecycleScope + Dispatchers.Default)
}
private fun updateWidget(cls: Class<*>) {
val intent = Intent(context, cls) val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context) val ids = AppWidgetManager.getInstance(context)

View File

@@ -0,0 +1,95 @@
package org.koitharu.kotatsu.core.backup
import org.junit.Assert.assertEquals
import org.junit.Test
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.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.SortOrder
import java.util.concurrent.TimeUnit
class JsonSerializerTest {
@Test
fun toFavouriteEntity() {
val entity = FavouriteEntity(
mangaId = 40,
categoryId = 20,
sortKey = 1,
createdAt = System.currentTimeMillis(),
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteEntity()
assertEquals(entity, result)
}
@Test
fun toMangaEntity() {
val entity = MangaEntity(
id = 231,
title = "Lorem Ipsum",
altTitle = "Lorem Ispum 2",
url = "erw",
publicUrl = "hthth",
rating = 0.78f,
isNsfw = true,
coverUrl = "5345",
largeCoverUrl = null,
state = MangaState.FINISHED.name,
author = "RERE",
source = MangaSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toMangaEntity()
assertEquals(entity, result)
}
@Test
fun toTagEntity() {
val entity = TagEntity(
id = 934023534,
title = "Adventure",
key = "adventure",
source = MangaSource.DUMMY.name,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toTagEntity()
assertEquals(entity, result)
}
@Test
fun toHistoryEntity() {
val entity = HistoryEntity(
mangaId = 304135341,
createdAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(6),
updatedAt = System.currentTimeMillis(),
chapterId = 29014843034,
page = 35,
scroll = 24.0f,
percent = 0.6f,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toHistoryEntity()
assertEquals(entity, result)
}
@Test
fun toFavouriteCategoryEntity() {
val entity = FavouriteCategoryEntity(
categoryId = 142,
createdAt = System.currentTimeMillis(),
sortKey = 14,
title = "Read later",
order = SortOrder.RATING.name,
track = false,
isVisibleInLibrary = true,
)
val json = JsonSerializer(entity).toJson()
val result = JsonDeserializer(json).toFavouriteCategoryEntity()
assertEquals(entity, result)
}
}

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.core.github
import kotlinx.coroutines.test.runTest
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.internal.headersContentLength
import org.junit.Assert
import org.junit.Test
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.parsers.util.await
class GithubRepositoryTest {
private val okHttpClient = OkHttpClient()
private val repository = GithubRepository(okHttpClient)
@Test
fun getLatestVersion() = runTest {
val version = repository.getLatestVersion()
val versionId = VersionId(version.name)
val apkHead = okHttpClient.newCall(
Request.Builder()
.url(version.apkUrl)
.head()
.build()
).await()
Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME))
Assert.assertTrue(apkHead.isSuccessful)
Assert.assertEquals(version.apkSize, apkHead.headersContentLength())
}
}