Compare commits

..

23 Commits
v6.2 ... v6.2.3

Author SHA1 Message Date
3d32bd9d58 Fix warnings 2023-10-25 15:42:00 +03:00
590120433c Update dependencies 2023-10-25 15:42:00 +03:00
4bd7656681 Fix loading footer in lists 2023-10-25 15:41:59 +03:00
2c7438e64d Add error reporting to import local manga 2023-10-25 15:41:59 +03:00
InfinityDouki56
665bebaa7b Translated using Weblate (Filipino)
Currently translated at 88.9% (443 of 498 strings)

Co-authored-by: InfinityDouki56 <ced.paltep10@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/fil/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
return_null
6ed5994726 Translated using Weblate (Chinese (Simplified))
Currently translated at 98.5% (491 of 498 strings)

Co-authored-by: return_null <demolang@dismail.de>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/zh_Hans/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
Dpper
311ed865b7 Translated using Weblate (Ukrainian)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Dpper <ruslan20020401@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
Bai
b59fb678fe Translated using Weblate (Turkish)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Bai <batuhanakkurt000@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/tr/
Translation: Kotatsu/Strings
2023-10-25 12:05:52 +03:00
ed9ebdcc55 Handle kotatsu scheme links 2023-10-23 17:20:44 +03:00
74569615e3 Fix splash background 2023-10-18 10:55:01 +03:00
f3c320a90f Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2023-10-18 10:02:13 +03:00
gallegonovato
a3012ab458 Translated using Weblate (Spanish)
Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/es/
Translation: Kotatsu/Strings
2023-10-18 09:59:52 +03:00
Макар Разин
6ec58879fd Translated using Weblate (Ukrainian)
Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (498 of 498 strings)

Translated using Weblate (Belarusian)

Currently translated at 100.0% (498 of 498 strings)

Co-authored-by: Макар Разин <makarrazin14@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/be/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/kotatsu/strings/uk/
Translation: Kotatsu/Strings
2023-10-18 09:59:52 +03:00
571cf08c53 Merge branch 'devel' of github.com:KotatsuApp/Kotatsu into devel 2023-10-18 09:50:52 +03:00
fca53eee7a Improve downloads list 2023-10-18 09:40:31 +03:00
ed9e2eb4d2 ActionMode and NavBar colors fix 2023-10-17 18:04:34 +03:00
c0e94f8415 Show chapters list in downloads 2023-10-17 12:19:44 +03:00
e172d619a1 Fix description expanding 2023-10-17 11:13:16 +03:00
d6c64fc638 Action to open online version of saved manga 2023-10-17 11:06:16 +03:00
37404cb9a6 UI improvements 2023-10-17 10:32:30 +03:00
9d5271ff26 Fix default branch selection #527 #528 2023-10-17 10:24:36 +03:00
5f59432e48 Handle NPE during network requests 2023-10-17 10:01:58 +03:00
5c082b5cdb Update parsers 2023-10-17 09:59:26 +03:00
88 changed files with 683 additions and 369 deletions

View File

@@ -16,11 +16,12 @@ android {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdk = 21 minSdk = 21
targetSdk = 34 targetSdk = 34
versionCode = 587 versionCode = 590
versionName = '6.2' versionName = '6.2.3'
generatedDensities = [] generatedDensities = []
testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner" testInstrumentationRunner "org.koitharu.kotatsu.HiltTestRunner"
ksp { ksp {
// arg("room.generateKotlin", "true") TODO: enable later
arg("room.schemaLocation", "$projectDir/schemas") arg("room.schemaLocation", "$projectDir/schemas")
} }
androidResources { androidResources {
@@ -81,7 +82,7 @@ afterEvaluate {
} }
dependencies { dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation('com.github.KotatsuApp:kotatsu-parsers:a61e441e79') { implementation('com.github.KotatsuApp:kotatsu-parsers:6bf0ae92e4') {
exclude group: 'org.json', module: 'json' exclude group: 'org.json', module: 'json'
} }
@@ -98,7 +99,7 @@ dependencies {
implementation 'androidx.lifecycle:lifecycle-process:2.6.2' implementation 'androidx.lifecycle:lifecycle-process:2.6.2'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.3.1' implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02' implementation 'androidx.viewpager2:viewpager2:1.1.0-beta02'
implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.preference:preference-ktx:1.2.1'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05' implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha05'
@@ -114,9 +115,9 @@ dependencies {
exclude group: 'com.google.j2objc', module: 'j2objc-annotations' exclude group: 'com.google.j2objc', module: 'j2objc-annotations'
} }
implementation 'androidx.room:room-runtime:2.5.2' implementation 'androidx.room:room-runtime:2.6.0'
implementation 'androidx.room:room-ktx:2.5.2' implementation 'androidx.room:room-ktx:2.6.0'
ksp 'androidx.room:room-compiler:2.5.2' ksp 'androidx.room:room-compiler:2.6.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0' implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0' implementation 'com.squareup.okhttp3:okhttp-dnsoverhttps:4.11.0'
@@ -152,7 +153,7 @@ dependencies {
androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3' androidTestImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
androidTestImplementation 'androidx.room:room-testing:2.5.2' androidTestImplementation 'androidx.room:room-testing:2.6.0'
androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0' androidTestImplementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1' androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48.1'

View File

@@ -82,7 +82,7 @@ class AppBackupAgentTest {
assertEquals(history, historyRepository.getOne(SampleData.manga)) assertEquals(history, historyRepository.getOne(SampleData.manga))
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id)) assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags() val allTags = database.getTagsDao().findTags(SampleData.tag.source.name).toMangaTags()
assertTrue(SampleData.tag in allTags) assertTrue(SampleData.tag in allTags)
} }

View File

@@ -83,6 +83,16 @@
<data android:host="kotatsu.app" /> <data android:host="kotatsu.app" />
<data android:path="/manga" /> <data android:path="/manga" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="kotatsu" />
<data android:host="manga" />
<data android:host="kotatsu.app" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity" android:name="org.koitharu.kotatsu.reader.ui.ReaderActivity"
@@ -327,6 +337,13 @@
<action android:name="android.intent.action.DOWNLOAD_COMPLETE" /> <action android:name="android.intent.action.DOWNLOAD_COMPLETE" />
</intent-filter> </intent-filter>
</receiver> </receiver>
<receiver
android:name="org.koitharu.kotatsu.core.ErrorReporterReceiver"
android:exported="false">
<intent-filter>
<action android:name="${applicationId}.action.REPORT_ERROR" />
</intent-filter>
</receiver>
<meta-data <meta-data
android:name="android.webkit.WebView.EnableSafeBrowsing" android:name="android.webkit.WebView.EnableSafeBrowsing"

View File

@@ -25,15 +25,15 @@ class BookmarksRepository @Inject constructor(
) { ) {
fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> { fun observeBookmark(manga: Manga, chapterId: Long, page: Int): Flow<Bookmark?> {
return db.bookmarksDao.observe(manga.id, chapterId, page).map { it?.toBookmark(manga) } return db.getBookmarksDao().observe(manga.id, chapterId, page).map { it?.toBookmark(manga) }
} }
fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> { fun observeBookmarks(manga: Manga): Flow<List<Bookmark>> {
return db.bookmarksDao.observe(manga.id).mapItems { it.toBookmark(manga) } return db.getBookmarksDao().observe(manga.id).mapItems { it.toBookmark(manga) }
} }
fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> { fun observeBookmarks(): Flow<Map<Manga, List<Bookmark>>> {
return db.bookmarksDao.observe().map { map -> return db.getBookmarksDao().observe().map { map ->
val res = LinkedHashMap<Manga, List<Bookmark>>(map.size) val res = LinkedHashMap<Manga, List<Bookmark>>(map.size)
for ((k, v) in map) { for ((k, v) in map) {
val manga = k.toManga() val manga = k.toManga()
@@ -46,9 +46,9 @@ class BookmarksRepository @Inject constructor(
suspend fun addBookmark(bookmark: Bookmark) { suspend fun addBookmark(bookmark: Bookmark) {
db.withTransaction { db.withTransaction {
val tags = bookmark.manga.tags.toEntities() val tags = bookmark.manga.tags.toEntities()
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(bookmark.manga.toEntity(), tags) db.getMangaDao().upsert(bookmark.manga.toEntity(), tags)
db.bookmarksDao.insert(bookmark.toEntity()) db.getBookmarksDao().insert(bookmark.toEntity())
} }
} }
@@ -56,11 +56,11 @@ class BookmarksRepository @Inject constructor(
val entity = bookmark.toEntity().copy( val entity = bookmark.toEntity().copy(
imageUrl = imageUrl, imageUrl = imageUrl,
) )
db.bookmarksDao.upsert(listOf(entity)) db.getBookmarksDao().upsert(listOf(entity))
} }
suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) { suspend fun removeBookmark(mangaId: Long, chapterId: Long, page: Int) {
check(db.bookmarksDao.delete(mangaId, chapterId, page) != 0) { check(db.getBookmarksDao().delete(mangaId, chapterId, page) != 0) {
"Bookmark not found" "Bookmark not found"
} }
} }
@@ -72,7 +72,7 @@ class BookmarksRepository @Inject constructor(
suspend fun removeBookmarks(ids: Set<Long>): ReversibleHandle { suspend fun removeBookmarks(ids: Set<Long>): ReversibleHandle {
val entities = ArrayList<BookmarkEntity>(ids.size) val entities = ArrayList<BookmarkEntity>(ids.size)
db.withTransaction { db.withTransaction {
val dao = db.bookmarksDao val dao = db.getBookmarksDao()
for (pageId in ids) { for (pageId in ids) {
val e = dao.find(pageId) val e = dao.find(pageId)
if (e != null) { if (e != null) {
@@ -92,7 +92,7 @@ class BookmarksRepository @Inject constructor(
db.withTransaction { db.withTransaction {
for (e in entities) { for (e in entities) {
try { try {
db.bookmarksDao.insert(e) db.getBookmarksDao().insert(e)
} catch (e: SQLException) { } catch (e: SQLException) {
e.printStackTraceDebug() e.printStackTraceDebug()
} }

View File

@@ -34,8 +34,8 @@ class BookmarksActivity :
val fm = supportFragmentManager val fm = supportFragmentManager
if (fm.findFragmentById(R.id.container) == null) { if (fm.findFragmentById(R.id.container) == null) {
fm.commit { fm.commit {
val fragment = BookmarksFragment.newInstance() setReorderingAllowed(true)
replace(R.id.container, fragment) replace(R.id.container, BookmarksFragment::class.java, null)
} }
} }
} }

View File

@@ -0,0 +1,33 @@
package org.koitharu.kotatsu.core
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.core.app.PendingIntentCompat
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.util.ext.getSerializableExtraCompat
import org.koitharu.kotatsu.core.util.ext.report
class ErrorReporterReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val e = intent?.getSerializableExtraCompat<Throwable>(EXTRA_ERROR) ?: return
e.report()
}
companion object {
private const val EXTRA_ERROR = "err"
private const val ACTION_REPORT = "${BuildConfig.APPLICATION_ID}.action.REPORT_ERROR"
fun getPendingIntent(context: Context, e: Throwable): PendingIntent {
val intent = Intent(context, ErrorReporterReceiver::class.java)
intent.setAction(ACTION_REPORT)
intent.setData(Uri.parse("err://${e.hashCode()}"))
intent.putExtra(EXTRA_ERROR, e)
return checkNotNull(PendingIntentCompat.getBroadcast(context, 0, intent, 0, false))
}
}
}

View File

@@ -22,7 +22,7 @@ class BackupRepository @Inject constructor(
var offset = 0 var offset = 0
val entry = BackupEntry(BackupEntry.HISTORY, JSONArray()) val entry = BackupEntry(BackupEntry.HISTORY, JSONArray())
while (true) { while (true) {
val history = db.historyDao.findAll(offset, PAGE_SIZE) val history = db.getHistoryDao().findAll(offset, PAGE_SIZE)
if (history.isEmpty()) { if (history.isEmpty()) {
break break
} }
@@ -42,7 +42,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpCategories(): BackupEntry { suspend fun dumpCategories(): BackupEntry {
val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray()) val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray())
val categories = db.favouriteCategoriesDao.findAll() val categories = db.getFavouriteCategoriesDao().findAll()
for (item in categories) { for (item in categories) {
entry.data.put(JsonSerializer(item).toJson()) entry.data.put(JsonSerializer(item).toJson())
} }
@@ -53,7 +53,7 @@ class BackupRepository @Inject constructor(
var offset = 0 var offset = 0
val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray()) val entry = BackupEntry(BackupEntry.FAVOURITES, JSONArray())
while (true) { while (true) {
val favourites = db.favouritesDao.findAll(offset, PAGE_SIZE) val favourites = db.getFavouritesDao().findAll(offset, PAGE_SIZE)
if (favourites.isEmpty()) { if (favourites.isEmpty()) {
break break
} }
@@ -73,7 +73,7 @@ class BackupRepository @Inject constructor(
suspend fun dumpBookmarks(): BackupEntry { suspend fun dumpBookmarks(): BackupEntry {
val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray()) val entry = BackupEntry(BackupEntry.BOOKMARKS, JSONArray())
val all = db.bookmarksDao.findAll() val all = db.getBookmarksDao().findAll()
for ((m, b) in all) { for ((m, b) in all) {
val json = JSONObject() val json = JSONObject()
val manga = JsonSerializer(m.manga).toJson() val manga = JsonSerializer(m.manga).toJson()
@@ -122,9 +122,9 @@ class BackupRepository @Inject constructor(
val history = JsonDeserializer(item).toHistoryEntity() val history = JsonDeserializer(item).toHistoryEntity()
result += runCatchingCancellable { result += runCatchingCancellable {
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga, tags) db.getMangaDao().upsert(manga, tags)
db.historyDao.upsert(history) db.getHistoryDao().upsert(history)
} }
} }
} }
@@ -136,7 +136,7 @@ class BackupRepository @Inject constructor(
for (item in entry.data.JSONIterator()) { for (item in entry.data.JSONIterator()) {
val category = JsonDeserializer(item).toFavouriteCategoryEntity() val category = JsonDeserializer(item).toFavouriteCategoryEntity()
result += runCatchingCancellable { result += runCatchingCancellable {
db.favouriteCategoriesDao.upsert(category) db.getFavouriteCategoriesDao().upsert(category)
} }
} }
return result return result
@@ -153,9 +153,9 @@ class BackupRepository @Inject constructor(
val favourite = JsonDeserializer(item).toFavouriteEntity() val favourite = JsonDeserializer(item).toFavouriteEntity()
result += runCatchingCancellable { result += runCatchingCancellable {
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga, tags) db.getMangaDao().upsert(manga, tags)
db.favouritesDao.upsert(favourite) db.getFavouritesDao().upsert(favourite)
} }
} }
} }
@@ -175,9 +175,9 @@ class BackupRepository @Inject constructor(
} }
result += runCatchingCancellable { result += runCatchingCancellable {
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga, tags) db.getMangaDao().upsert(manga, tags)
db.bookmarksDao.upsert(bookmarks) db.getBookmarksDao().upsert(bookmarks)
} }
} }
} }

View File

@@ -66,29 +66,29 @@ const val DATABASE_VERSION = 17
) )
abstract class MangaDatabase : RoomDatabase() { abstract class MangaDatabase : RoomDatabase() {
abstract val historyDao: HistoryDao abstract fun getHistoryDao(): HistoryDao
abstract val tagsDao: TagsDao abstract fun getTagsDao(): TagsDao
abstract val mangaDao: MangaDao abstract fun getMangaDao(): MangaDao
abstract val favouritesDao: FavouritesDao abstract fun getFavouritesDao(): FavouritesDao
abstract val preferencesDao: PreferencesDao abstract fun getPreferencesDao(): PreferencesDao
abstract val favouriteCategoriesDao: FavouriteCategoriesDao abstract fun getFavouriteCategoriesDao(): FavouriteCategoriesDao
abstract val tracksDao: TracksDao abstract fun getTracksDao(): TracksDao
abstract val trackLogsDao: TrackLogsDao abstract fun getTrackLogsDao(): TrackLogsDao
abstract val suggestionDao: SuggestionDao abstract fun getSuggestionDao(): SuggestionDao
abstract val bookmarksDao: BookmarksDao abstract fun getBookmarksDao(): BookmarksDao
abstract val scrobblingDao: ScrobblingDao abstract fun getScrobblingDao(): ScrobblingDao
abstract val sourcesDao: MangaSourcesDao abstract fun getSourcesDao(): MangaSourcesDao
} }
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf( fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(

View File

@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration10To11 : Migration(10, 11) { class Migration10To11 : Migration(10, 11) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL( db.execSQL(
""" """
CREATE TABLE IF NOT EXISTS `bookmarks` ( CREATE TABLE IF NOT EXISTS `bookmarks` (
`manga_id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL,
@@ -20,7 +20,7 @@ class Migration10To11 : Migration(10, 11) {
FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE ) FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )
""".trimIndent() """.trimIndent()
) )
database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `bookmarks` (`manga_id`)")
database.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)") db.execSQL("CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `bookmarks` (`page_id`)")
} }
} }

View File

@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration11To12 : Migration(11, 12) { class Migration11To12 : Migration(11, 12) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL( db.execSQL(
""" """
CREATE TABLE IF NOT EXISTS `scrobblings` ( CREATE TABLE IF NOT EXISTS `scrobblings` (
`scrobbler` INTEGER NOT NULL, `scrobbler` INTEGER NOT NULL,
@@ -21,7 +21,7 @@ class Migration11To12 : Migration(11, 12) {
) )
""".trimIndent() """.trimIndent()
) )
database.execSQL("ALTER TABLE history ADD COLUMN `percent` REAL NOT NULL DEFAULT -1") db.execSQL("ALTER TABLE history ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
database.execSQL("ALTER TABLE bookmarks ADD COLUMN `percent` REAL NOT NULL DEFAULT -1") db.execSQL("ALTER TABLE bookmarks ADD COLUMN `percent` REAL NOT NULL DEFAULT -1")
} }
} }

View File

@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration12To13 : Migration(12, 13) { class Migration12To13 : Migration(12, 13) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1") db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `show_in_lib` INTEGER NOT NULL DEFAULT 1")
database.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE favourites ADD COLUMN `sort_key` INTEGER NOT NULL DEFAULT 0")
} }
} }

View File

@@ -5,11 +5,11 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration13To14 : Migration(13, 14) { class Migration13To14 : Migration(13, 14) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE favourites ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE history ADD COLUMN `deleted_at` INTEGER NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_brightness` REAL NOT NULL DEFAULT 0")
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_contrast` REAL NOT NULL DEFAULT 0")
} }
} }

View File

@@ -5,5 +5,5 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration14To15 : Migration(14, 15) { class Migration14To15 : Migration(14, 15) {
override fun migrate(database: SupportSQLiteDatabase) = Unit override fun migrate(db: SupportSQLiteDatabase) = Unit
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration15To16 : Migration(15, 16) { class Migration15To16 : Migration(15, 16) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE preferences ADD COLUMN `cf_invert` INTEGER NOT NULL DEFAULT 0")
} }
} }

View File

@@ -10,9 +10,9 @@ class Migration16To17(context: Context) : Migration(16, 17) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE `sources` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))") db.execSQL("CREATE TABLE `sources` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))")
database.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)") db.execSQL("CREATE INDEX `index_sources_sort_key` ON `sources` (`sort_key`)")
val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty() val hiddenSources = prefs.getStringSet("sources_hidden", null).orEmpty()
val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty() val order = prefs.getString("sources_order_2", null)?.split('|').orEmpty()
val sources = MangaSource.entries val sources = MangaSource.entries
@@ -30,7 +30,7 @@ class Migration16To17(context: Context) : Migration(16, 17) {
continue continue
} }
} }
database.execSQL( db.execSQL(
"INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)", "INSERT INTO `sources` (`source`, `enabled`, `sort_key`) VALUES (?, ?, ?)",
arrayOf(name, (!isHidden).toInt(), sortKey), arrayOf(name, (!isHidden).toInt(), sortKey),
) )

View File

@@ -7,48 +7,48 @@ class Migration1To2 : Migration(1, 2) {
/** /**
* Adding foreign keys * Adding foreign keys
*/ */
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
/* manga_tags */ /* manga_tags */
database.execSQL( db.execSQL(
"CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " + "CREATE TABLE IF NOT EXISTS manga_tags_tmp (manga_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, tag_id), " + "PRIMARY KEY(manga_id, tag_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " + "FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE, " +
"FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )" "FOREIGN KEY(tag_id) REFERENCES tags(tag_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
) )
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_manga_id ON manga_tags_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_manga_tags_tag_id ON manga_tags_tmp (tag_id)")
database.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags") db.execSQL("INSERT INTO manga_tags_tmp (manga_id, tag_id) SELECT manga_id, tag_id FROM manga_tags")
database.execSQL("DROP TABLE manga_tags") db.execSQL("DROP TABLE manga_tags")
database.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags") db.execSQL("ALTER TABLE manga_tags_tmp RENAME TO manga_tags")
/* favourites */ /* favourites */
database.execSQL( db.execSQL(
"CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " + "CREATE TABLE IF NOT EXISTS favourites_tmp (manga_id INTEGER NOT NULL, category_id INTEGER NOT NULL, created_at INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id, category_id), " + "PRIMARY KEY(manga_id, category_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " + "FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE , " +
"FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )" "FOREIGN KEY(category_id) REFERENCES favourite_categories(category_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
) )
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_manga_id ON favourites_tmp (manga_id)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_favourites_category_id ON favourites_tmp (category_id)")
database.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites") db.execSQL("INSERT INTO favourites_tmp (manga_id, category_id, created_at) SELECT manga_id, category_id, created_at FROM favourites")
database.execSQL("DROP TABLE favourites") db.execSQL("DROP TABLE favourites")
database.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites") db.execSQL("ALTER TABLE favourites_tmp RENAME TO favourites")
/* history */ /* history */
database.execSQL( db.execSQL(
"CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " + "CREATE TABLE IF NOT EXISTS history_tmp (manga_id INTEGER NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, chapter_id INTEGER NOT NULL, page INTEGER NOT NULL, " +
"PRIMARY KEY(manga_id), " + "PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )" "FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
) )
database.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history") db.execSQL("INSERT INTO history_tmp (manga_id, created_at, updated_at, chapter_id, page) SELECT manga_id, created_at, updated_at, chapter_id, page FROM history")
database.execSQL("DROP TABLE history") db.execSQL("DROP TABLE history")
database.execSQL("ALTER TABLE history_tmp RENAME TO history") db.execSQL("ALTER TABLE history_tmp RENAME TO history")
/* preferences */ /* preferences */
database.execSQL( db.execSQL(
"CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," + "CREATE TABLE IF NOT EXISTS preferences_tmp (manga_id INTEGER NOT NULL, mode INTEGER NOT NULL," +
" PRIMARY KEY(manga_id), " + " PRIMARY KEY(manga_id), " +
"FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )" "FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )"
) )
database.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences") db.execSQL("INSERT INTO preferences_tmp (manga_id, mode) SELECT manga_id, mode FROM preferences")
database.execSQL("DROP TABLE preferences") db.execSQL("DROP TABLE preferences")
database.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences") db.execSQL("ALTER TABLE preferences_tmp RENAME TO preferences")
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration2To3 : Migration(2, 3) { class Migration2To3 : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE history ADD COLUMN scroll REAL NOT NULL DEFAULT 0")
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration3To4 : Migration(3, 4) { class Migration3To4 : Migration(3, 4) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS tracks (manga_id INTEGER NOT NULL, chapters_total INTEGER NOT NULL, last_chapter_id INTEGER NOT NULL, chapters_new INTEGER NOT NULL, last_check INTEGER NOT NULL, last_notified_id INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration4To5 : Migration(4, 5) { class Migration4To5 : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
} }
} }

View File

@@ -5,8 +5,8 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration5To6 : Migration(5, 6) { class Migration5To6 : Migration(5, 6) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)") db.execSQL("CREATE TABLE IF NOT EXISTS track_logs (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, manga_id INTEGER NOT NULL, chapters TEXT NOT NULL, created_at INTEGER NOT NULL, FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE)")
database.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_track_logs_manga_id ON track_logs (manga_id)")
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration6To7 : Migration(6, 7) { class Migration6To7 : Migration(6, 7) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''") db.execSQL("ALTER TABLE manga ADD COLUMN public_url TEXT NOT NULL DEFAULT ''")
} }
} }

View File

@@ -5,9 +5,9 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration7To8 : Migration(7, 8) { class Migration7To8 : Migration(7, 8) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0") db.execSQL("ALTER TABLE manga ADD COLUMN nsfw INTEGER NOT NULL DEFAULT 0")
database.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )") db.execSQL("CREATE TABLE IF NOT EXISTS suggestions (manga_id INTEGER NOT NULL, relevance REAL NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY(manga_id), FOREIGN KEY(manga_id) REFERENCES manga(manga_id) ON UPDATE NO ACTION ON DELETE CASCADE )")
database.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)") db.execSQL("CREATE INDEX IF NOT EXISTS index_suggestions_manga_id ON suggestions (manga_id)")
} }
} }

View File

@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
class Migration8To9 : Migration(8, 9) { class Migration8To9 : Migration(8, 9) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}") db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `order` TEXT NOT NULL DEFAULT ${SortOrder.NEWEST.name}")
} }
} }

View File

@@ -5,7 +5,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase
class Migration9To10 : Migration(9, 10) { class Migration9To10 : Migration(9, 10) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(db: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1") db.execSQL("ALTER TABLE favourite_categories ADD COLUMN `track` INTEGER NOT NULL DEFAULT 1")
} }
} }

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okio.IOException
import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING import org.koitharu.kotatsu.core.network.CommonHeaders.CONTENT_ENCODING
class GZipInterceptor : Interceptor { class GZipInterceptor : Interceptor {
@@ -9,6 +10,10 @@ class GZipInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val newRequest = chain.request().newBuilder() val newRequest = chain.request().newBuilder()
newRequest.addHeader(CONTENT_ENCODING, "gzip") newRequest.addHeader(CONTENT_ENCODING, "gzip")
return chain.proceed(newRequest.build()) return try {
chain.proceed(newRequest.build())
} catch (e: NullPointerException) {
throw IOException(e)
}
} }
} }

View File

@@ -28,16 +28,16 @@ class MangaDataRepository @Inject constructor(
suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) { suspend fun saveReaderMode(manga: Manga, mode: ReaderMode) {
db.withTransaction { db.withTransaction {
storeManga(manga) storeManga(manga)
val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id) val entity = db.getPreferencesDao().find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert(entity.copy(mode = mode.id)) db.getPreferencesDao().upsert(entity.copy(mode = mode.id))
} }
} }
suspend fun saveColorFilter(manga: Manga, colorFilter: ReaderColorFilter?) { suspend fun saveColorFilter(manga: Manga, colorFilter: ReaderColorFilter?) {
db.withTransaction { db.withTransaction {
storeManga(manga) storeManga(manga)
val entity = db.preferencesDao.find(manga.id) ?: newEntity(manga.id) val entity = db.getPreferencesDao().find(manga.id) ?: newEntity(manga.id)
db.preferencesDao.upsert( db.getPreferencesDao().upsert(
entity.copy( entity.copy(
cfBrightness = colorFilter?.brightness ?: 0f, cfBrightness = colorFilter?.brightness ?: 0f,
cfContrast = colorFilter?.contrast ?: 0f, cfContrast = colorFilter?.contrast ?: 0f,
@@ -48,25 +48,25 @@ class MangaDataRepository @Inject constructor(
} }
suspend fun getReaderMode(mangaId: Long): ReaderMode? { suspend fun getReaderMode(mangaId: Long): ReaderMode? {
return db.preferencesDao.find(mangaId)?.let { ReaderMode.valueOf(it.mode) } return db.getPreferencesDao().find(mangaId)?.let { ReaderMode.valueOf(it.mode) }
} }
suspend fun getColorFilter(mangaId: Long): ReaderColorFilter? { suspend fun getColorFilter(mangaId: Long): ReaderColorFilter? {
return db.preferencesDao.find(mangaId)?.getColorFilterOrNull() return db.getPreferencesDao().find(mangaId)?.getColorFilterOrNull()
} }
fun observeColorFilter(mangaId: Long): Flow<ReaderColorFilter?> { fun observeColorFilter(mangaId: Long): Flow<ReaderColorFilter?> {
return db.preferencesDao.observe(mangaId) return db.getPreferencesDao().observe(mangaId)
.map { it?.getColorFilterOrNull() } .map { it?.getColorFilterOrNull() }
.distinctUntilChanged() .distinctUntilChanged()
} }
suspend fun findMangaById(mangaId: Long): Manga? { suspend fun findMangaById(mangaId: Long): Manga? {
return db.mangaDao.find(mangaId)?.toManga() return db.getMangaDao().find(mangaId)?.toManga()
} }
suspend fun findMangaByPublicUrl(publicUrl: String): Manga? { suspend fun findMangaByPublicUrl(publicUrl: String): Manga? {
return db.mangaDao.findByPublicUrl(publicUrl)?.toManga() return db.getMangaDao().findByPublicUrl(publicUrl)?.toManga()
} }
suspend fun resolveIntent(intent: MangaIntent): Manga? = when { suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
@@ -79,13 +79,13 @@ class MangaDataRepository @Inject constructor(
suspend fun storeManga(manga: Manga) { suspend fun storeManga(manga: Manga) {
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags) db.getMangaDao().upsert(manga.toEntity(), tags)
} }
} }
suspend fun findTags(source: MangaSource): Set<MangaTag> { suspend fun findTags(source: MangaSource): Set<MangaTag> {
return db.tagsDao.findTags(source.name).toMangaTags() return db.getTagsDao().findTags(source.name).toMangaTags()
} }
private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? { private fun MangaPrefsEntity.getColorFilterOrNull(): ReaderColorFilter? {

View File

@@ -23,14 +23,14 @@ class MangaLinkResolver @Inject constructor(
) { ) {
suspend fun resolve(uri: Uri): Manga { suspend fun resolve(uri: Uri): Manga {
return if (uri.host == "kotatsu.app") { return if (uri.scheme == "kotatsu" || uri.host == "kotatsu.app") {
resolveAppLink(uri) resolveAppLink(uri)
} else { } else {
resolveExternalLink(uri) resolveExternalLink(uri)
} ?: throw NotFoundException("Manga not found", uri.toString()) } ?: throw NotFoundException("Cannot resolve link", uri.toString())
} }
suspend fun resolveAppLink(uri: Uri): Manga? { private suspend fun resolveAppLink(uri: Uri): Manga? {
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" } require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" } val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
val source = MangaSource(sourceName) val source = MangaSource(sourceName)
@@ -42,7 +42,7 @@ class MangaLinkResolver @Inject constructor(
) )
} }
suspend fun resolveExternalLink(uri: Uri): Manga? { private suspend fun resolveExternalLink(uri: Uri): Manga? {
dataRepository.findMangaByPublicUrl(uri.toString())?.let { dataRepository.findMangaByPublicUrl(uri.toString())?.let {
return it return it
} }

View File

@@ -128,6 +128,10 @@ class RemoteMangaRepository(
return details.await() return details.await()
} }
suspend fun peekDetails(manga: Manga): Manga? {
return cache.getDetails(source, manga.url)
}
suspend fun find(manga: Manga): Manga? { suspend fun find(manga: Manga): Manga? {
val list = getList(0, manga.title) val list = getList(0, manga.title)
return list.find { x -> x.id == manga.id } return list.find { x -> x.id == manga.id }

View File

@@ -97,7 +97,8 @@ abstract class BaseActivity<B : ViewBinding> :
} }
override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) { override fun onOptionsItemSelected(item: MenuItem) = if (item.itemId == android.R.id.home) {
onBackPressed() onBackPressedDispatcher.onBackPressed()
// TODO: navigateUpTo
true true
} else super.onOptionsItemSelected(item) } else super.onOptionsItemSelected(item)
@@ -126,10 +127,10 @@ abstract class BaseActivity<B : ViewBinding> :
val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val actionModeColor = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
ColorUtils.compositeColors( ColorUtils.compositeColors(
ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color), ContextCompat.getColor(this, com.google.android.material.R.color.m3_appbar_overlay_color),
getThemeColor(com.google.android.material.R.attr.colorSurface), getThemeColor(R.attr.m3ColorBackground),
) )
} else { } else {
ContextCompat.getColor(this, R.color.kotatsu_secondaryContainer) ContextCompat.getColor(this, R.color.kotatsu_m3_background)
} }
val insets = ViewCompat.getRootWindowInsets(viewBinding.root) val insets = ViewCompat.getRootWindowInsets(viewBinding.root)
?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return ?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: return

View File

@@ -26,6 +26,17 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
} }
} }
fun <T> Flow<T>.onEachWhile(action: suspend (T) -> Boolean): Flow<T> {
var isCalled = false
return onEach {
if (!isCalled) {
isCalled = action(it)
}
}.onCompletion {
isCalled = false
}
}
inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> { inline fun <T, R> Flow<List<T>>.mapItems(crossinline transform: (T) -> R): Flow<List<R>> {
return map { list -> list.map(transform) } return map { list -> list.map(transform) }
} }

View File

@@ -86,4 +86,6 @@ class DetailsInteractor @Inject constructor(
subject subject
} }
} }
suspend fun findLocal(seed: Manga) = localMangaRepository.getRemoteManga(seed)
} }

View File

@@ -16,7 +16,7 @@ class ProgressUpdateUseCase @Inject constructor(
) { ) {
suspend operator fun invoke(manga: Manga): Float { suspend operator fun invoke(manga: Manga): Float {
val history = database.historyDao.find(manga.id) ?: return PROGRESS_NONE val history = database.getHistoryDao().find(manga.id) ?: return PROGRESS_NONE
val seed = if (manga.isLocal) { val seed = if (manga.isLocal) {
localMangaRepository.getRemoteManga(manga) ?: manga localMangaRepository.getRemoteManga(manga) ?: manga
} else { } else {
@@ -43,7 +43,7 @@ class ProgressUpdateUseCase @Inject constructor(
val ppc = 1f / chaptersCount val ppc = 1f / chaptersCount
val result = ppc * chapterIndex + ppc * pagePercent val result = ppc * chapterIndex + ppc * pagePercent
if (result != history.percent) { if (result != history.percent) {
database.historyDao.update( database.getHistoryDao().update(
history.copy( history.copy(
chapterId = chapter.id, chapterId = chapter.id,
percent = result, percent = result,

View File

@@ -137,7 +137,9 @@ class DetailsActivity :
this, this,
MenuInvalidator(viewBinding.toolbarChapters ?: this), MenuInvalidator(viewBinding.toolbarChapters ?: this),
) )
viewModel.favouriteCategories.observe(this, MenuInvalidator(this)) val menuInvalidator = MenuInvalidator(this)
viewModel.favouriteCategories.observe(this, menuInvalidator)
viewModel.remoteManga.observe(this, menuInvalidator)
viewModel.branches.observe(this) { viewModel.branches.observe(this) {
viewBinding.buttonDropdown.isVisible = it.size > 1 viewBinding.buttonDropdown.isVisible = it.size > 1
} }

View File

@@ -81,7 +81,7 @@ class DetailsFragment :
BaseFragment<FragmentDetailsBinding>(), BaseFragment<FragmentDetailsBinding>(),
View.OnClickListener, View.OnClickListener,
ChipsView.OnChipClickListener, ChipsView.OnChipClickListener,
OnListItemClickListener<Bookmark>, ViewTreeObserver.OnDrawListener { OnListItemClickListener<Bookmark>, ViewTreeObserver.OnDrawListener, View.OnLayoutChangeListener {
@Inject @Inject
lateinit var coil: ImageLoader lateinit var coil: ImageLoader
@@ -105,6 +105,7 @@ class DetailsFragment :
binding.buttonScrobblingMore.setOnClickListener(this) binding.buttonScrobblingMore.setOnClickListener(this)
binding.buttonRelatedMore.setOnClickListener(this) binding.buttonRelatedMore.setOnClickListener(this)
binding.infoLayout.textViewSource.setOnClickListener(this) binding.infoLayout.textViewSource.setOnClickListener(this)
binding.textViewDescription.addOnLayoutChangeListener(this)
binding.textViewDescription.viewTreeObserver.addOnDrawListener(this) binding.textViewDescription.viewTreeObserver.addOnDrawListener(this)
binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance() binding.textViewDescription.movementMethod = LinkMovementMethod.getInstance()
binding.chipsTags.onChipClickListener = this binding.chipsTags.onChipClickListener = this
@@ -150,6 +151,22 @@ class DetailsFragment :
} }
} }
override fun onLayoutChange(
v: View?,
left: Int,
top: Int,
right: Int,
bottom: Int,
oldLeft: Int,
oldTop: Int,
oldRight: Int,
oldBottom: Int
) {
with(viewBinding ?: return) {
buttonDescriptionMore.isVisible = textViewDescription.isTextTruncated
}
}
private fun onMangaUpdated(manga: Manga) { private fun onMangaUpdated(manga: Manga) {
with(requireViewBinding()) { with(requireViewBinding()) {
// Main // Main
@@ -228,7 +245,6 @@ class DetailsFragment :
} else { } else {
tv.text = description tv.text = description
} }
requireViewBinding().buttonDescriptionMore.isVisible = tv.isTextTruncated
} }
private fun onLocalSizeChanged(size: Long) { private fun onLocalSizeChanged(size: Long) {

View File

@@ -42,6 +42,7 @@ class DetailsMenuProvider(
menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL menu.findItem(R.id.action_browser).isVisible = manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity) menu.findItem(R.id.action_shortcut).isVisible = ShortcutManagerCompat.isRequestPinShortcutSupported(activity)
menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable menu.findItem(R.id.action_scrobbling).isVisible = viewModel.isScrobblingAvailable
menu.findItem(R.id.action_online).isVisible = viewModel.remoteManga.value != null
menu.findItem(R.id.action_favourite).setIcon( menu.findItem(R.id.action_favourite).setIcon(
if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline, if (viewModel.favouriteCategories.value) R.drawable.ic_heart else R.drawable.ic_heart_outline,
) )
@@ -88,6 +89,12 @@ class DetailsMenuProvider(
} }
} }
R.id.action_online -> {
viewModel.remoteManga.value?.let {
activity.startActivity(DetailsActivity.newIntent(activity, it))
}
}
R.id.action_related -> { R.id.action_related -> {
viewModel.manga.value?.let { viewModel.manga.value?.let {
activity.startActivity(MultiSearchActivity.newIntent(activity, it.title)) activity.startActivity(MultiSearchActivity.newIntent(activity, it.title))

View File

@@ -33,7 +33,7 @@ import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.computeSize import org.koitharu.kotatsu.core.util.ext.computeSize
import org.koitharu.kotatsu.core.util.ext.onFirst import org.koitharu.kotatsu.core.util.ext.onEachWhile
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.details.data.MangaDetails import org.koitharu.kotatsu.details.data.MangaDetails
import org.koitharu.kotatsu.details.domain.BranchComparator import org.koitharu.kotatsu.details.domain.BranchComparator
@@ -94,6 +94,8 @@ class DetailsViewModel @Inject constructor(
val favouriteCategories = interactor.observeIsFavourite(mangaId) val favouriteCategories = interactor.observeIsFavourite(mangaId)
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false) .stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
val remoteManga = MutableStateFlow<Manga?>(null)
val newChaptersCount = details.flatMapLatest { d -> val newChaptersCount = details.flatMapLatest { d ->
if (d?.isLocal == false) { if (d?.isLocal == false) {
interactor.observeNewChapters(mangaId) interactor.observeNewChapters(mangaId)
@@ -213,6 +215,10 @@ class DetailsViewModel @Inject constructor(
progressUpdateUseCase(manga.toManga()) progressUpdateUseCase(manga.toManga())
} }
} }
launchJob(Dispatchers.Default) {
val manga = details.firstOrNull { it != null && it.isLocal } ?: return@launchJob
remoteManga.value = interactor.findLocal(manga.toManga())
}
} }
fun reload() { fun reload() {
@@ -313,11 +319,15 @@ class DetailsViewModel @Inject constructor(
private fun doLoad() = launchLoadingJob(Dispatchers.Default) { private fun doLoad() = launchLoadingJob(Dispatchers.Default) {
detailsLoadUseCase.invoke(intent) detailsLoadUseCase.invoke(intent)
.onFirst { .onEachWhile {
if (it.allChapters.isEmpty()) {
return@onEachWhile false
}
val manga = it.toManga() val manga = it.toManga()
// find default branch // find default branch
val hist = historyRepository.getOne(manga) val hist = historyRepository.getOne(manga)
selectedBranch.value = manga.getPreferredBranch(hist) selectedBranch.value = manga.getPreferredBranch(hist)
true
}.collect { }.collect {
details.value = it details.value = it
} }

View File

@@ -19,6 +19,7 @@ data class DownloadState(
val eta: Long = -1L, val eta: Long = -1L,
val localManga: LocalManga? = null, val localManga: LocalManga? = null,
val downloadedChapters: LongArray = LongArray(0), val downloadedChapters: LongArray = LongArray(0),
val scheduledChapters: LongArray = LongArray(0),
val timestamp: Long = System.currentTimeMillis(), val timestamp: Long = System.currentTimeMillis(),
) { ) {
@@ -42,6 +43,7 @@ data class DownloadState(
.putLong(DATA_TIMESTAMP, timestamp) .putLong(DATA_TIMESTAMP, timestamp)
.putString(DATA_ERROR, error) .putString(DATA_ERROR, error)
.putLongArray(DATA_CHAPTERS, downloadedChapters) .putLongArray(DATA_CHAPTERS, downloadedChapters)
.putLongArray(DATA_CHAPTERS_SRC, scheduledChapters)
.putBoolean(DATA_INDETERMINATE, isIndeterminate) .putBoolean(DATA_INDETERMINATE, isIndeterminate)
.putBoolean(DATA_PAUSED, isPaused) .putBoolean(DATA_PAUSED, isPaused)
.build() .build()
@@ -64,10 +66,13 @@ data class DownloadState(
if (eta != other.eta) return false if (eta != other.eta) return false
if (localManga != other.localManga) return false if (localManga != other.localManga) return false
if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false
if (!scheduledChapters.contentEquals(other.scheduledChapters)) return false
if (timestamp != other.timestamp) return false if (timestamp != other.timestamp) return false
if (max != other.max) return false if (max != other.max) return false
if (progress != other.progress) return false if (progress != other.progress) return false
return percent == other.percent if (percent != other.percent) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@@ -83,6 +88,7 @@ data class DownloadState(
result = 31 * result + eta.hashCode() result = 31 * result + eta.hashCode()
result = 31 * result + (localManga?.hashCode() ?: 0) result = 31 * result + (localManga?.hashCode() ?: 0)
result = 31 * result + downloadedChapters.contentHashCode() result = 31 * result + downloadedChapters.contentHashCode()
result = 31 * result + scheduledChapters.contentHashCode()
result = 31 * result + timestamp.hashCode() result = 31 * result + timestamp.hashCode()
result = 31 * result + max result = 31 * result + max
result = 31 * result + progress result = 31 * result + progress
@@ -90,12 +96,14 @@ data class DownloadState(
return result return result
} }
companion object { companion object {
private const val DATA_MANGA_ID = "manga_id" private const val DATA_MANGA_ID = "manga_id"
private const val DATA_MAX = "max" private const val DATA_MAX = "max"
private const val DATA_PROGRESS = "progress" private const val DATA_PROGRESS = "progress"
private const val DATA_CHAPTERS = "chapter" private const val DATA_CHAPTERS = "chapter"
private const val DATA_CHAPTERS_SRC = "chapters_src"
private const val DATA_ETA = "eta" private const val DATA_ETA = "eta"
private const val DATA_TIMESTAMP = "timestamp" private const val DATA_TIMESTAMP = "timestamp"
private const val DATA_ERROR = "error" private const val DATA_ERROR = "error"
@@ -119,5 +127,7 @@ data class DownloadState(
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L)) fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0) fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
fun getScheduledChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS_SRC) ?: LongArray(0)
} }
} }

View File

@@ -1,18 +1,29 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import android.transition.TransitionManager
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.list.chapters.downloadChapterAD
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
@@ -25,6 +36,9 @@ fun downloadItemAD(
) { ) {
val percentPattern = context.resources.getString(R.string.percent_string_pattern) val percentPattern = context.resources.getString(R.string.percent_string_pattern)
val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse)
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
val clickListener = object : View.OnClickListener, View.OnLongClickListener { val clickListener = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
@@ -45,8 +59,13 @@ fun downloadItemAD(
binding.buttonResume.setOnClickListener(clickListener) binding.buttonResume.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener) itemView.setOnClickListener(clickListener)
itemView.setOnLongClickListener(clickListener) itemView.setOnLongClickListener(clickListener)
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
binding.recyclerViewChapters.adapter = chaptersAdapter
bind { payloads -> bind { payloads ->
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
TransitionManager.beginDelayedTransition(binding.constraintLayout)
}
binding.textViewTitle.text = item.manga.title binding.textViewTitle.text = item.manga.title
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply { binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
@@ -57,6 +76,10 @@ fun downloadItemAD(
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
binding.textViewTitle.isChecked = item.isExpanded
binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
binding.cardDetails.isVisible = item.isExpanded
chaptersAdapter.items = item.chapters
when (item.workState) { when (item.workState) {
WorkInfo.State.ENQUEUED, WorkInfo.State.ENQUEUED,
WorkInfo.State.BLOCKED -> { WorkInfo.State.BLOCKED -> {

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.download.ui.list
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.work.WorkInfo import androidx.work.WorkInfo
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import java.util.Date import java.util.Date
@@ -19,6 +21,8 @@ data class DownloadItemModel(
val progress: Int, val progress: Int,
val eta: Long, val eta: Long,
val timestamp: Date, val timestamp: Date,
val chapters: List<DownloadChapter>,
val isExpanded: Boolean,
) : ListModel, Comparable<DownloadItemModel> { ) : ListModel, Comparable<DownloadItemModel> {
val percent: Float val percent: Float
@@ -33,6 +37,9 @@ data class DownloadItemModel(
val canResume: Boolean val canResume: Boolean
get() = workState == WorkInfo.State.RUNNING && isPaused get() = workState == WorkInfo.State.RUNNING && isPaused
val isExpandable: Boolean
get() = chapters.isNotEmpty()
fun getEtaString(): CharSequence? = if (hasEta) { fun getEtaString(): CharSequence? = if (hasEta) {
DateUtils.getRelativeTimeSpanString( DateUtils.getRelativeTimeSpanString(
eta, eta,
@@ -51,17 +58,10 @@ data class DownloadItemModel(
return other is DownloadItemModel && other.id == id return other is DownloadItemModel && other.id == id
} }
override fun getChangePayload(previousState: ListModel): Any? { override fun getChangePayload(previousState: ListModel): Any? = when {
return when (previousState) { previousState !is DownloadItemModel -> super.getChangePayload(previousState)
is DownloadItemModel -> { workState != previousState.workState -> null
if (workState == previousState.workState) { isExpanded != previousState.isExpanded -> ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED
Unit else -> ListModelDiffCallback.PAYLOAD_ANYTHING_CHANGED
} else {
null
}
}
else -> super.getChangePayload(previousState)
}
} }
} }

View File

@@ -82,7 +82,11 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
if (selectionController.onItemClick(item.id.mostSignificantBits)) { if (selectionController.onItemClick(item.id.mostSignificantBits)) {
return return
} }
startActivity(DetailsActivity.newIntent(view.context, item.manga)) if (item.isExpandable) {
viewModel.expandCollapse(item)
} else {
startActivity(DetailsActivity.newIntent(view.context, item.manga))
}
} }
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean { override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {

View File

@@ -8,15 +8,19 @@ import androidx.work.Data
import androidx.work.WorkInfo import androidx.work.WorkInfo
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
@@ -24,6 +28,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
@@ -31,6 +36,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import java.util.Date import java.util.Date
import java.util.LinkedList import java.util.LinkedList
import java.util.UUID import java.util.UUID
@@ -41,13 +47,18 @@ import javax.inject.Inject
class DownloadsViewModel @Inject constructor( class DownloadsViewModel @Inject constructor(
private val workScheduler: DownloadWorker.Scheduler, private val workScheduler: DownloadWorker.Scheduler,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : BaseViewModel() { ) : BaseViewModel() {
private val mangaCache = LongSparseArray<Manga>() private val mangaCache = LongSparseArray<Manga>()
private val cacheMutex = Mutex() private val cacheMutex = Mutex()
private val works = workScheduler.observeWorks() private val expanded = MutableStateFlow(emptySet<UUID>())
.mapLatest { it.toDownloadsList() } private val works = combine(
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) workScheduler.observeWorks(),
expanded,
) { list, exp ->
list.toDownloadsList(exp)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
@@ -169,11 +180,21 @@ class DownloadsViewModel @Inject constructor(
it.id.mostSignificantBits it.id.mostSignificantBits
} ?: emptySet() } ?: emptySet()
private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> { fun expandCollapse(item: DownloadItemModel) {
expanded.update {
if (item.id in it) {
it - item.id
} else {
it + item.id
}
}
}
private suspend fun List<WorkInfo>.toDownloadsList(exp: Set<UUID>): List<DownloadItemModel> {
if (isEmpty()) { if (isEmpty()) {
return emptyList() return emptyList()
} }
val list = mapNotNullTo(ArrayList(size)) { it.toUiModel() } val list = mapNotNullTo(ArrayList(size)) { it.toUiModel(it.id in exp) }
list.sortByDescending { it.timestamp } list.sortByDescending { it.timestamp }
return list return list
} }
@@ -213,11 +234,13 @@ class DownloadsViewModel @Inject constructor(
return destination return destination
} }
private suspend fun WorkInfo.toUiModel(): DownloadItemModel? { private suspend fun WorkInfo.toUiModel(isExpanded: Boolean): DownloadItemModel? {
val workData = if (outputData == Data.EMPTY) progress else outputData val workData = if (outputData == Data.EMPTY) progress else outputData
val mangaId = DownloadState.getMangaId(workData) val mangaId = DownloadState.getMangaId(workData)
if (mangaId == 0L) return null if (mangaId == 0L) return null
val manga = getManga(mangaId) ?: return null val manga = getManga(mangaId) ?: return null
val downloadedChapters = DownloadState.getDownloadedChapters(workData)
val scheduledChapters = DownloadState.getScheduledChapters(workData).toSet()
return DownloadItemModel( return DownloadItemModel(
id = id, id = id,
workState = state, workState = state,
@@ -229,7 +252,19 @@ class DownloadsViewModel @Inject constructor(
progress = DownloadState.getProgress(workData), progress = DownloadState.getProgress(workData),
eta = DownloadState.getEta(workData), eta = DownloadState.getEta(workData),
timestamp = DownloadState.getTimestamp(workData), timestamp = DownloadState.getTimestamp(workData),
totalChapters = DownloadState.getDownloadedChapters(workData).size, totalChapters = downloadedChapters.size,
isExpanded = isExpanded,
chapters = manga.chapters?.mapNotNull {
if (it.id in scheduledChapters) {
DownloadChapter(
number = it.number,
name = it.name,
isDownloaded = it.id in downloadedChapters,
)
} else {
null
}
}.orEmpty(),
) )
} }
@@ -261,8 +296,16 @@ class DownloadsViewModel @Inject constructor(
} }
return cacheMutex.withLock { return cacheMutex.withLock {
mangaCache.getOrElse(mangaId) { mangaCache.getOrElse(mangaId) {
mangaDataRepository.findMangaById(mangaId)?.also { mangaCache[mangaId] = it } ?: return null mangaDataRepository.findMangaById(mangaId)?.let {
tryLoad(it) ?: it
}?.also {
mangaCache[mangaId] = it
} ?: return null
} }
} }
} }
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).peekDetails(manga)
}.getOrNull()
} }

View File

@@ -0,0 +1,14 @@
package org.koitharu.kotatsu.download.ui.list.chapters
import org.koitharu.kotatsu.list.ui.model.ListModel
data class DownloadChapter(
val number: Int,
val name: String,
val isDownloaded: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DownloadChapter && other.name == name
}
}

View File

@@ -0,0 +1,20 @@
package org.koitharu.kotatsu.download.ui.list.chapters
import androidx.core.content.ContextCompat
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.databinding.ItemChapterDownloadBinding
fun downloadChapterAD() = adapterDelegateViewBinding<DownloadChapter, DownloadChapter, ItemChapterDownloadBinding>(
{ layoutInflater, parent -> ItemChapterDownloadBinding.inflate(layoutInflater, parent, false) },
) {
val iconDone = ContextCompat.getDrawable(context, R.drawable.ic_check)
bind {
binding.textViewNumber.text = item.number.toString()
binding.textViewTitle.text = item.name
binding.textViewTitle.drawableEnd = if (item.isDownloaded) iconDone else null
}
}

View File

@@ -178,6 +178,9 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
val chapters = getChapters(mangaDetails, includedIds) val chapters = getChapters(mangaDetails, includedIds)
publishState(
currentState.copy(scheduledChapters = LongArray(chapters.size) { i -> chapters[i].id }),
)
for ((chapterIndex, chapter) in chapters.withIndex()) { for ((chapterIndex, chapter) in chapters.withIndex()) {
if (chaptersToSkip.remove(chapter.id)) { if (chaptersToSkip.remove(chapter.id)) {
publishState( publishState(

View File

@@ -30,7 +30,7 @@ class MangaSourcesRepository @Inject constructor(
) { ) {
private val dao: MangaSourcesDao private val dao: MangaSourcesDao
get() = db.sourcesDao get() = db.getSourcesDao()
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply { private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
remove(MangaSource.LOCAL) remove(MangaSource.LOCAL)

View File

@@ -31,27 +31,27 @@ class FavouritesRepository @Inject constructor(
) { ) {
suspend fun getAllManga(): List<Manga> { suspend fun getAllManga(): List<Manga> {
val entities = db.favouritesDao.findAll() val entities = db.getFavouritesDao().findAll()
return entities.toMangaList() return entities.toMangaList()
} }
suspend fun getLastManga(limit: Int): List<Manga> { suspend fun getLastManga(limit: Int): List<Manga> {
val entities = db.favouritesDao.findLast(limit) val entities = db.getFavouritesDao().findLast(limit)
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(order: ListSortOrder): Flow<List<Manga>> { fun observeAll(order: ListSortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(order) return db.getFavouritesDao().observeAll(order)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
suspend fun getManga(categoryId: Long): List<Manga> { suspend fun getManga(categoryId: Long): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId) val entities = db.getFavouritesDao().findAll(categoryId)
return entities.toMangaList() return entities.toMangaList()
} }
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> { fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<Manga>> {
return db.favouritesDao.observeAll(categoryId, order) return db.getFavouritesDao().observeAll(categoryId, order)
.mapItems { it.toManga() } .mapItems { it.toManga() }
} }
@@ -61,25 +61,25 @@ class FavouritesRepository @Inject constructor(
} }
fun observeCategories(): Flow<List<FavouriteCategory>> { fun observeCategories(): Flow<List<FavouriteCategory>> {
return db.favouriteCategoriesDao.observeAll().mapItems { return db.getFavouriteCategoriesDao().observeAll().mapItems {
it.toFavouriteCategory() it.toFavouriteCategory()
}.distinctUntilChanged() }.distinctUntilChanged()
} }
fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> { fun observeCategoriesForLibrary(): Flow<List<FavouriteCategory>> {
return db.favouriteCategoriesDao.observeAllForLibrary().mapItems { return db.getFavouriteCategoriesDao().observeAllForLibrary().mapItems {
it.toFavouriteCategory() it.toFavouriteCategory()
}.distinctUntilChanged() }.distinctUntilChanged()
} }
fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> { fun observeCategoriesWithCovers(): Flow<Map<FavouriteCategory, List<Cover>>> {
return db.favouriteCategoriesDao.observeAll() return db.getFavouriteCategoriesDao().observeAll()
.map { .map {
db.withTransaction { db.withTransaction {
val res = LinkedHashMap<FavouriteCategory, List<Cover>>() val res = LinkedHashMap<FavouriteCategory, List<Cover>>()
for (entity in it) { for (entity in it) {
val cat = entity.toFavouriteCategory() val cat = entity.toFavouriteCategory()
res[cat] = db.favouritesDao.findCovers( res[cat] = db.getFavouritesDao().findCovers(
categoryId = cat.id, categoryId = cat.id,
order = cat.order, order = cat.order,
) )
@@ -90,16 +90,16 @@ class FavouritesRepository @Inject constructor(
} }
fun observeCategory(id: Long): Flow<FavouriteCategory?> { fun observeCategory(id: Long): Flow<FavouriteCategory?> {
return db.favouriteCategoriesDao.observe(id) return db.getFavouriteCategoriesDao().observe(id)
.map { it?.toFavouriteCategory() } .map { it?.toFavouriteCategory() }
} }
fun observeCategoriesIds(mangaId: Long): Flow<Set<Long>> { fun observeCategoriesIds(mangaId: Long): Flow<Set<Long>> {
return db.favouritesDao.observeIds(mangaId).map { it.toSet() } return db.getFavouritesDao().observeIds(mangaId).map { it.toSet() }
} }
suspend fun getCategory(id: Long): FavouriteCategory { suspend fun getCategory(id: Long): FavouriteCategory {
return db.favouriteCategoriesDao.find(id.toInt()).toFavouriteCategory() return db.getFavouriteCategoriesDao().find(id.toInt()).toFavouriteCategory()
} }
suspend fun createCategory( suspend fun createCategory(
@@ -111,14 +111,14 @@ class FavouritesRepository @Inject constructor(
val entity = FavouriteCategoryEntity( val entity = FavouriteCategoryEntity(
title = title, title = title,
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
sortKey = db.favouriteCategoriesDao.getNextSortKey(), sortKey = db.getFavouriteCategoriesDao().getNextSortKey(),
categoryId = 0, categoryId = 0,
order = sortOrder.name, order = sortOrder.name,
track = isTrackerEnabled, track = isTrackerEnabled,
deletedAt = 0L, deletedAt = 0L,
isVisibleInLibrary = isVisibleOnShelf, isVisibleInLibrary = isVisibleOnShelf,
) )
val id = db.favouriteCategoriesDao.insert(entity) val id = db.getFavouriteCategoriesDao().insert(entity)
val category = entity.toFavouriteCategory(id) val category = entity.toFavouriteCategory(id)
channels.createChannel(category) channels.createChannel(category)
return category return category
@@ -131,22 +131,22 @@ class FavouritesRepository @Inject constructor(
isTrackerEnabled: Boolean, isTrackerEnabled: Boolean,
isVisibleOnShelf: Boolean, isVisibleOnShelf: Boolean,
) { ) {
db.favouriteCategoriesDao.update(id, title, sortOrder.name, isTrackerEnabled, isVisibleOnShelf) db.getFavouriteCategoriesDao().update(id, title, sortOrder.name, isTrackerEnabled, isVisibleOnShelf)
} }
suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) { suspend fun updateCategory(id: Long, isVisibleInLibrary: Boolean) {
db.favouriteCategoriesDao.updateLibVisibility(id, isVisibleInLibrary) db.getFavouriteCategoriesDao().updateLibVisibility(id, isVisibleInLibrary)
} }
suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) { suspend fun updateCategoryTracking(id: Long, isTrackingEnabled: Boolean) {
db.favouriteCategoriesDao.updateTracking(id, isTrackingEnabled) db.getFavouriteCategoriesDao().updateTracking(id, isTrackingEnabled)
} }
suspend fun removeCategories(ids: Collection<Long>) { suspend fun removeCategories(ids: Collection<Long>) {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.deleteAll(id) db.getFavouritesDao().deleteAll(id)
db.favouriteCategoriesDao.delete(id) db.getFavouriteCategoriesDao().delete(id)
} }
} }
// run after transaction success // run after transaction success
@@ -156,11 +156,11 @@ class FavouritesRepository @Inject constructor(
} }
suspend fun setCategoryOrder(id: Long, order: ListSortOrder) { suspend fun setCategoryOrder(id: Long, order: ListSortOrder) {
db.favouriteCategoriesDao.updateOrder(id, order.name) db.getFavouriteCategoriesDao().updateOrder(id, order.name)
} }
suspend fun reorderCategories(orderedIds: List<Long>) { suspend fun reorderCategories(orderedIds: List<Long>) {
val dao = db.favouriteCategoriesDao val dao = db.getFavouriteCategoriesDao()
db.withTransaction { db.withTransaction {
for ((i, id) in orderedIds.withIndex()) { for ((i, id) in orderedIds.withIndex()) {
dao.updateSortKey(id, i) dao.updateSortKey(id, i)
@@ -172,8 +172,8 @@ class FavouritesRepository @Inject constructor(
db.withTransaction { db.withTransaction {
for (manga in mangas) { for (manga in mangas) {
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags) db.getMangaDao().upsert(manga.toEntity(), tags)
val entity = FavouriteEntity( val entity = FavouriteEntity(
mangaId = manga.id, mangaId = manga.id,
categoryId = categoryId, categoryId = categoryId,
@@ -181,7 +181,7 @@ class FavouritesRepository @Inject constructor(
sortKey = 0, sortKey = 0,
deletedAt = 0L, deletedAt = 0L,
) )
db.favouritesDao.insert(entity) db.getFavouritesDao().insert(entity)
} }
} }
} }
@@ -189,7 +189,7 @@ class FavouritesRepository @Inject constructor(
suspend fun removeFromFavourites(ids: Collection<Long>): ReversibleHandle { suspend fun removeFromFavourites(ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.delete(mangaId = id) db.getFavouritesDao().delete(mangaId = id)
} }
} }
return ReversibleHandle { recoverToFavourites(ids) } return ReversibleHandle { recoverToFavourites(ids) }
@@ -198,14 +198,14 @@ class FavouritesRepository @Inject constructor(
suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>): ReversibleHandle { suspend fun removeFromCategory(categoryId: Long, ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.delete(categoryId = categoryId, mangaId = id) db.getFavouritesDao().delete(categoryId = categoryId, mangaId = id)
} }
} }
return ReversibleHandle { recoverToCategory(categoryId, ids) } return ReversibleHandle { recoverToCategory(categoryId, ids) }
} }
private fun observeOrder(categoryId: Long): Flow<ListSortOrder> { private fun observeOrder(categoryId: Long): Flow<ListSortOrder> {
return db.favouriteCategoriesDao.observe(categoryId) return db.getFavouriteCategoriesDao().observe(categoryId)
.filterNotNull() .filterNotNull()
.map { x -> ListSortOrder(x.order, ListSortOrder.NEWEST) } .map { x -> ListSortOrder(x.order, ListSortOrder.NEWEST) }
.distinctUntilChanged() .distinctUntilChanged()
@@ -214,7 +214,7 @@ class FavouritesRepository @Inject constructor(
private suspend fun recoverToFavourites(ids: Collection<Long>) { private suspend fun recoverToFavourites(ids: Collection<Long>) {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.recover(mangaId = id) db.getFavouritesDao().recover(mangaId = id)
} }
} }
} }
@@ -222,7 +222,7 @@ class FavouritesRepository @Inject constructor(
private suspend fun recoverToCategory(categoryId: Long, ids: Collection<Long>) { private suspend fun recoverToCategory(categoryId: Long, ids: Collection<Long>) {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.favouritesDao.recover(mangaId = id, categoryId = categoryId) db.getFavouritesDao().recover(mangaId = id, categoryId = categoryId)
} }
} }
} }

View File

@@ -23,7 +23,7 @@ abstract class HistoryDao {
@Transaction @Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 AND manga_id IN (:ids)") @Query("SELECT * FROM history WHERE deleted_at = 0 AND manga_id IN (:ids)")
abstract suspend fun findAll(ids: Collection<Long>): List<HistoryEntity?> abstract suspend fun findAll(ids: Collection<Long>): List<HistoryEntity>
@Transaction @Transaction
@Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC") @Query("SELECT * FROM history WHERE deleted_at = 0 ORDER BY updated_at DESC")

View File

@@ -38,36 +38,36 @@ class HistoryRepository @Inject constructor(
) { ) {
suspend fun getList(offset: Int, limit: Int): List<Manga> { suspend fun getList(offset: Int, limit: Int): List<Manga> {
val entities = db.historyDao.findAll(offset, limit) val entities = db.getHistoryDao().findAll(offset, limit)
return entities.map { it.manga.toManga(it.tags.toMangaTags()) } return entities.map { it.manga.toManga(it.tags.toMangaTags()) }
} }
suspend fun getLastOrNull(): Manga? { suspend fun getLastOrNull(): Manga? {
val entity = db.historyDao.findAll(0, 1).firstOrNull() ?: return null val entity = db.getHistoryDao().findAll(0, 1).firstOrNull() ?: return null
return entity.manga.toManga(entity.tags.toMangaTags()) return entity.manga.toManga(entity.tags.toMangaTags())
} }
fun observeLast(): Flow<Manga?> { fun observeLast(): Flow<Manga?> {
return db.historyDao.observeAll(1).map { return db.getHistoryDao().observeAll(1).map {
val first = it.firstOrNull() val first = it.firstOrNull()
first?.manga?.toManga(first.tags.toMangaTags()) first?.manga?.toManga(first.tags.toMangaTags())
} }
} }
fun observeAll(): Flow<List<Manga>> { fun observeAll(): Flow<List<Manga>> {
return db.historyDao.observeAll().mapItems { return db.getHistoryDao().observeAll().mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
fun observeAll(limit: Int): Flow<List<Manga>> { fun observeAll(limit: Int): Flow<List<Manga>> {
return db.historyDao.observeAll(limit).mapItems { return db.getHistoryDao().observeAll(limit).mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
fun observeAllWithHistory(order: ListSortOrder): Flow<List<MangaWithHistory>> { fun observeAllWithHistory(order: ListSortOrder): Flow<List<MangaWithHistory>> {
return db.historyDao.observeAll(order).mapItems { return db.getHistoryDao().observeAll(order).mapItems {
MangaWithHistory( MangaWithHistory(
it.manga.toManga(it.tags.toMangaTags()), it.manga.toManga(it.tags.toMangaTags()),
it.history.toMangaHistory(), it.history.toMangaHistory(),
@@ -76,13 +76,13 @@ class HistoryRepository @Inject constructor(
} }
fun observeOne(id: Long): Flow<MangaHistory?> { fun observeOne(id: Long): Flow<MangaHistory?> {
return db.historyDao.observe(id).map { return db.getHistoryDao().observe(id).map {
it?.toMangaHistory() it?.toMangaHistory()
} }
} }
fun observeHasItems(): Flow<Boolean> { fun observeHasItems(): Flow<Boolean> {
return db.historyDao.observeCount() return db.getHistoryDao().observeCount()
.map { it > 0 } .map { it > 0 }
.distinctUntilChanged() .distinctUntilChanged()
} }
@@ -93,12 +93,12 @@ class HistoryRepository @Inject constructor(
} }
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
db.withTransaction { db.withTransaction {
val existing = db.mangaDao.find(manga.id)?.manga val existing = db.getMangaDao().find(manga.id)?.manga
if (existing == null || existing.source == manga.source.name) { if (existing == null || existing.source == manga.source.name) {
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags) db.getMangaDao().upsert(manga.toEntity(), tags)
} }
db.historyDao.upsert( db.getHistoryDao().upsert(
HistoryEntity( HistoryEntity(
mangaId = manga.id, mangaId = manga.id,
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
@@ -119,29 +119,29 @@ class HistoryRepository @Inject constructor(
} }
suspend fun getOne(manga: Manga): MangaHistory? { suspend fun getOne(manga: Manga): MangaHistory? {
return db.historyDao.find(manga.id)?.recoverIfNeeded(manga)?.toMangaHistory() return db.getHistoryDao().find(manga.id)?.recoverIfNeeded(manga)?.toMangaHistory()
} }
suspend fun getProgress(mangaId: Long): Float { suspend fun getProgress(mangaId: Long): Float {
return db.historyDao.findProgress(mangaId) ?: PROGRESS_NONE return db.getHistoryDao().findProgress(mangaId) ?: PROGRESS_NONE
} }
suspend fun clear() { suspend fun clear() {
db.historyDao.clear() db.getHistoryDao().clear()
} }
suspend fun delete(manga: Manga) { suspend fun delete(manga: Manga) {
db.historyDao.delete(manga.id) db.getHistoryDao().delete(manga.id)
} }
suspend fun deleteAfter(minDate: Long) { suspend fun deleteAfter(minDate: Long) {
db.historyDao.deleteAfter(minDate) db.getHistoryDao().deleteAfter(minDate)
} }
suspend fun delete(ids: Collection<Long>): ReversibleHandle { suspend fun delete(ids: Collection<Long>): ReversibleHandle {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.historyDao.delete(id) db.getHistoryDao().delete(id)
} }
} }
return ReversibleHandle { return ReversibleHandle {
@@ -154,13 +154,13 @@ class HistoryRepository @Inject constructor(
* Useful for replacing saved manga on deleting it with remote source * Useful for replacing saved manga on deleting it with remote source
*/ */
suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) { suspend fun deleteOrSwap(manga: Manga, alternative: Manga?) {
if (alternative == null || db.mangaDao.update(alternative.toEntity()) <= 0) { if (alternative == null || db.getMangaDao().update(alternative.toEntity()) <= 0) {
db.historyDao.delete(manga.id) db.getHistoryDao().delete(manga.id)
} }
} }
suspend fun getPopularTags(limit: Int): List<MangaTag> { suspend fun getPopularTags(limit: Int): List<MangaTag> {
return db.historyDao.findPopularTags(limit).map { x -> x.toMangaTag() } return db.getHistoryDao().findPopularTags(limit).map { x -> x.toMangaTag() }
} }
fun shouldSkip(manga: Manga): Boolean { fun shouldSkip(manga: Manga): Boolean {
@@ -178,7 +178,7 @@ class HistoryRepository @Inject constructor(
private suspend fun recover(ids: Collection<Long>) { private suspend fun recover(ids: Collection<Long>) {
db.withTransaction { db.withTransaction {
for (id in ids) { for (id in ids) {
db.historyDao.recover(id) db.getHistoryDao().recover(id)
} }
} }
} }
@@ -192,7 +192,7 @@ class HistoryRepository @Inject constructor(
(chapters.size * percent).toInt(), (chapters.size * percent).toInt(),
)?.id ?: return this )?.id ?: return this
val newEntity = copy(chapterId = newChapterId) val newEntity = copy(chapterId = newChapterId)
db.historyDao.update(newEntity) db.getHistoryDao().update(newEntity)
return newEntity return newEntity
} }
} }

View File

@@ -26,4 +26,5 @@ enum class ListItemType {
CATEGORY_LARGE, CATEGORY_LARGE,
MANGA_SCROBBLING, MANGA_SCROBBLING,
NAV_ITEM, NAV_ITEM,
CHAPTER,
} }

View File

@@ -59,6 +59,7 @@ class TypedListSpacingDecoration(
ListItemType.MANGA_NESTED_GROUP, ListItemType.MANGA_NESTED_GROUP,
ListItemType.CATEGORY_LARGE, ListItemType.CATEGORY_LARGE,
ListItemType.NAV_ITEM, ListItemType.NAV_ITEM,
ListItemType.CHAPTER,
null, null,
-> outRect.set(0) -> outRect.set(0)
@@ -77,6 +78,6 @@ class TypedListSpacingDecoration(
private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing) private fun Rect.set(spacing: Int) = set(spacing, spacing, spacing, spacing)
private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP private fun ListItemType?.isEdgeToEdge() = this == ListItemType.MANGA_NESTED_GROUP
|| this == ListItemType.FILTER_SORT || this == ListItemType.FILTER_SORT
|| this == ListItemType.FILTER_TAG || this == ListItemType.FILTER_TAG
} }

View File

@@ -40,13 +40,15 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
val mangaUri = root.toUri().toString() val mangaUri = root.toUri().toString()
val chapterFiles = getChaptersFiles() val chapterFiles = getChaptersFiles()
val info = index?.getMangaInfo() val info = index?.getMangaInfo()
val cover = fileUri(
root,
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
)
val manga = info?.copy2( val manga = info?.copy2(
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
url = mangaUri, url = mangaUri,
coverUrl = fileUri( coverUrl = cover,
root, largeCoverUrl = cover,
index.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
),
chapters = info.chapters?.mapIndexed { i, c -> chapters = info.chapters?.mapIndexed { i, c ->
c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL) c.copy(url = chapterFiles[i].toUri().toString(), source = MangaSource.LOCAL)
}, },

View File

@@ -67,10 +67,11 @@ sealed class LocalMangaInput(
@JvmStatic @JvmStatic
protected fun Manga.copy2( protected fun Manga.copy2(
url: String = this.url, url: String,
coverUrl: String = this.coverUrl, coverUrl: String,
chapters: List<MangaChapter>? = this.chapters, largeCoverUrl: String,
source: MangaSource = this.source, chapters: List<MangaChapter>?,
source: MangaSource,
) = Manga( ) = Manga(
id = id, id = id,
title = title, title = title,
@@ -91,8 +92,8 @@ sealed class LocalMangaInput(
@JvmStatic @JvmStatic
protected fun MangaChapter.copy( protected fun MangaChapter.copy(
url: String = this.url, url: String,
source: MangaSource = this.source, source: MangaSource,
) = MangaChapter( ) = MangaChapter(
id = id, id = id,
name = name, name = name,

View File

@@ -41,14 +41,15 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
val index = entry?.let(zip::readText)?.let(::MangaIndex) val index = entry?.let(zip::readText)?.let(::MangaIndex)
val info = index?.getMangaInfo() val info = index?.getMangaInfo()
if (info != null) { if (info != null) {
val cover = zipUri(
root,
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
)
return@use info.copy2( return@use info.copy2(
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
url = fileUri, url = fileUri,
coverUrl = zipUri( coverUrl = cover,
root, largeCoverUrl = cover,
entryName = index.getCoverEntry()
?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
),
chapters = info.chapters?.map { c -> chapters = info.chapters?.map { c ->
c.copy(url = fileUri, source = MangaSource.LOCAL) c.copy(url = fileUri, source = MangaSource.LOCAL)
}, },

View File

@@ -24,6 +24,7 @@ import coil.request.ImageRequest
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ErrorReporterReceiver
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
@@ -91,6 +92,7 @@ class ImportWorker @AssistedInject constructor(
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setDefaults(0) .setDefaults(0)
.setSilent(true) .setSilent(true)
.setAutoCancel(true)
result.onSuccess { manga -> result.onSuccess { manga ->
notification.setLargeIcon( notification.setLargeIcon(
coil.execute( coil.execute(
@@ -110,10 +112,9 @@ class ImportWorker @AssistedInject constructor(
PendingIntent.FLAG_UPDATE_CURRENT, PendingIntent.FLAG_UPDATE_CURRENT,
false, false,
), ),
).setAutoCancel(true) ).setVisibility(
.setVisibility( if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC,
if (manga.isNsfw) NotificationCompat.VISIBILITY_SECRET else NotificationCompat.VISIBILITY_PUBLIC, )
)
notification.setContentTitle(applicationContext.getString(R.string.import_completed)) notification.setContentTitle(applicationContext.getString(R.string.import_completed))
.setContentText(applicationContext.getString(R.string.import_completed_hint)) .setContentText(applicationContext.getString(R.string.import_completed_hint))
.setSmallIcon(R.drawable.ic_stat_done) .setSmallIcon(R.drawable.ic_stat_done)
@@ -123,6 +124,11 @@ class ImportWorker @AssistedInject constructor(
notification.setContentTitle(applicationContext.getString(R.string.error_occurred)) notification.setContentTitle(applicationContext.getString(R.string.error_occurred))
.setContentText(error.getDisplayMessage(applicationContext.resources)) .setContentText(error.getDisplayMessage(applicationContext.resources))
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.addAction(
R.drawable.ic_alert_outline,
applicationContext.getString(R.string.report),
ErrorReporterReceiver.getPendingIntent(applicationContext, error),
)
} }
return notification.build() return notification.build()
} }

View File

@@ -16,6 +16,7 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.distinctById import org.koitharu.kotatsu.core.model.distinctById
@@ -95,7 +96,6 @@ open class RemoteListViewModel @Inject constructor(
.onEach { filterState -> .onEach { filterState ->
loadingJob?.cancelAndJoin() loadingJob?.cancelAndJoin()
mangaList.value = null mangaList.value = null
hasNextPage.value = false
loadList(filterState, false) loadList(filterState, false)
}.catch { error -> }.catch { error ->
listError.value = error listError.value = error
@@ -134,12 +134,16 @@ open class RemoteListViewModel @Inject constructor(
sortOrder = filterState.sortOrder, sortOrder = filterState.sortOrder,
tags = filterState.tags, tags = filterState.tags,
) )
if (!append) { mangaList.update { oldList ->
mangaList.value = list if (!append || oldList.isNullOrEmpty()) {
} else if (list.isNotEmpty()) { list
mangaList.value = mangaList.value?.plus(list) ?: list } else {
oldList + list
}
}
if (append) {
hasNextPage.value = list.isNotEmpty()
} }
hasNextPage.value = list.isNotEmpty() // TODO check if new ids added
} catch (e: CancellationException) { } catch (e: CancellationException) {
throw e throw e
} catch (e: Throwable) { } catch (e: Throwable) {

View File

@@ -106,7 +106,7 @@ class AniListRepository @Inject constructor(
} }
override suspend fun unregister(mangaId: Long) { override suspend fun unregister(mangaId: Long) {
return db.scrobblingDao.delete(ScrobblerService.ANILIST.id, mangaId) return db.getScrobblingDao().delete(ScrobblerService.ANILIST.id, mangaId)
} }
override fun logout() { override fun logout() {
@@ -223,7 +223,7 @@ class AniListRepository @Inject constructor(
comment = json.getString("notes"), comment = json.getString("notes"),
rating = scoreFormat.normalize(json.getDouble("score").toFloat()), rating = scoreFormat.normalize(json.getDouble("score").toFloat()),
) )
db.scrobblingDao.upsert(entity) db.getScrobblingDao().upsert(entity)
} }
private fun ScrobblerManga(json: JSONObject): ScrobblerManga { private fun ScrobblerManga(json: JSONObject): ScrobblerManga {

View File

@@ -29,7 +29,7 @@ class AniListScrobbler @Inject constructor(
status: ScrobblingStatus?, status: ScrobblingStatus?,
comment: String?, comment: String?,
) { ) {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) val entity = db.getScrobblingDao().find(scrobblerService.id, mangaId)
requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" } requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" }
repository.updateRate( repository.updateRate(
rateId = entity.id, rateId = entity.id,

View File

@@ -67,24 +67,24 @@ abstract class Scrobbler(
} }
suspend fun scrobble(mangaId: Long, chapter: MangaChapter) { suspend fun scrobble(mangaId: Long, chapter: MangaChapter) {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return val entity = db.getScrobblingDao().find(scrobblerService.id, mangaId) ?: return
repository.updateRate(entity.id, entity.mangaId, chapter) repository.updateRate(entity.id, entity.mangaId, chapter)
} }
suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? { suspend fun getScrobblingInfoOrNull(mangaId: Long): ScrobblingInfo? {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) ?: return null val entity = db.getScrobblingDao().find(scrobblerService.id, mangaId) ?: return null
return entity.toScrobblingInfo() return entity.toScrobblingInfo()
} }
abstract suspend fun updateScrobblingInfo(mangaId: Long, rating: Float, status: ScrobblingStatus?, comment: String?) abstract suspend fun updateScrobblingInfo(mangaId: Long, rating: Float, status: ScrobblingStatus?, comment: String?)
fun observeScrobblingInfo(mangaId: Long): Flow<ScrobblingInfo?> { fun observeScrobblingInfo(mangaId: Long): Flow<ScrobblingInfo?> {
return db.scrobblingDao.observe(scrobblerService.id, mangaId) return db.getScrobblingDao().observe(scrobblerService.id, mangaId)
.map { it?.toScrobblingInfo() } .map { it?.toScrobblingInfo() }
} }
fun observeAllScrobblingInfo(): Flow<List<ScrobblingInfo>> { fun observeAllScrobblingInfo(): Flow<List<ScrobblingInfo>> {
return db.scrobblingDao.observe(scrobblerService.id) return db.getScrobblingDao().observe(scrobblerService.id)
.map { entities -> .map { entities ->
coroutineScope { coroutineScope {
entities.map { entities.map {

View File

@@ -10,6 +10,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -18,6 +19,7 @@ import org.koitharu.kotatsu.core.parser.MangaIntent
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.require import org.koitharu.kotatsu.core.util.ext.require
import org.koitharu.kotatsu.core.util.ext.requireValue import org.koitharu.kotatsu.core.util.ext.requireValue
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
@@ -27,7 +29,6 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler import org.koitharu.kotatsu.scrobbling.common.domain.Scrobbler
import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga import org.koitharu.kotatsu.scrobbling.common.domain.model.ScrobblerManga
import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint import org.koitharu.kotatsu.scrobbling.common.ui.selector.model.ScrobblerHint
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -53,7 +54,7 @@ class ScrobblingSelectorViewModel @Inject constructor(
get() = availableScrobblers[selectedScrobblerIndex.requireValue()] get() = availableScrobblers[selectedScrobblerIndex.requireValue()]
val content: StateFlow<List<ListModel>> = combine( val content: StateFlow<List<ListModel>> = combine(
scrobblerMangaList, scrobblerMangaList.map { it.distinctBy { x -> x.id } },
listError, listError,
hasNextPage, hasNextPage,
) { list, error, isHasNextPage -> ) { list, error, isHasNextPage ->

View File

@@ -85,7 +85,7 @@ class MALRepository @Inject constructor(
} }
override suspend fun unregister(mangaId: Long) { override suspend fun unregister(mangaId: Long) {
return db.scrobblingDao.delete(ScrobblerService.MAL.id, mangaId) return db.getScrobblingDao().delete(ScrobblerService.MAL.id, mangaId)
} }
override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> { override suspend fun findManga(query: String, offset: Int): List<ScrobblerManga> {
@@ -98,6 +98,7 @@ class MALRepository @Inject constructor(
.build() .build()
val request = Request.Builder().url(url).get().build() val request = Request.Builder().url(url).get().build()
val response = okHttp.newCall(request).await().parseJson() val response = okHttp.newCall(request).await().parseJson()
check(response.has("data")) { "Invalid response: \"$response\"" }
val data = response.getJSONArray("data") val data = response.getJSONArray("data")
return data.mapJSONNotNull { jsonToManga(it) } return data.mapJSONNotNull { jsonToManga(it) }
} }
@@ -175,7 +176,7 @@ class MALRepository @Inject constructor(
comment = json.getString("comments"), comment = json.getString("comments"),
rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f), rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f),
) )
db.scrobblingDao.upsert(entity) db.getScrobblingDao().upsert(entity)
} }
override fun logout() { override fun logout() {

View File

@@ -30,7 +30,7 @@ class MALScrobbler @Inject constructor(
status: ScrobblingStatus?, status: ScrobblingStatus?,
comment: String?, comment: String?,
) { ) {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) val entity = db.getScrobblingDao().find(scrobblerService.id, mangaId)
requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" } requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" }
repository.updateRate( repository.updateRate(
rateId = entity.id, rateId = entity.id,

View File

@@ -84,7 +84,7 @@ class ShikimoriRepository @Inject constructor(
} }
override suspend fun unregister(mangaId: Long) { override suspend fun unregister(mangaId: Long) {
return db.scrobblingDao.delete(ScrobblerService.SHIKIMORI.id, mangaId) return db.getScrobblingDao().delete(ScrobblerService.SHIKIMORI.id, mangaId)
} }
override fun logout() { override fun logout() {
@@ -192,7 +192,7 @@ class ShikimoriRepository @Inject constructor(
comment = json.getString("text"), comment = json.getString("text"),
rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f), rating = (json.getDouble("score").toFloat() / 10f).coerceIn(0f, 1f),
) )
db.scrobblingDao.upsert(entity) db.getScrobblingDao().upsert(entity)
} }
private fun ScrobblerManga(json: JSONObject) = ScrobblerManga( private fun ScrobblerManga(json: JSONObject) = ScrobblerManga(

View File

@@ -31,7 +31,7 @@ class ShikimoriScrobbler @Inject constructor(
status: ScrobblingStatus?, status: ScrobblingStatus?,
comment: String?, comment: String?,
) { ) {
val entity = db.scrobblingDao.find(scrobblerService.id, mangaId) val entity = db.getScrobblingDao().find(scrobblerService.id, mangaId)
requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" } requireNotNull(entity) { "Scrobbling info for manga $mangaId not found" }
repository.updateRate( repository.updateRate(
rateId = entity.id, rateId = entity.id,

View File

@@ -40,9 +40,9 @@ class MangaSearchRepository @Inject constructor(
} }
val skipNsfw = settings.isNsfwContentDisabled val skipNsfw = settings.isNsfwContentDisabled
return if (source != null) { return if (source != null) {
db.mangaDao.searchByTitle("%$query%", source.name, limit) db.getMangaDao().searchByTitle("%$query%", source.name, limit)
} else { } else {
db.mangaDao.searchByTitle("%$query%", limit) db.getMangaDao().searchByTitle("%$query%", limit)
}.let { }.let {
if (skipNsfw) it.filterNot { x -> x.manga.isNsfw } else it if (skipNsfw) it.filterNot { x -> x.manga.isNsfw } else it
} }
@@ -83,7 +83,7 @@ class MangaSearchRepository @Inject constructor(
if (query.isEmpty()) { if (query.isEmpty()) {
return emptyList() return emptyList()
} }
val titles = db.suggestionDao.getTitles("$query%") val titles = db.getSuggestionDao().getTitles("$query%")
if (titles.isEmpty()) { if (titles.isEmpty()) {
return emptyList() return emptyList()
} }
@@ -92,19 +92,20 @@ class MangaSearchRepository @Inject constructor(
suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> { suspend fun getTagsSuggestion(query: String, limit: Int, source: MangaSource?): List<MangaTag> {
return when { return when {
query.isNotEmpty() && source != null -> db.tagsDao.findTags(source.name, "%$query%", limit) query.isNotEmpty() && source != null -> db.getTagsDao()
query.isNotEmpty() -> db.tagsDao.findTags("%$query%", limit) .findTags(source.name, "%$query%", limit)
source != null -> db.tagsDao.findPopularTags(source.name, limit) query.isNotEmpty() -> db.getTagsDao().findTags("%$query%", limit)
else -> db.tagsDao.findPopularTags(limit) source != null -> db.getTagsDao().findPopularTags(source.name, limit)
else -> db.getTagsDao().findPopularTags(limit)
}.toMangaTagsList() }.toMangaTagsList()
} }
suspend fun getTagsSuggestion(tags: Set<MangaTag>): List<MangaTag> { suspend fun getTagsSuggestion(tags: Set<MangaTag>): List<MangaTag> {
val ids = tags.mapToSet { it.toEntity().id } val ids = tags.mapToSet { it.toEntity().id }
return if (ids.size == 1) { return if (ids.size == 1) {
db.tagsDao.findRelatedTags(ids.first()) db.getTagsDao().findRelatedTags(ids.first())
} else { } else {
db.tagsDao.findRelatedTags(ids) db.getTagsDao().findRelatedTags(ids)
}.mapNotNull { x -> }.mapNotNull { x ->
if (x.id in ids) null else x.toMangaTag() if (x.id in ids) null else x.toMangaTag()
} }

View File

@@ -12,7 +12,7 @@ fun searchSuggestionQueryHintAD(
{ inflater, parent -> ItemSearchSuggestionQueryHintBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemSearchSuggestionQueryHintBinding.inflate(inflater, parent, false) },
) { ) {
val viewClickListener = View.OnClickListener { v -> val viewClickListener = View.OnClickListener { _ ->
listener.onQueryClick(item.query, true) listener.onQueryClick(item.query, true)
} }

View File

@@ -11,7 +11,7 @@ class TagsAutoCompleteProvider @Inject constructor(
if (query.isEmpty()) { if (query.isEmpty()) {
return emptyList() return emptyList()
} }
val tags = db.tagsDao.findTags(query = "$query%", limit = 6) val tags = db.getTagsDao().findTags(query = "$query%", limit = 6)
val set = HashSet<String>() val set = HashSet<String>()
val result = ArrayList<String>(tags.size) val result = ArrayList<String>(tags.size)
for (tag in tags) { for (tag in tags) {

View File

@@ -17,39 +17,39 @@ class SuggestionRepository @Inject constructor(
) { ) {
fun observeAll(): Flow<List<Manga>> { fun observeAll(): Flow<List<Manga>> {
return db.suggestionDao.observeAll().mapItems { return db.getSuggestionDao().observeAll().mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
fun observeAll(limit: Int): Flow<List<Manga>> { fun observeAll(limit: Int): Flow<List<Manga>> {
return db.suggestionDao.observeAll(limit).mapItems { return db.getSuggestionDao().observeAll(limit).mapItems {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
suspend fun getRandom(): Manga? { suspend fun getRandom(): Manga? {
return db.suggestionDao.getRandom()?.let { return db.getSuggestionDao().getRandom()?.let {
it.manga.toManga(it.tags.toMangaTags()) it.manga.toManga(it.tags.toMangaTags())
} }
} }
suspend fun clear() { suspend fun clear() {
db.suggestionDao.deleteAll() db.getSuggestionDao().deleteAll()
} }
suspend fun isEmpty(): Boolean { suspend fun isEmpty(): Boolean {
return db.suggestionDao.count() == 0 return db.getSuggestionDao().count() == 0
} }
suspend fun replace(suggestions: Iterable<MangaSuggestion>) { suspend fun replace(suggestions: Iterable<MangaSuggestion>) {
db.withTransaction { db.withTransaction {
db.suggestionDao.deleteAll() db.getSuggestionDao().deleteAll()
suggestions.forEach { (manga, relevance) -> suggestions.forEach { (manga, relevance) ->
val tags = manga.tags.toEntities() val tags = manga.tags.toEntities()
db.tagsDao.upsert(tags) db.getTagsDao().upsert(tags)
db.mangaDao.upsert(manga.toEntity(), tags) db.getMangaDao().upsert(manga.toEntity(), tags)
db.suggestionDao.upsert( db.getSuggestionDao().upsert(
SuggestionEntity( SuggestionEntity(
mangaId = manga.id, mangaId = manga.id,
relevance = relevance, relevance = relevance,

View File

@@ -29,8 +29,7 @@ class SuggestionsActivity :
if (fm.findFragmentById(R.id.container) == null) { if (fm.findFragmentById(R.id.container) == null) {
fm.commit { fm.commit {
setReorderingAllowed(true) setReorderingAllowed(true)
val fragment = SuggestionsFragment.newInstance() replace(R.id.container, SuggestionsFragment::class.java, null)
replace(R.id.container, fragment)
} }
} }
} }

View File

@@ -120,11 +120,11 @@ class SyncController @Inject constructor(
private suspend fun MangaDatabase.gc(favourites: Boolean, history: Boolean) = withTransaction { private suspend fun MangaDatabase.gc(favourites: Boolean, history: Boolean) = withTransaction {
val deletedAt = System.currentTimeMillis() - defaultGcPeriod val deletedAt = System.currentTimeMillis() - defaultGcPeriod
if (history) { if (history) {
historyDao.gc(deletedAt) getHistoryDao().gc(deletedAt)
} }
if (favourites) { if (favourites) {
favouritesDao.gc(deletedAt) getFavouritesDao().gc(deletedAt)
favouriteCategoriesDao.gc(deletedAt) getFavouriteCategoriesDao().gc(deletedAt)
} }
} }

View File

@@ -20,11 +20,11 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
class TrackEntity( class TrackEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "manga_id") val mangaId: Long,
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR) @get:Deprecated(message = "Should not be used", level = DeprecationLevel.WARNING)
@ColumnInfo(name = "chapters_total") val totalChapters: Int, @ColumnInfo(name = "chapters_total") val totalChapters: Int,
@ColumnInfo(name = "last_chapter_id") val lastChapterId: Long, @ColumnInfo(name = "last_chapter_id") val lastChapterId: Long,
@ColumnInfo(name = "chapters_new") val newChapters: Int, @ColumnInfo(name = "chapters_new") val newChapters: Int,
@ColumnInfo(name = "last_check") val lastCheck: Long, @ColumnInfo(name = "last_check") val lastCheck: Long,
@get:Deprecated(message = "Should not be used", level = DeprecationLevel.ERROR) @get:Deprecated(message = "Should not be used", level = DeprecationLevel.WARNING)
@ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long @ColumnInfo(name = "last_notified_id") val lastNotifiedChapterId: Long
) )

View File

@@ -1,7 +1,7 @@
package org.koitharu.kotatsu.tracker.data package org.koitharu.kotatsu.tracker.data
import androidx.room.Dao import androidx.room.Dao
import androidx.room.MapInfo import androidx.room.MapColumn
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import androidx.room.Upsert import androidx.room.Upsert
@@ -23,9 +23,8 @@ abstract class TracksDao {
@Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId") @Query("SELECT chapters_new FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun findNewChapters(mangaId: Long): Int? abstract suspend fun findNewChapters(mangaId: Long): Int?
@MapInfo(keyColumn = "manga_id", valueColumn = "chapters_new")
@Query("SELECT manga_id, chapters_new FROM tracks") @Query("SELECT manga_id, chapters_new FROM tracks")
abstract fun observeNewChaptersMap(): Flow<Map<Long, Int>> abstract fun observeNewChaptersMap(): Flow<Map<@MapColumn(columnName = "manga_id") Long, @MapColumn(columnName = "chapters_new") Int>>
@Query("SELECT chapters_new FROM tracks") @Query("SELECT chapters_new FROM tracks")
abstract fun observeNewChapters(): Flow<List<Int>> abstract fun observeNewChapters(): Flow<List<Int>>

View File

@@ -41,23 +41,23 @@ class TrackingRepository @Inject constructor(
private var isGcCalled = AtomicBoolean(false) private var isGcCalled = AtomicBoolean(false)
suspend fun getNewChaptersCount(mangaId: Long): Int { suspend fun getNewChaptersCount(mangaId: Long): Int {
return db.tracksDao.findNewChapters(mangaId) ?: 0 return db.getTracksDao().findNewChapters(mangaId) ?: 0
} }
fun observeNewChaptersCount(mangaId: Long): Flow<Int> { fun observeNewChaptersCount(mangaId: Long): Flow<Int> {
return db.tracksDao.observeNewChapters(mangaId).map { it ?: 0 } return db.getTracksDao().observeNewChapters(mangaId).map { it ?: 0 }
} }
fun observeUpdatedMangaCount(): Flow<Int> { fun observeUpdatedMangaCount(): Flow<Int> {
return db.tracksDao.observeNewChapters().map { list -> list.count { it > 0 } } return db.getTracksDao().observeNewChapters().map { list -> list.count { it > 0 } }
.onStart { gcIfNotCalled() } .onStart { gcIfNotCalled() }
} }
fun observeUpdatedManga(limit: Int = 0): Flow<List<Manga>> { fun observeUpdatedManga(limit: Int = 0): Flow<List<Manga>> {
return if (limit == 0) { return if (limit == 0) {
db.tracksDao.observeUpdatedManga() db.getTracksDao().observeUpdatedManga()
} else { } else {
db.tracksDao.observeUpdatedManga(limit) db.getTracksDao().observeUpdatedManga(limit)
}.mapItems { it.toManga() } }.mapItems { it.toManga() }
.distinctUntilChanged() .distinctUntilChanged()
.onStart { gcIfNotCalled() } .onStart { gcIfNotCalled() }
@@ -65,7 +65,7 @@ class TrackingRepository @Inject constructor(
suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> { suspend fun getTracks(mangaList: Collection<Manga>): List<MangaTracking> {
val ids = mangaList.mapToSet { it.id } val ids = mangaList.mapToSet { it.id }
val tracks = db.tracksDao.findAll(ids).groupBy { it.mangaId } val tracks = db.getTracksDao().findAll(ids).groupBy { it.mangaId }
val idSet = HashSet<Long>() val idSet = HashSet<Long>()
val result = ArrayList<MangaTracking>(mangaList.size) val result = ArrayList<MangaTracking>(mangaList.size)
for (item in mangaList) { for (item in mangaList) {
@@ -89,7 +89,7 @@ class TrackingRepository @Inject constructor(
@VisibleForTesting @VisibleForTesting
suspend fun getTrack(manga: Manga): MangaTracking { suspend fun getTrack(manga: Manga): MangaTracking {
val track = db.tracksDao.find(manga.id) val track = db.getTracksDao().find(manga.id)
return MangaTracking( return MangaTracking(
manga = manga, manga = manga,
lastChapterId = track?.lastChapterId ?: NO_ID, lastChapterId = track?.lastChapterId ?: NO_ID,
@@ -99,14 +99,14 @@ class TrackingRepository @Inject constructor(
@VisibleForTesting @VisibleForTesting
suspend fun deleteTrack(mangaId: Long) { suspend fun deleteTrack(mangaId: Long) {
db.tracksDao.delete(mangaId) db.getTracksDao().delete(mangaId)
} }
fun observeTrackingLog(limit: Flow<Int>): Flow<List<TrackingLogItem>> { fun observeTrackingLog(limit: Flow<Int>): Flow<List<TrackingLogItem>> {
return limit.flatMapLatest { limitValue -> return limit.flatMapLatest { limitValue ->
combine( combine(
db.tracksDao.observeNewChaptersMap(), db.getTracksDao().observeNewChaptersMap(),
db.trackLogsDao.observeAll(limitValue), db.getTrackLogsDao().observeAll(limitValue),
) { counters, entities -> ) { counters, entities ->
val countersMap = counters.toMutableMap() val countersMap = counters.toMutableMap()
entities.map { x -> x.toTrackingLogItem(countersMap) } entities.map { x -> x.toTrackingLogItem(countersMap) }
@@ -116,21 +116,21 @@ class TrackingRepository @Inject constructor(
} }
} }
suspend fun getLogsCount() = db.trackLogsDao.count() suspend fun getLogsCount() = db.getTrackLogsDao().count()
suspend fun clearLogs() = db.trackLogsDao.clear() suspend fun clearLogs() = db.getTrackLogsDao().clear()
suspend fun clearCounters() = db.tracksDao.clearCounters() suspend fun clearCounters() = db.getTracksDao().clearCounters()
suspend fun gc() { suspend fun gc() {
db.tracksDao.gc() db.getTracksDao().gc()
db.trackLogsDao.gc() db.getTrackLogsDao().gc()
} }
suspend fun saveUpdates(updates: MangaUpdates.Success) { suspend fun saveUpdates(updates: MangaUpdates.Success) {
db.withTransaction { db.withTransaction {
val track = getOrCreateTrack(updates.manga.id).mergeWith(updates) val track = getOrCreateTrack(updates.manga.id).mergeWith(updates)
db.tracksDao.upsert(track) db.getTracksDao().upsert(track)
if (updates.isValid && updates.newChapters.isNotEmpty()) { if (updates.isValid && updates.newChapters.isNotEmpty()) {
updatePercent(updates) updatePercent(updates)
val logEntity = TrackLogEntity( val logEntity = TrackLogEntity(
@@ -138,7 +138,7 @@ class TrackingRepository @Inject constructor(
chapters = updates.newChapters.joinToString("\n") { x -> x.name }, chapters = updates.newChapters.joinToString("\n") { x -> x.name },
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
) )
db.trackLogsDao.insert(logEntity) db.getTrackLogsDao().insert(logEntity)
} }
} }
} }
@@ -146,10 +146,10 @@ class TrackingRepository @Inject constructor(
suspend fun clearUpdates(ids: Collection<Long>) { suspend fun clearUpdates(ids: Collection<Long>) {
when { when {
ids.isEmpty() -> return ids.isEmpty() -> return
ids.size == 1 -> db.tracksDao.clearCounter(ids.single()) ids.size == 1 -> db.getTracksDao().clearCounter(ids.single())
else -> db.withTransaction { else -> db.withTransaction {
for (id in ids) { for (id in ids) {
db.tracksDao.clearCounter(id) db.getTracksDao().clearCounter(id)
} }
} }
} }
@@ -174,11 +174,11 @@ class TrackingRepository @Inject constructor(
lastCheck = System.currentTimeMillis(), lastCheck = System.currentTimeMillis(),
lastNotifiedChapterId = lastChapterId, lastNotifiedChapterId = lastChapterId,
) )
db.tracksDao.upsert(entity) db.getTracksDao().upsert(entity)
} }
suspend fun getCategoriesCount(): IntArray { suspend fun getCategoriesCount(): IntArray {
val categories = db.favouriteCategoriesDao.findAll() val categories = db.getFavouriteCategoriesDao().findAll()
return intArrayOf( return intArrayOf(
categories.count { it.track }, categories.count { it.track },
categories.size, categories.size,
@@ -186,19 +186,19 @@ class TrackingRepository @Inject constructor(
} }
suspend fun getAllFavouritesManga(): Map<FavouriteCategory, List<Manga>> { suspend fun getAllFavouritesManga(): Map<FavouriteCategory, List<Manga>> {
val categories = db.favouriteCategoriesDao.findAll() val categories = db.getFavouriteCategoriesDao().findAll()
return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity -> return categories.associateTo(LinkedHashMap(categories.size)) { categoryEntity ->
categoryEntity.toFavouriteCategory() to categoryEntity.toFavouriteCategory() to
db.favouritesDao.findAllManga(categoryEntity.categoryId).toMangaList() db.getFavouritesDao().findAllManga(categoryEntity.categoryId).toMangaList()
} }
} }
suspend fun getAllHistoryManga(): List<Manga> { suspend fun getAllHistoryManga(): List<Manga> {
return db.historyDao.findAllManga().toMangaList() return db.getHistoryDao().findAllManga().toMangaList()
} }
private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity { private suspend fun getOrCreateTrack(mangaId: Long): TrackEntity {
return db.tracksDao.find(mangaId) ?: TrackEntity( return db.getTracksDao().find(mangaId) ?: TrackEntity(
mangaId = mangaId, mangaId = mangaId,
totalChapters = 0, totalChapters = 0,
lastChapterId = 0L, lastChapterId = 0L,
@@ -209,7 +209,7 @@ class TrackingRepository @Inject constructor(
} }
private suspend fun updatePercent(updates: MangaUpdates.Success) { private suspend fun updatePercent(updates: MangaUpdates.Success) {
val history = db.historyDao.find(updates.manga.id) ?: return val history = db.getHistoryDao().find(updates.manga.id) ?: return
val chapters = updates.manga.chapters val chapters = updates.manga.chapters
if (chapters.isNullOrEmpty()) { if (chapters.isNullOrEmpty()) {
return return
@@ -220,7 +220,7 @@ class TrackingRepository @Inject constructor(
} }
val position = (chapters.size - updates.newChapters.size) * history.percent val position = (chapters.size - updates.newChapters.size) * history.percent
val newPercent = position / chapters.size.toFloat() val newPercent = position / chapters.size.toFloat()
db.historyDao.update(history.copy(percent = newPercent)) db.getHistoryDao().update(history.copy(percent = newPercent))
} }
private fun TrackEntity.mergeWith(updates: MangaUpdates.Success): TrackEntity { private fun TrackEntity.mergeWith(updates: MangaUpdates.Success): TrackEntity {

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"> <selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.7" android:color="?attr/m3ColorBackground" /> <item android:alpha="0.7" android:color="?attr/m3ColorBottomMenuBackground" />
</selector> </selector>

View File

@@ -7,6 +7,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clipToPadding="false" android:clipToPadding="false"
android:scrollIndicators="top"
android:scrollbars="vertical" android:scrollbars="vertical"
app:layout_behavior="@string/appbar_scrolling_view_behavior"> app:layout_behavior="@string/appbar_scrolling_view_behavior">

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<TextView
android:id="@+id/textView_number"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:background="@drawable/bg_badge_default"
android:ellipsize="none"
android:gravity="center"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?attr/colorOnPrimary"
android:textSize="12sp"
tools:text="13" />
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:drawableTint="?colorControlNormal"
tools:drawableEnd="@drawable/ic_check"
tools:text="@tools:sample/lorem[15]" />
</LinearLayout>

View File

@@ -8,6 +8,7 @@
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/constraintLayout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:paddingBottom="12dp"> android:paddingBottom="12dp">
@@ -24,7 +25,7 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<TextView <CheckedTextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@@ -32,11 +33,14 @@
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
app:drawableTint="?android:colorControlNormal"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:drawableEndCompat="@drawable/ic_expand_collapse"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
@@ -100,6 +104,31 @@
app:layout_constraintTop_toBottomOf="@id/textView_status" app:layout_constraintTop_toBottomOf="@id/textView_status"
tools:text="@tools:sample/lorem[3]" /> tools:text="@tools:sample/lorem[3]" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_details"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_default="wrap"
app:layout_constraintHeight_max="280dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
app:shapeAppearance="?shapeAppearanceCornerMedium">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:scrollbars="vertical"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="200"
tools:listitem="@layout/item_chapter_download" />
</com.google.android.material.card.MaterialCardView>
<Button <Button
android:id="@+id/button_pause" android:id="@+id/button_pause"
style="?materialButtonOutlinedStyle" style="?materialButtonOutlinedStyle"
@@ -110,7 +139,7 @@
android:text="@string/pause" android:text="@string/pause"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_resume" app:layout_constraintEnd_toStartOf="@id/button_resume"
app:layout_constraintTop_toBottomOf="@id/progressBar" app:layout_constraintTop_toBottomOf="@id/card_details"
tools:visibility="visible" /> tools:visibility="visible" />
<Button <Button
@@ -123,7 +152,7 @@
android:text="@string/resume" android:text="@string/resume"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_cancel" app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/progressBar" /> app:layout_constraintTop_toBottomOf="@id/card_details" />
<Button <Button
android:id="@+id/button_cancel" android:id="@+id/button_cancel"
@@ -135,7 +164,7 @@
android:text="@android:string/cancel" android:text="@android:string/cancel"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar" app:layout_constraintTop_toBottomOf="@id/card_details"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -43,6 +43,12 @@
android:title="@string/find_similar" android:title="@string/find_similar"
app:showAsAction="never" /> app:showAsAction="never" />
<item
android:id="@+id/action_online"
android:orderInCategory="50"
android:title="@string/online_variant"
app:showAsAction="never" />
<item <item
android:id="@+id/action_browser" android:id="@+id/action_browser"
android:orderInCategory="50" android:orderInCategory="50"

View File

@@ -1,30 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_import"
android:orderInCategory="50"
android:title="@string/_import"
app:showAsAction="never" />
<item
android:id="@+id/action_categories"
android:orderInCategory="50"
android:title="@string/settings"
app:showAsAction="never" />
<item
android:id="@+id/action_grid_size"
android:orderInCategory="50"
android:title="@string/grid_size"
app:showAsAction="never" />
<item
android:id="@+id/action_clear_history"
android:orderInCategory="50"
android:title="@string/clear_history"
app:showAsAction="never" />
</menu>

View File

@@ -487,10 +487,11 @@
<string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string> <string name="keep_screen_on_summary">Ня выключаць экран падчас чытання мангі</string>
<string name="state_abandoned">Кінута</string> <string name="state_abandoned">Кінута</string>
<string name="categories">Катэгорыі</string> <string name="categories">Катэгорыі</string>
<string name="list_options">Спіс опцый</string> <string name="list_options">Параметры спісу</string>
<string name="suggest_new_sources">Прапаноўваць новыя крыніцы пасля абнаўлення праграмы</string> <string name="suggest_new_sources">Прапаноўваць новыя крыніцы пасля абнаўлення праграмы</string>
<string name="enhanced_colors_summary">Памяншае паласы, але можа паўплываць на прадукцыйнасць</string> <string name="enhanced_colors_summary">Памяншае паласы, але можа паўплываць на прадукцыйнасць</string>
<string name="by_relevance">Актуальнасць</string> <string name="by_relevance">Актуальнасць</string>
<string name="enhanced_colors">32-бітны каляровы рэжым</string> <string name="enhanced_colors">32-бітны каляровы рэжым</string>
<string name="suggest_new_sources_summary">Запыт на ўключэнне зноў дададзеных крыніц пасля абнаўлення праграмы</string> <string name="suggest_new_sources_summary">Прапаноўваць крыніцы мангі, дададзеныя ў апошнім абнаўленні праграмы</string>
<string name="online_variant">Анлайн варыянт</string>
</resources> </resources>

View File

@@ -493,4 +493,5 @@
<string name="list_options">Lista de opciones</string> <string name="list_options">Lista de opciones</string>
<string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string> <string name="enhanced_colors_summary">Reduce el banding, pero puede afectar al rendimiento</string>
<string name="by_relevance">Relevancia</string> <string name="by_relevance">Relevancia</string>
<string name="online_variant">Variante en línea</string>
</resources> </resources>

View File

@@ -493,4 +493,5 @@
<string name="categories">Mga Kategorya</string> <string name="categories">Mga Kategorya</string>
<string name="list_options">Opsyon sa Listahan</string> <string name="list_options">Opsyon sa Listahan</string>
<string name="by_relevance">Kaugnayan</string> <string name="by_relevance">Kaugnayan</string>
<string name="online_variant">Online na baryante</string>
</resources> </resources>

View File

@@ -1,5 +1,12 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_900</item>
<item name="android:windowSplashScreenAnimationDuration">800</item>
</style>
<!-- From ThemeOverlay.Material3.DynamicColors.Dark --> <!-- From ThemeOverlay.Material3.DynamicColors.Dark -->
<style name="Theme.Kotatsu.Monet"> <style name="Theme.Kotatsu.Monet">
<item name="isMaterial3DynamicColorApplied">true</item> <item name="isMaterial3DynamicColorApplied">true</item>

View File

@@ -493,4 +493,5 @@
<string name="by_relevance">Актуальность</string> <string name="by_relevance">Актуальность</string>
<string name="enhanced_colors">32-битный цветовой режим</string> <string name="enhanced_colors">32-битный цветовой режим</string>
<string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string> <string name="suggest_new_sources_summary">Предлагать источники манги, добавленные в последнем обновлении приложения</string>
<string name="online_variant">Онлайн вариант</string>
</resources> </resources>

View File

@@ -490,4 +490,8 @@
<string name="download_option_all_chapters">Çevirisi %s olan tüm bölümler</string> <string name="download_option_all_chapters">Çevirisi %s olan tüm bölümler</string>
<string name="color_black">Siyah</string> <string name="color_black">Siyah</string>
<string name="this_month">Bu ay</string> <string name="this_month">Bu ay</string>
<string name="categories">Kategoriler</string>
<string name="list_options">Seçenekleri listele</string>
<string name="online_variant">Çevrimiçi varyant</string>
<string name="by_relevance">Alaka düzeyi</string>
</resources> </resources>

View File

@@ -58,7 +58,7 @@
<string name="clear_pages_cache">Очистити кеш сторінок</string> <string name="clear_pages_cache">Очистити кеш сторінок</string>
<string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string> <string name="text_file_sizes">Б|кБ|МБ|ГБ|ТБ</string>
<string name="standard">Стандартний</string> <string name="standard">Стандартний</string>
<string name="webtoon">Манхва</string> <string name="webtoon">Вебтун</string>
<string name="read_mode">Режим читання</string> <string name="read_mode">Режим читання</string>
<string name="grid_size">Розмір сітки</string> <string name="grid_size">Розмір сітки</string>
<string name="search_on_s">Пошук по %s</string> <string name="search_on_s">Пошук по %s</string>
@@ -479,18 +479,19 @@
<string name="directories">Каталоги</string> <string name="directories">Каталоги</string>
<string name="main_screen_sections">Розділи головного екрана</string> <string name="main_screen_sections">Розділи головного екрана</string>
<string name="to_top">Вгору</string> <string name="to_top">Вгору</string>
<string name="zoom_in">Збільшити</string> <string name="zoom_in">Наблизити</string>
<string name="reader_zoom_buttons_summary">Чи показувати елементи керування масштабуванням у нижньому правому куті</string> <string name="reader_zoom_buttons_summary">Чи показувати елементи керування масштабуванням у нижньому правому куті</string>
<string name="reader_zoom_buttons">Показати кнопки масштабування</string> <string name="reader_zoom_buttons">Показати кнопки масштабування</string>
<string name="zoom_out">Зменшити</string> <string name="zoom_out">Віддалити</string>
<string name="keep_screen_on">Тримати екран увімкненим</string> <string name="keep_screen_on">Тримати екран увімкненим</string>
<string name="keep_screen_on_summary">Не вимикати екран під час читання манги</string> <string name="keep_screen_on_summary">Не вимикати екран під час читання манги</string>
<string name="state_abandoned">Кинута</string> <string name="state_abandoned">Кинута</string>
<string name="categories">Категорії</string> <string name="categories">Категорії</string>
<string name="list_options">Список опцій</string> <string name="list_options">Параметри списку</string>
<string name="suggest_new_sources">Пропонуйте нові джерела після оновлення застосунка</string> <string name="suggest_new_sources">Пропонувати нові джерела після оновлення застосунку</string>
<string name="enhanced_colors_summary">Зменшує смуги, але може вплинути на продуктивність</string> <string name="enhanced_colors_summary">Зменшує смуги, але може вплинути на продуктивність</string>
<string name="by_relevance">Актуальність</string> <string name="by_relevance">Актуальність</string>
<string name="enhanced_colors">32-бітний колірний режим</string> <string name="enhanced_colors">32-бітний колірний режим</string>
<string name="suggest_new_sources_summary">Запропонувати ввімкнути щойно додані джерела після оновлення застосунка</string> <string name="suggest_new_sources_summary">Пропонує включити нові джерела манґи після оновлення застосунку</string>
<string name="online_variant">Онлайн варіант</string>
</resources> </resources>

View File

@@ -3,6 +3,7 @@
<style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu"> <style name="Base.V31.Kotatsu" parent="Base.V27.Kotatsu">
<item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item> <item name="android:windowSplashScreenAnimatedIcon">@drawable/avd_splash</item>
<item name="android:windowSplashScreenBackground">@android:color/system_neutral2_50</item>
<item name="android:windowSplashScreenAnimationDuration">800</item> <item name="android:windowSplashScreenAnimationDuration">800</item>
</style> </style>

View File

@@ -68,7 +68,7 @@
<string name="downloads">下载</string> <string name="downloads">下载</string>
<string name="by_name">名称</string> <string name="by_name">名称</string>
<string name="popular">热门</string> <string name="popular">热门</string>
<string name="updated">已更新</string> <string name="updated">最近更新</string>
<string name="sort_order">排序</string> <string name="sort_order">排序</string>
<string name="filter">筛选</string> <string name="filter">筛选</string>
<string name="theme">主题</string> <string name="theme">主题</string>
@@ -483,4 +483,9 @@
<string name="zoom_out">缩小</string> <string name="zoom_out">缩小</string>
<string name="keep_screen_on">保持亮屏</string> <string name="keep_screen_on">保持亮屏</string>
<string name="keep_screen_on_summary">阅读漫画时不关闭屏幕</string> <string name="keep_screen_on_summary">阅读漫画时不关闭屏幕</string>
<string name="list_options">列表选项</string>
<string name="suggest_new_sources">推荐新漫画源</string>
<string name="enhanced_colors_summary">减少色带,但可能会影响性能</string>
<string name="enhanced_colors">32位色彩模式</string>
<string name="suggest_new_sources_summary">应用更新后快速启用新增的漫画源</string>
</resources> </resources>

View File

@@ -499,4 +499,5 @@
<string name="list_options">List options</string> <string name="list_options">List options</string>
<string name="by_relevance">Relevance</string> <string name="by_relevance">Relevance</string>
<string name="categories">Categories</string> <string name="categories">Categories</string>
<string name="online_variant">Online variant</string>
</resources> </resources>