Compare commits

...

12 Commits
v0.3.2 ... v0.4

Author SHA1 Message Date
6d193baa69 Increment version 2020-05-21 20:48:01 +03:00
3bd7b54405 Capitalize MangaLib genres 2020-05-21 20:47:10 +03:00
d99450c5a3 Prepopulate favourite categories 2020-05-21 20:43:24 +03:00
fe14ccb5ec Update readme 2020-05-20 20:05:51 +03:00
e38e5fdf0f Small enhancements 2020-05-20 19:51:28 +03:00
c1c2b11bd8 Show local manga size 2020-05-20 19:31:17 +03:00
7d147b3c37 Check wakelock is held in download service 2020-05-20 19:04:31 +03:00
260ff32cd1 Detect chapters in cbz if index missing 2020-05-20 18:59:30 +03:00
ccc5f3e423 Fix webtoon scroll 2020-05-17 17:25:18 +03:00
8b32a60743 Misc fixes 2020-05-16 10:20:55 +03:00
c1faf2fe06 Remember last opened section 2020-05-16 09:07:09 +03:00
3588270742 Show favourites by categories and manager categories order 2020-05-16 08:49:34 +03:00
72 changed files with 740 additions and 253 deletions

View File

@@ -22,6 +22,10 @@ Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/re
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
* Notifications about new chapters * Notifications about new chapters
### Coming Features
* Global search
### Screenshots ### Screenshots
| ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/80315102-3478db00-87fe-11ea-9ce8-4bbd1c254b2b.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/80315110-3f337000-87fe-11ea-95df-944c196b6667.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/80315121-49ee0500-87fe-11ea-8d9b-537a041bbf2f.png) | | ![Screenshot_20200226-210337](https://user-images.githubusercontent.com/8948226/80315102-3478db00-87fe-11ea-9ce8-4bbd1c254b2b.png) | ![Screenshot_20200226-210310](https://user-images.githubusercontent.com/8948226/80315110-3f337000-87fe-11ea-95df-944c196b6667.png) | ![Screenshot_20200226-210232](https://user-images.githubusercontent.com/8948226/80315121-49ee0500-87fe-11ea-8d9b-537a041bbf2f.png) |

View File

@@ -17,7 +17,7 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName '0.3.2' versionName '0.4'
buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\"" buildConfigField 'String', 'GIT_BRANCH', "\"${gitBranch}\""
@@ -60,15 +60,15 @@ androidExtensions {
dependencies { dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar']) implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.5' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.5' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation 'androidx.core:core-ktx:1.3.0-rc01' implementation 'androidx.core:core-ktx:1.3.0-rc01'
implementation 'androidx.appcompat:appcompat:1.2.0-beta01' implementation 'androidx.appcompat:appcompat:1.2.0-rc01'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha04' implementation 'androidx.activity:activity-ktx:1.2.0-alpha04'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha04' implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha04'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha02'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta5' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01' implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
@@ -86,12 +86,12 @@ dependencies {
implementation 'com.github.moxy-community:moxy-ktx:2.1.2' implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2' kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.6.0' implementation 'com.squareup.okhttp3:okhttp:4.7.2'
implementation 'com.squareup.okio:okio:2.6.0' implementation 'com.squareup.okio:okio:2.6.0'
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.1.5' implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.10.1' implementation 'io.coil-kt:coil:0.11.0'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0' implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0' implementation 'com.tomclaw.cache:cache:1.0'

View File

@@ -14,10 +14,12 @@ import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback
import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.migrations.Migration1To2 import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration2To3 import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4 import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
import org.koitharu.kotatsu.core.local.CbzFetcher import org.koitharu.kotatsu.core.local.CbzFetcher
import org.koitharu.kotatsu.core.local.PagesCache import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
@@ -124,5 +126,6 @@ class KotatsuApp : Application() {
applicationContext, applicationContext,
MangaDatabase::class.java, MangaDatabase::class.java,
"kotatsu-db" "kotatsu-db"
).addMigrations(Migration1To2, Migration2To3, Migration3To4) ).addMigrations(Migration1To2, Migration2To3, Migration3To4, Migration4To5)
.addCallback(DatabasePrePopulateCallback(resources))
} }

View File

@@ -0,0 +1,16 @@
package org.koitharu.kotatsu.core.db
import android.content.res.Resources
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import org.koitharu.kotatsu.R
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
db.execSQL(
"INSERT INTO favourite_categories (created_at, sort_key, title) VALUES (?,?,?)",
arrayOf(System.currentTimeMillis(), 1, resources.getString(R.string.read_later))
)
}
}

View File

@@ -9,8 +9,8 @@ import org.koitharu.kotatsu.core.db.entity.FavouriteCategoryEntity
@Dao @Dao
abstract class FavouriteCategoriesDao { abstract class FavouriteCategoriesDao {
@Query("SELECT category_id,title,created_at FROM favourite_categories ORDER BY :orderBy") @Query("SELECT * FROM favourite_categories ORDER BY sort_key")
abstract suspend fun findAll(orderBy: String): List<FavouriteCategoryEntity> abstract suspend fun findAll(): List<FavouriteCategoryEntity>
@Insert(onConflict = OnConflictStrategy.ABORT) @Insert(onConflict = OnConflictStrategy.ABORT)
abstract suspend fun insert(category: FavouriteCategoryEntity): Long abstract suspend fun insert(category: FavouriteCategoryEntity): Long
@@ -20,4 +20,14 @@ abstract class FavouriteCategoriesDao {
@Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id") @Query("UPDATE favourite_categories SET title = :title WHERE category_id = :id")
abstract suspend fun update(id: Long, title: String) abstract suspend fun update(id: Long, title: String)
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
abstract suspend fun update(id: Long, sortKey: Int)
@Query("SELECT MAX(sort_key) FROM favourite_categories")
protected abstract suspend fun getMaxSortKey(): Int?
suspend fun getNextSortKey(): Int {
return (getMaxSortKey() ?: 0) + 1
}
} }

View File

@@ -8,10 +8,18 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Dao @Dao
abstract class FavouritesDao { abstract class FavouritesDao {
@Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(): List<FavouriteManga>
@Transaction @Transaction
@Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
@Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at")
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
@Transaction @Transaction
@Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset") @Query("SELECT * FROM favourites WHERE category_id = :categoryId GROUP BY manga_id ORDER BY created_at LIMIT :limit OFFSET :offset")
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga> abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>

View File

@@ -8,7 +8,7 @@ import org.koitharu.kotatsu.core.db.entity.*
entities = [ entities = [
MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class,
FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class FavouriteCategoryEntity::class, FavouriteEntity::class, MangaPrefsEntity::class, TrackEntity::class
], version = 4 ], version = 5
) )
abstract class MangaDatabase : RoomDatabase() { abstract class MangaDatabase : RoomDatabase() {

View File

@@ -11,12 +11,14 @@ data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "category_id") val categoryId: Int,
@ColumnInfo(name = "created_at") val createdAt: Long, @ColumnInfo(name = "created_at") val createdAt: Long,
@ColumnInfo(name = "sort_key") val sortKey: Int,
@ColumnInfo(name = "title") val title: String @ColumnInfo(name = "title") val title: String
) { ) {
fun toFavouriteCategory(id: Long? = null) = FavouriteCategory( fun toFavouriteCategory(id: Long? = null) = FavouriteCategory(
id = id ?: categoryId.toLong(), id = id ?: categoryId.toLong(),
title = title, title = title,
sortKey = sortKey,
createdAt = Date(createdAt) createdAt = Date(createdAt)
) )
} }

View File

@@ -32,6 +32,6 @@ data class HistoryEntity(
updatedAt = Date(updatedAt), updatedAt = Date(updatedAt),
chapterId = chapterId, chapterId = chapterId,
page = page, page = page,
scroll = scroll scroll = scroll.toInt()
) )
} }

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
object Migration4To5 : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE favourite_categories ADD COLUMN sort_key INTEGER NOT NULL DEFAULT 0")
}
}

View File

@@ -1,5 +1,3 @@
package org.koitharu.kotatsu.core.exceptions package org.koitharu.kotatsu.core.exceptions
import java.lang.NullPointerException
class MangaNotFoundException(s: String? = null) : RuntimeException(s) class MangaNotFoundException(s: String? = null) : RuntimeException(s)

View File

@@ -37,7 +37,7 @@ class SetCookieCache : CookieCache {
override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator() override fun iterator(): MutableIterator<Cookie> = SetCookieCacheIterator()
private inner class SetCookieCacheIterator() : MutableIterator<Cookie> { private inner class SetCookieCacheIterator : MutableIterator<Cookie> {
private val iterator = cookies.iterator() private val iterator = cookies.iterator()

View File

@@ -8,5 +8,6 @@ import java.util.*
data class FavouriteCategory( data class FavouriteCategory(
val id: Long, val id: Long,
val title: String, val title: String,
val sortKey: Int,
val createdAt: Date val createdAt: Date
) : Parcelable ) : Parcelable

View File

@@ -10,5 +10,5 @@ data class MangaHistory(
val updatedAt: Date, val updatedAt: Date,
val chapterId: Long, val chapterId: Long,
val page: Int, val page: Int,
val scroll: Float val scroll: Int
) : Parcelable ) : Parcelable

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.core.parser package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import androidx.core.net.toFile import androidx.core.net.toFile
@@ -41,24 +42,34 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
@Suppress("BlockingMethodInNonBlockingContext") @Suppress("BlockingMethodInNonBlockingContext")
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val file = Uri.parse(chapter.url).toFile() val uri = Uri.parse(chapter.url)
val file = uri.toFile()
val zip = ZipFile(file) val zip = ZipFile(file)
val pattern = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex) val index = zip.getEntry(MangaZip.INDEX_ENTRY)?.let(zip::readText)?.let(::MangaIndex)
?.getChapterNamesPattern(chapter) var entries = zip.entries().asSequence()
val entries = if (pattern != null) { entries = if (index != null) {
zip.entries().asSequence() val pattern = index.getChapterNamesPattern(chapter)
.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) } entries.filter { x -> !x.isDirectory && x.name.substringBefore('.').matches(pattern) }
} else { } else {
zip.entries().asSequence().filter { x -> !x.isDirectory } val parent = uri.fragment.orEmpty()
}.toList().sortedWith(compareBy(AlphanumComparator()) { x -> x.name }) entries.filter { x ->
return entries.map { x -> !x.isDirectory && x.name.substringBeforeLast(
val uri = zipUri(file, x.name) File.separatorChar,
MangaPage( ""
id = uri.longHashCode(), ) == parent
url = uri, }
source = MangaSource.LOCAL
)
} }
return entries
.toList()
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
.map { x ->
val entryUri = zipUri(file, x.name)
MangaPage(
id = entryUri.longHashCode(),
url = entryUri,
source = MangaSource.LOCAL
)
}
} }
@@ -67,43 +78,50 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
return file.delete() return file.delete()
} }
@SuppressLint("DefaultLocale")
fun getFromFile(file: File): Manga { fun getFromFile(file: File): Manga {
val zip = ZipFile(file) val zip = ZipFile(file)
val fileUri = file.toUri().toString() val fileUri = file.toUri().toString()
val entry = zip.getEntry(MangaZip.INDEX_ENTRY) val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex) val index = entry?.let(zip::readText)?.let(::MangaIndex)
return index?.let { val info = index?.getMangaInfo()
it.getMangaInfo()?.let { x -> if (index != null && info != null) {
x.copy( return info.copy(
source = MangaSource.LOCAL,
url = fileUri,
coverUrl = zipUri(
file,
entryName = it.getCoverEntry()
?: findFirstEntry(zip.entries())?.name.orEmpty()
),
chapters = x.chapters?.map { c -> c.copy(url = fileUri) }
)
}
} ?: run {
val title = file.nameWithoutExtension.replace("_", " ").capitalize()
Manga(
id = file.absolutePath.longHashCode(),
title = title,
url = fileUri,
source = MangaSource.LOCAL, source = MangaSource.LOCAL,
coverUrl = zipUri(file, findFirstEntry(zip.entries())?.name.orEmpty()), url = fileUri,
chapters = listOf( coverUrl = zipUri(
MangaChapter( file,
id = file.absolutePath.longHashCode(), entryName = index.getCoverEntry()
url = fileUri, ?: findFirstEntry(zip.entries())?.name.orEmpty()
number = 1, ),
source = MangaSource.LOCAL, chapters = info.chapters?.map { c -> c.copy(url = fileUri) }
name = title
)
)
) )
} }
// fallback
val title = file.nameWithoutExtension.replace("_", " ").capitalize()
val chapters = HashSet<String>()
for (x in zip.entries()) {
if (!x.isDirectory) {
chapters += x.name.substringBeforeLast(File.separatorChar, "")
}
}
val uriBuilder = file.toUri().buildUpon()
return Manga(
id = file.absolutePath.longHashCode(),
title = title,
url = fileUri,
source = MangaSource.LOCAL,
coverUrl = zipUri(file, findFirstEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(AlphanumComparator()).mapIndexed { i, s ->
MangaChapter(
id = "$i$s".longHashCode(),
name = if (s.isEmpty()) title else s,
number = i + 1,
source = MangaSource.LOCAL,
url = uriBuilder.fragment(s).build().toString()
)
}
)
} }
fun getRemoteManga(localManga: Manga): Manga? { fun getRemoteManga(localManga: Manga): Manga? {

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.os.Build import android.os.Build
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import java.util.* import java.util.*

View File

@@ -128,7 +128,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
tags = info.getElementsMatchingOwnText("Жанры")?.firstOrNull() tags = info.getElementsMatchingOwnText("Жанры")?.firstOrNull()
?.nextElementSibling()?.select("a")?.mapNotNull { a -> ?.nextElementSibling()?.select("a")?.mapNotNull { a ->
MangaTag( MangaTag(
title = a.text(), title = a.text().capitalize(),
key = a.attr("href").substringAfterLast('='), key = a.attr("href").substringAfterLast('='),
source = source source = source
) )
@@ -182,7 +182,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
result += MangaTag( result += MangaTag(
source = source, source = source,
key = x.getInt("id").toString(), key = x.getInt("id").toString(),
title = x.getString("name") title = x.getString("name").capitalize()
) )
} }
return result return result

View File

@@ -0,0 +1,6 @@
package org.koitharu.kotatsu.core.prefs
enum class AppSection {
LOCAL, FAVOURITES, HISTORY;
}

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.core.prefs
import android.content.Context import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.content.res.Resources import android.content.res.Resources
import android.os.StatFs
import android.provider.Settings import android.provider.Settings
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit import androidx.core.content.edit
@@ -27,6 +26,12 @@ class AppSettings private constructor(resources: Resources, private val prefs: S
ListMode.DETAILED_LIST ListMode.DETAILED_LIST
) )
var defaultSection by EnumPreferenceDelegate(
AppSection::class.java,
resources.getString(R.string.key_app_section),
AppSection.HISTORY
)
val theme by StringIntPreferenceDelegate( val theme by StringIntPreferenceDelegate(
resources.getString(R.string.key_theme), resources.getString(R.string.key_theme),
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM

View File

@@ -1,13 +1,6 @@
package org.koitharu.kotatsu.core.prefs package org.koitharu.kotatsu.core.prefs
enum class ListMode(val id: Int) { enum class ListMode {
LIST(0), LIST, DETAILED_LIST, GRID;
DETAILED_LIST(1),
GRID(2);
companion object {
fun valueOf(id: Int) = values().firstOrNull { it.id == id }
}
} }

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.domain package org.koitharu.kotatsu.domain
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.net.Uri
import android.util.Size import android.util.Size
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -12,6 +13,7 @@ import org.koitharu.kotatsu.core.prefs.ReaderMode
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.await
import org.koitharu.kotatsu.utils.ext.medianOrNull import org.koitharu.kotatsu.utils.ext.medianOrNull
import java.io.InputStream import java.io.InputStream
import java.util.zip.ZipFile
object MangaUtils : KoinComponent { object MangaUtils : KoinComponent {
@@ -23,13 +25,22 @@ object MangaUtils : KoinComponent {
try { try {
val page = pages.medianOrNull() ?: return null val page = pages.medianOrNull() ?: return null
val url = MangaProviderFactory.create(page.source).getPageFullUrl(page) val url = MangaProviderFactory.create(page.source).getPageFullUrl(page)
val client = get<OkHttpClient>() val uri = Uri.parse(url)
val request = Request.Builder() val size = if (uri.scheme == "cbz") {
.url(url) val zip = ZipFile(uri.schemeSpecificPart)
.get() val entry = zip.getEntry(uri.fragment)
.build() zip.getInputStream(entry).use {
val size = client.newCall(request).await().use { getBitmapSize(it)
getBitmapSize(it.body?.byteStream()) }
} else {
val client = get<OkHttpClient>()
val request = Request.Builder()
.url(url)
.get()
.build()
client.newCall(request).await().use {
getBitmapSize(it.body?.byteStream())
}
} }
return when { return when {
size.width * 2 < size.height -> ReaderMode.WEBTOON size.width * 2 < size.height -> ReaderMode.WEBTOON

View File

@@ -18,18 +18,28 @@ class FavouritesRepository : KoinComponent {
private val db: MangaDatabase by inject() private val db: MangaDatabase by inject()
suspend fun getAllManga(): List<Manga> {
val entities = db.favouritesDao.findAll()
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
}
suspend fun getAllManga(offset: Int): List<Manga> { suspend fun getAllManga(offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(offset, 20) val entities = db.favouritesDao.findAll(offset, 20)
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun getManga(categoryId: Long): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId)
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
}
suspend fun getManga(categoryId: Long, offset: Int): List<Manga> { suspend fun getManga(categoryId: Long, offset: Int): List<Manga> {
val entities = db.favouritesDao.findAll(categoryId, offset, 20) val entities = db.favouritesDao.findAll(categoryId, offset, 20)
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun getAllCategories(): List<FavouriteCategory> { suspend fun getAllCategories(): List<FavouriteCategory> {
val entities = db.favouriteCategoriesDao.findAll("created_at") val entities = db.favouriteCategoriesDao.findAll()
return entities.map { it.toFavouriteCategory() } return entities.map { it.toFavouriteCategory() }
} }
@@ -42,17 +52,32 @@ class FavouritesRepository : KoinComponent {
val entity = FavouriteCategoryEntity( val entity = FavouriteCategoryEntity(
title = title, title = title,
createdAt = System.currentTimeMillis(), createdAt = System.currentTimeMillis(),
sortKey = db.favouriteCategoriesDao.getNextSortKey(),
categoryId = 0 categoryId = 0
) )
val id = db.favouriteCategoriesDao.insert(entity) val id = db.favouriteCategoriesDao.insert(entity)
notifyCategoriesChanged()
return entity.toFavouriteCategory(id) return entity.toFavouriteCategory(id)
} }
suspend fun renameCategory(id: Long, title: String) { suspend fun renameCategory(id: Long, title: String) {
db.favouriteCategoriesDao.update(id, title) db.favouriteCategoriesDao.update(id, title)
notifyCategoriesChanged()
} }
suspend fun removeCategory(id: Long) { suspend fun removeCategory(id: Long) {
db.favouriteCategoriesDao.delete(id) db.favouriteCategoriesDao.delete(id)
notifyCategoriesChanged()
}
suspend fun reorderCategories(orderedIds: List<Long>) {
val dao = db.favouriteCategoriesDao
db.withTransaction {
for ((i, id) in orderedIds.withIndex()) {
dao.update(id, i)
}
}
notifyCategoriesChanged()
} }
suspend fun addToCategory(manga: Manga, categoryId: Long) { suspend fun addToCategory(manga: Manga, categoryId: Long) {
@@ -80,7 +105,7 @@ class FavouritesRepository : KoinComponent {
} }
fun unsubscribe(listener: OnFavouritesChangeListener) { fun unsubscribe(listener: OnFavouritesChangeListener) {
listeners += listener listeners -= listener
} }
private suspend fun notifyFavouritesChanged(mangaId: Long) { private suspend fun notifyFavouritesChanged(mangaId: Long) {
@@ -88,5 +113,11 @@ class FavouritesRepository : KoinComponent {
listeners.forEach { x -> x.onFavouritesChanged(mangaId) } listeners.forEach { x -> x.onFavouritesChanged(mangaId) }
} }
} }
private suspend fun notifyCategoriesChanged() {
withContext(Dispatchers.Main) {
listeners.forEach { x -> x.onCategoriesChanged() }
}
}
} }
} }

View File

@@ -3,4 +3,6 @@ package org.koitharu.kotatsu.domain.favourites
interface OnFavouritesChangeListener { interface OnFavouritesChangeListener {
fun onFavouritesChanged(mangaId: Long) fun onFavouritesChanged(mangaId: Long)
fun onCategoriesChanged()
} }

View File

@@ -24,7 +24,7 @@ class HistoryRepository : KoinComponent {
return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) } return entities.map { it.manga.toManga(it.tags.map(TagEntity::toMangaTag).toSet()) }
} }
suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Float) { suspend fun addOrUpdate(manga: Manga, chapterId: Long, page: Int, scroll: Int) {
val tags = manga.tags.map(TagEntity.Companion::fromMangaTag) val tags = manga.tags.map(TagEntity.Companion::fromMangaTag)
db.withTransaction { db.withTransaction {
db.tagsDao.upsert(tags) db.tagsDao.upsert(tags)
@@ -36,7 +36,7 @@ class HistoryRepository : KoinComponent {
updatedAt = System.currentTimeMillis(), updatedAt = System.currentTimeMillis(),
chapterId = chapterId, chapterId = chapterId,
page = page, page = page,
scroll = scroll scroll = scroll.toFloat() // we migrate to int, but decide to not update database
) )
) )
trackingRepository.upsert(manga) trackingRepository.upsert(manga)
@@ -45,15 +45,7 @@ class HistoryRepository : KoinComponent {
} }
suspend fun getOne(manga: Manga): MangaHistory? { suspend fun getOne(manga: Manga): MangaHistory? {
return db.historyDao.find(manga.id)?.let { return db.historyDao.find(manga.id)?.toMangaHistory()
MangaHistory(
createdAt = Date(it.createdAt),
updatedAt = Date(it.updatedAt),
chapterId = it.chapterId,
page = it.page,
scroll = it.scroll
)
}
} }
suspend fun clear() { suspend fun clear() {
@@ -86,7 +78,7 @@ class HistoryRepository : KoinComponent {
} }
fun unsubscribe(listener: OnHistoryChangeListener) { fun unsubscribe(listener: OnHistoryChangeListener) {
listeners += listener listeners -= listener
} }
private suspend fun notifyHistoryChanged() { private suspend fun notifyHistoryChanged() {

View File

@@ -5,7 +5,6 @@ import android.os.Bundle
import android.view.View import android.view.View
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment
import moxy.MvpAppCompatDialogFragment import moxy.MvpAppCompatDialogFragment
abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() { abstract class AlertDialogFragment(@LayoutRes private val layoutResId: Int) : MvpAppCompatDialogFragment() {

View File

@@ -6,7 +6,7 @@ import androidx.annotation.DrawableRes
import com.google.android.material.chip.Chip import com.google.android.material.chip.Chip
import org.koitharu.kotatsu.utils.ext.getThemeColor import org.koitharu.kotatsu.utils.ext.getThemeColor
class ChipsFactory(private val context: Context) { class ChipsFactory(val context: Context) {
fun create(convertView: Chip? = null, text: CharSequence, @DrawableRes iconRes: Int = 0, fun create(convertView: Chip? = null, text: CharSequence, @DrawableRes iconRes: Int = 0,
tag: Any? = null, onClickListener: View.OnClickListener? = null): Chip { tag: Any? = null, onClickListener: View.OnClickListener? = null): Chip {

View File

@@ -123,6 +123,7 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
manga ?: return false, manga ?: return false,
adapter.checkedItemsIds adapter.checkedItemsIds
) )
mode.finish()
true true
} }
R.id.action_select_all -> { R.id.action_select_all -> {

View File

@@ -104,6 +104,8 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
manga?.source != null && manga?.source != MangaSource.LOCAL manga?.source != null && manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_delete).isVisible = menu.findItem(R.id.action_delete).isVisible =
manga?.source == MangaSource.LOCAL manga?.source == MangaSource.LOCAL
menu.findItem(R.id.action_browser).isVisible =
manga?.source != MangaSource.LOCAL
menu.findItem(R.id.action_shortcut).isVisible = menu.findItem(R.id.action_shortcut).isVisible =
ShortcutManagerCompat.isRequestPinShortcutSupported(this) ShortcutManagerCompat.isRequestPinShortcutSupported(this)
return super.onPrepareOptionsMenu(menu) return super.onPrepareOptionsMenu(menu)
@@ -139,7 +141,16 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
if (chaptersCount > 5) { if (chaptersCount > 5) {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle(R.string.save_manga) .setTitle(R.string.save_manga)
.setMessage(getString(R.string.large_manga_save_confirm, chaptersCount)) .setMessage(
getString(
R.string.large_manga_save_confirm,
resources.getQuantityString(
R.plurals.chapters,
chaptersCount,
chaptersCount
)
)
)
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(R.string.save) { _, _ -> .setPositiveButton(R.string.save) { _, _ ->
DownloadService.start(this, it) DownloadService.start(this, it)
@@ -174,7 +185,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
} }
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) { override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
tab.text = when(position) { tab.text = when (position) {
0 -> getString(R.string.details) 0 -> getString(R.string.details)
1 -> getString(R.string.chapters) 1 -> getString(R.string.chapters)
else -> null else -> null

View File

@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.ui.details
import android.text.Spanned import android.text.Spanned
import android.view.View import android.view.View
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.api.load import coil.api.load
@@ -16,9 +17,11 @@ import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.FavouriteCategoriesDialog import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.FavouriteCategoriesDialog
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.search.MangaSearchSheet import org.koitharu.kotatsu.ui.search.MangaSearchSheet
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.addChips import org.koitharu.kotatsu.utils.ext.addChips
import org.koitharu.kotatsu.utils.ext.showPopupMenu import org.koitharu.kotatsu.utils.ext.showPopupMenu
import org.koitharu.kotatsu.utils.ext.textAndVisible import org.koitharu.kotatsu.utils.ext.textAndVisible
import org.koitharu.kotatsu.utils.ext.toFileOrNull
import kotlin.math.roundToInt import kotlin.math.roundToInt
class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView, class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetailsView,
@@ -67,6 +70,16 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
onClickListener = this@MangaDetailsFragment onClickListener = this@MangaDetailsFragment
) )
} }
manga.url.toUri().toFileOrNull()?.let { f ->
chips_tags.addChips(listOf(f)) {
create(
text = FileSizeUtils.formatBytes(context, it.length()),
iconRes = R.drawable.ic_chip_storage,
tag = it,
onClickListener = this@MangaDetailsFragment
)
}
}
imageView_favourite.setOnClickListener(this) imageView_favourite.setOnClickListener(this)
button_read.setOnClickListener(this) button_read.setOnClickListener(this)
button_read.setOnLongClickListener(this) button_read.setOnLongClickListener(this)
@@ -123,8 +136,8 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
} }
override fun onLongClick(v: View): Boolean { override fun onLongClick(v: View): Boolean {
when { when (v.id) {
v.id == R.id.button_read -> { R.id.button_read -> {
if (history == null) { if (history == null) {
return false return false
} }

View File

@@ -157,6 +157,8 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} }
} }
override fun onCategoriesChanged() = Unit
override fun onDestroy() { override fun onDestroy() {
HistoryRepository.unsubscribe(this) HistoryRepository.unsubscribe(this)
FavouritesRepository.unsubscribe(this) FavouritesRepository.unsubscribe(this)

View File

@@ -76,7 +76,7 @@ class DownloadService : BaseService() {
private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job { private fun downloadManga(manga: Manga, chaptersIds: Set<Long>?, startId: Int): Job {
return launch(Dispatchers.IO) { return launch(Dispatchers.IO) {
mutex.lock() mutex.lock()
wakeLock.acquire(TimeUnit.MINUTES.toMillis(20)) wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.fillFrom(manga) notification.fillFrom(manga)
notification.setCancelId(startId) notification.setCancelId(startId)
@@ -167,7 +167,9 @@ class DownloadService : BaseService() {
notification.dismiss() notification.dismiss()
stopSelf(startId) stopSelf(startId)
} }
wakeLock.release() if (wakeLock.isHeld) {
wakeLock.release()
}
mutex.unlock() mutex.unlock()
} }
} }

View File

@@ -21,10 +21,11 @@ import moxy.ktx.moxyPresenter
import org.koin.core.inject import org.koin.core.inject
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.prefs.AppSection
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesListFragment import org.koitharu.kotatsu.ui.main.list.favourites.FavouritesContainerFragment
import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment import org.koitharu.kotatsu.ui.main.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment import org.koitharu.kotatsu.ui.main.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.main.list.remote.RemoteListFragment
@@ -65,8 +66,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
supportFragmentManager.findFragmentById(R.id.container)?.let { supportFragmentManager.findFragmentById(R.id.container)?.let {
fab.isVisible = it is HistoryListFragment fab.isVisible = it is HistoryListFragment
} ?: run { } ?: run {
navigationView.setCheckedItem(R.id.nav_history) openDefaultSection()
setPrimaryFragment(HistoryListFragment.newInstance())
} }
drawer.postDelayed(2000) { drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext) AppUpdateService.startIfRequired(applicationContext)
@@ -106,9 +106,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
val source = MangaSource.values().getOrNull(item.itemId) ?: return false val source = MangaSource.values().getOrNull(item.itemId) ?: return false
setPrimaryFragment(RemoteListFragment.newInstance(source)) setPrimaryFragment(RemoteListFragment.newInstance(source))
} else when (item.itemId) { } else when (item.itemId) {
R.id.nav_history -> setPrimaryFragment(HistoryListFragment.newInstance()) R.id.nav_history -> {
R.id.nav_favourites -> setPrimaryFragment(FavouritesListFragment.newInstance()) settings.defaultSection = AppSection.HISTORY
R.id.nav_local_storage -> setPrimaryFragment(LocalListFragment.newInstance()) setPrimaryFragment(HistoryListFragment.newInstance())
}
R.id.nav_favourites -> {
settings.defaultSection = AppSection.FAVOURITES
setPrimaryFragment(FavouritesContainerFragment.newInstance())
}
R.id.nav_local_storage -> {
settings.defaultSection = AppSection.LOCAL
setPrimaryFragment(LocalListFragment.newInstance())
}
R.id.nav_action_settings -> { R.id.nav_action_settings -> {
startActivity(SettingsActivity.newIntent(this)) startActivity(SettingsActivity.newIntent(this))
return true return true
@@ -167,6 +176,23 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
} }
private fun openDefaultSection() {
when(settings.defaultSection) {
AppSection.LOCAL -> {
navigationView.setCheckedItem(R.id.nav_local_storage)
setPrimaryFragment(LocalListFragment.newInstance())
}
AppSection.FAVOURITES -> {
navigationView.setCheckedItem(R.id.nav_favourites)
setPrimaryFragment(FavouritesContainerFragment.newInstance())
}
AppSection.HISTORY -> {
navigationView.setCheckedItem(R.id.nav_history)
setPrimaryFragment(HistoryListFragment.newInstance())
}
}
}
private fun setPrimaryFragment(fragment: Fragment) { private fun setPrimaryFragment(fragment: Fragment) {
supportFragmentManager.beginTransaction() supportFragmentManager.beginTransaction()
.replace(R.id.container, fragment) .replace(R.id.container, fragment)

View File

@@ -1,7 +1,6 @@
package org.koitharu.kotatsu.ui.main package org.koitharu.kotatsu.ui.main
import moxy.viewstate.strategy.alias.OneExecution import moxy.viewstate.strategy.alias.OneExecution
import org.koitharu.kotatsu.core.model.MangaState
import org.koitharu.kotatsu.ui.common.BaseMvpView import org.koitharu.kotatsu.ui.common.BaseMvpView
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState

View File

@@ -0,0 +1,88 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.fragment_favourites.*
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.domain.favourites.FavouritesRepository
import org.koitharu.kotatsu.domain.favourites.OnFavouritesChangeListener
import org.koitharu.kotatsu.ui.common.BaseFragment
import org.koitharu.kotatsu.ui.main.list.favourites.categories.CategoriesActivity
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesPresenter
import org.koitharu.kotatsu.ui.main.list.favourites.categories.FavouriteCategoriesView
import java.util.*
import kotlin.collections.ArrayList
class FavouritesContainerFragment : BaseFragment(R.layout.fragment_favourites), FavouriteCategoriesView,
OnFavouritesChangeListener {
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val adapter = FavouritesPagerAdapter(this)
pager.adapter = adapter
TabLayoutMediator(tabs, pager, adapter).attach()
FavouritesRepository.subscribe(this)
}
override fun onDestroyView() {
FavouritesRepository.unsubscribe(this)
super.onDestroyView()
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories
(pager.adapter as? FavouritesPagerAdapter)?.replaceData(data)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_favourites, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_categories -> {
context?.let {
startActivity(CategoriesActivity.newIntent(it))
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun getTitle(): CharSequence? {
return getString(R.string.favourites)
}
override fun onCheckedCategoriesChanged(checkedIds: Set<Int>) = Unit
override fun onError(e: Throwable) {
Snackbar.make(pager, e.message ?: return, Snackbar.LENGTH_LONG).show()
}
override fun onFavouritesChanged(mangaId: Long) = Unit
override fun onCategoriesChanged() {
presenter.loadAllCategories()
}
companion object {
fun newInstance() = FavouritesContainerFragment()
}
}

View File

@@ -1,49 +1,38 @@
package org.koitharu.kotatsu.ui.main.list.favourites package org.koitharu.kotatsu.ui.main.list.favourites
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import kotlinx.android.synthetic.main.fragment_list.* import kotlinx.android.synthetic.main.fragment_list.*
import moxy.ktx.moxyPresenter import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.main.list.MangaListFragment import org.koitharu.kotatsu.ui.main.list.MangaListFragment
import org.koitharu.kotatsu.ui.main.list.MangaListView import org.koitharu.kotatsu.ui.main.list.MangaListView
import org.koitharu.kotatsu.ui.main.list.favourites.categories.CategoriesActivity import org.koitharu.kotatsu.utils.ext.withArgs
class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> { class FavouritesListFragment : MangaListFragment<Unit>(), MangaListView<Unit> {
private val presenter by moxyPresenter(factory = ::FavouritesListPresenter) private val presenter by moxyPresenter(factory = ::FavouritesListPresenter)
private val categoryId: Long
get() = arguments?.getLong(ARG_CATEGORY_ID) ?: 0L
override fun onRequestMoreItems(offset: Int) { override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset) presenter.loadList(categoryId, offset)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.opt_favourites, menu)
super.onCreateOptionsMenu(menu, inflater)
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.action_categories -> {
context?.let {
startActivity(CategoriesActivity.newIntent(it))
}
true
}
else -> super.onOptionsItemSelected(item)
}
override fun getTitle(): CharSequence? {
return getString(R.string.favourites)
} }
override fun setUpEmptyListHolder() { override fun setUpEmptyListHolder() {
textView_holder.setText(R.string.you_have_not_favourites_yet) textView_holder.setText(if (categoryId == 0L) {
R.string.you_have_not_favourites_yet
} else {
R.string.favourites_category_empty
})
textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) textView_holder.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0)
} }
companion object { companion object {
fun newInstance() = FavouritesListFragment() private const val ARG_CATEGORY_ID = "category_id"
fun newInstance(categoryId: Long) = FavouritesListFragment().withArgs(1) {
putLong(ARG_CATEGORY_ID, categoryId)
}
} }
} }

View File

@@ -21,12 +21,16 @@ class FavouritesListPresenter : BasePresenter<MangaListView<Unit>>() {
super.onFirstViewAttach() super.onFirstViewAttach()
} }
fun loadList(offset: Int) { fun loadList(categoryId: Long, offset: Int) {
presenterScope.launch { presenterScope.launch {
viewState.onLoadingStateChanged(true) viewState.onLoadingStateChanged(true)
try { try {
val list = withContext(Dispatchers.IO) { val list = withContext(Dispatchers.IO) {
repository.getAllManga(offset = offset) if (categoryId == 0L) {
repository.getAllManga(offset = offset)
} else {
repository.getManga(categoryId = categoryId, offset = offset)
}
} }
if (offset == 0) { if (offset == 0) {
viewState.onListChanged(list) viewState.onListChanged(list)

View File

@@ -0,0 +1,32 @@
package org.koitharu.kotatsu.ui.main.list.favourites
import androidx.fragment.app.Fragment
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.AdapterUpdater
import org.koitharu.kotatsu.utils.ext.replaceWith
class FavouritesPagerAdapter(fragment: Fragment) : FragmentStateAdapter(fragment), TabLayoutMediator.TabConfigurationStrategy {
private val dataSet = ArrayList<FavouriteCategory>()
override fun getItemCount() = dataSet.size
override fun createFragment(position: Int): Fragment {
val item = dataSet[position]
return FavouritesListFragment.newInstance(item.id)
}
override fun onConfigureTab(tab: TabLayout.Tab, position: Int) {
val item = dataSet[position]
tab.text = item.title
}
fun replaceData(data: List<FavouriteCategory>) {
val updater = AdapterUpdater(dataSet, data, FavouriteCategory::id)
dataSet.replaceWith(data)
updater(this)
}
}

View File

@@ -10,6 +10,7 @@ import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_categories.* import kotlinx.android.synthetic.main.activity_categories.*
@@ -28,6 +29,7 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter) private val presenter by moxyPresenter(factory = ::FavouriteCategoriesPresenter)
private lateinit var adapter: CategoriesAdapter private lateinit var adapter: CategoriesAdapter
private lateinit var reorderHelper: ItemTouchHelper
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -38,6 +40,8 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) recyclerView.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL))
recyclerView.adapter = adapter recyclerView.adapter = adapter
fab_add.setOnClickListener(this) fab_add.setOnClickListener(this)
reorderHelper = ItemTouchHelper(ReorderHelperCallback())
reorderHelper.attachToRecyclerView(recyclerView)
} }
override fun onClick(v: View) { override fun onClick(v: View) {
@@ -56,6 +60,11 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
} }
} }
override fun onItemLongClick(item: FavouriteCategory, position: Int, view: View): Boolean {
reorderHelper.startDrag(recyclerView.findViewHolderForAdapterPosition(position) ?: return false)
return true
}
override fun onCategoriesChanged(categories: List<FavouriteCategory>) { override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
adapter.replaceData(categories) adapter.replaceData(categories)
textView_holder.isVisible = categories.isEmpty() textView_holder.isVisible = categories.isEmpty()
@@ -106,6 +115,25 @@ class CategoriesActivity : BaseActivity(), OnRecyclerItemClickListener<Favourite
.show() .show()
} }
private inner class ReorderHelperCallback : ItemTouchHelper.SimpleCallback(
ItemTouchHelper.DOWN or ItemTouchHelper.UP, 0
) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val oldPos = viewHolder.bindingAdapterPosition
val newPos = target.bindingAdapterPosition
adapter.moveItem(oldPos, newPos)
presenter.storeCategoriesOrder(adapter.items.map { it.id })
return true
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) = Unit
}
companion object { companion object {
fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java) fun newIntent(context: Context) = Intent(context, CategoriesActivity::class.java)

View File

@@ -1,17 +1,40 @@
package org.koitharu.kotatsu.ui.main.list.favourites.categories package org.koitharu.kotatsu.ui.main.list.favourites.categories
import android.annotation.SuppressLint
import android.view.MotionEvent
import android.view.ViewGroup import android.view.ViewGroup
import kotlinx.android.synthetic.main.item_category.*
import org.koitharu.kotatsu.core.model.FavouriteCategory import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter import org.koitharu.kotatsu.ui.common.list.BaseRecyclerAdapter
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.main.list.favourites.categories.select.CategoryCheckableHolder
class CategoriesAdapter(onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>? = null) : class CategoriesAdapter(private val onItemClickListener: OnRecyclerItemClickListener<FavouriteCategory>) :
BaseRecyclerAdapter<FavouriteCategory, Unit>(onItemClickListener) { BaseRecyclerAdapter<FavouriteCategory, Unit>() {
override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent) override fun onCreateViewHolder(parent: ViewGroup) = CategoryHolder(parent)
override fun onGetItemId(item: FavouriteCategory) = item.id override fun onGetItemId(item: FavouriteCategory) = item.id
override fun getExtra(item: FavouriteCategory, position: Int) = Unit override fun getExtra(item: FavouriteCategory, position: Int) = Unit
@SuppressLint("ClickableViewAccessibility")
override fun onViewHolderCreated(holder: BaseViewHolder<FavouriteCategory, Unit>) {
holder.imageView_more.setOnClickListener { v ->
onItemClickListener.onItemClick(holder.requireData(), holder.bindingAdapterPosition, v)
}
holder.imageView_handle.setOnTouchListener { v, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
onItemClickListener.onItemLongClick(holder.requireData(), holder.bindingAdapterPosition, v)
} else {
false
}
}
}
fun moveItem(oldPos: Int, newPos: Int) {
val item = dataSet.removeAt(oldPos)
dataSet.add(newPos, item)
notifyItemMoved(oldPos, newPos)
}
} }

View File

@@ -10,6 +10,6 @@ class CategoryHolder(parent: ViewGroup) :
BaseViewHolder<FavouriteCategory, Unit>(parent, R.layout.item_category) { BaseViewHolder<FavouriteCategory, Unit>(parent, R.layout.item_category) {
override fun onBind(data: FavouriteCategory, extra: Unit) { override fun onBind(data: FavouriteCategory, extra: Unit) {
textView.text = data.title textView_title.text = data.title
} }
} }

View File

@@ -2,6 +2,8 @@ package org.koitharu.kotatsu.ui.main.list.favourites.categories
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import moxy.InjectViewState import moxy.InjectViewState
import moxy.presenterScope import moxy.presenterScope
@@ -14,6 +16,9 @@ import org.koitharu.kotatsu.ui.common.BasePresenter
class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() { class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
private lateinit var repository: FavouritesRepository private lateinit var repository: FavouritesRepository
private val reorderMutex by lazy {
Mutex()
}
override fun onFirstViewAttach() { override fun onFirstViewAttach() {
repository = FavouritesRepository() repository = FavouritesRepository()
@@ -28,7 +33,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
repository.getAllCategories() repository.getAllCategories()
} }
viewState.onCategoriesChanged(categories) viewState.onCategoriesChanged(categories)
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -44,7 +49,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
repository.getCategories(manga.id) repository.getCategories(manga.id)
} }
viewState.onCheckedCategoriesChanged(categories.map { it.id.toInt() }.toSet()) viewState.onCheckedCategoriesChanged(categories.map { it.id.toInt() }.toSet())
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -61,7 +66,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
repository.getAllCategories() repository.getAllCategories()
} }
viewState.onCategoriesChanged(categories) viewState.onCategoriesChanged(categories)
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -78,7 +83,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
repository.getAllCategories() repository.getAllCategories()
} }
viewState.onCategoriesChanged(categories) viewState.onCategoriesChanged(categories)
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -95,7 +100,22 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
repository.getAllCategories() repository.getAllCategories()
} }
viewState.onCategoriesChanged(categories) viewState.onCategoriesChanged(categories)
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
viewState.onError(e)
}
}
}
fun storeCategoriesOrder(orderedIds: List<Long>) {
presenterScope.launch {
try {
reorderMutex.withLock {
repository.reorderCategories(orderedIds)
}
} catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -110,7 +130,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
repository.addToCategory(manga,categoryId) repository.addToCategory(manga,categoryId)
} }
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }
@@ -125,7 +145,7 @@ class FavouriteCategoriesPresenter : BasePresenter<FavouriteCategoriesView>() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
repository.removeFromCategory(manga, categoryId) repository.removeFromCategory(manga, categoryId)
} }
} catch (e: Exception) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
} }

View File

@@ -212,7 +212,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
} }
} }
override fun saveState(chapterId: Long, page: Int, scroll: Float) { override fun saveState(chapterId: Long, page: Int, scroll: Int) {
state = state.copy(chapterId = chapterId, page = page, scroll = scroll) state = state.copy(chapterId = chapterId, page = page, scroll = scroll)
ReaderPresenter.saveState(state) ReaderPresenter.saveState(state)
} }
@@ -311,7 +311,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
state = state.copy( state = state.copy(
chapterId = chapter.id, chapterId = chapter.id,
page = 0, page = 0,
scroll = 0f scroll = 0
) )
reader?.updateState(chapterId = chapter.id) reader?.updateState(chapterId = chapter.id)
} }
@@ -395,7 +395,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id chapterId = if (chapterId == -1L) manga.chapters?.firstOrNull()?.id
?: -1 else chapterId, ?: -1 else chapterId,
page = 0, page = 0,
scroll = 0f scroll = 0
) )
) )

View File

@@ -7,5 +7,5 @@ interface ReaderListener : BaseMvpView {
fun onPageChanged(chapter: MangaChapter, page: Int, total: Int) fun onPageChanged(chapter: MangaChapter, page: Int, total: Int)
fun saveState(chapterId: Long, page: Int, scroll: Float) fun saveState(chapterId: Long, page: Int, scroll: Int)
} }

View File

@@ -11,7 +11,7 @@ data class ReaderState(
val manga: Manga, val manga: Manga,
val chapterId: Long, val chapterId: Long,
val page: Int, val page: Int,
val scroll: Float val scroll: Int
) : Parcelable { ) : Parcelable {
@IgnoredOnParcel @IgnoredOnParcel

View File

@@ -54,7 +54,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
pages.addLast(state.chapterId, it) pages.addLast(state.chapterId, it)
adapter?.notifyDataSetChanged() adapter?.notifyDataSetChanged()
setCurrentItem(state.page, false) setCurrentItem(state.page, false)
if (state.scroll != 0f) { if (state.scroll != 0) {
restorePageScroll(state.page, state.scroll) restorePageScroll(state.page, state.scroll)
} }
} }
@@ -196,7 +196,7 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
if (pageId == 0L) { if (pageId == 0L) {
0 0
} else { } else {
it.indexOfFirst { it.id == pageId }.coerceAtLeast(0) it.indexOfFirst { x -> x.id == pageId }.coerceAtLeast(0)
}, false }, false
) )
} }
@@ -217,9 +217,9 @@ abstract class AbstractReader(contentLayoutId: Int) : BaseFragment(contentLayout
protected abstract fun getCurrentItem(): Int protected abstract fun getCurrentItem(): Int
protected abstract fun getCurrentPageScroll(): Float protected abstract fun getCurrentPageScroll(): Int
protected abstract fun restorePageScroll(position: Int, scroll: Float) protected abstract fun restorePageScroll(position: Int, scroll: Int)
protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean) protected abstract fun setCurrentItem(position: Int, isSmooth: Boolean)

View File

@@ -62,9 +62,9 @@ class PagerReaderFragment : AbstractReader(R.layout.fragment_reader_standard),
pager.setCurrentItem(position, isSmooth) pager.setCurrentItem(position, isSmooth)
} }
override fun getCurrentPageScroll() = 0f override fun getCurrentPageScroll() = 0
override fun restorePageScroll(position: Int, scroll: Float) = Unit override fun restorePageScroll(position: Int, scroll: Int) = Unit
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
when (key) { when (key) {

View File

@@ -13,5 +13,12 @@ class WebtoonFrameLayout @JvmOverloads constructor(
findViewById<WebtoonImageView>(R.id.ssiv) findViewById<WebtoonImageView>(R.id.ssiv)
} }
fun dispatchVerticalScroll(dy: Int) = target.dispatchVerticalScroll(dy) fun dispatchVerticalScroll(dy: Int): Int {
if (dy == 0) {
return 0
}
val oldScroll = target.getScroll()
target.scrollBy(dy)
return target.getScroll() - oldScroll
}
} }

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.ui.reader.wetoon package org.koitharu.kotatsu.ui.reader.wetoon
import android.graphics.PointF
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -21,7 +20,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader { SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
private var job: Job? = null private var job: Job? = null
private var scrollToRestore = 0f private var scrollToRestore = 0
init { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(this)
@@ -36,7 +35,7 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
private fun doLoad(data: MangaPage, force: Boolean) { private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel() job?.cancel()
scrollToRestore = 0f scrollToRestore = 0
job = launch { job = launch {
layout_error.isVisible = false layout_error.isVisible = false
progressBar.isVisible = true progressBar.isVisible = true
@@ -60,17 +59,11 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.recycle() ssiv.recycle()
} }
fun getScrollY() = ssiv.center?.y ?: 0f fun getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Float) { fun restoreScroll(scroll: Int) {
if (ssiv.isReady) { if (ssiv.isReady) {
ssiv.setScaleAndCenter( ssiv.scrollTo(scroll)
ssiv.scale,
PointF(
ssiv.sWidth / 2f,
scroll
)
)
} else { } else {
scrollToRestore = scroll scrollToRestore = scroll
} }
@@ -80,17 +73,11 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat() ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM) ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat() ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.setScaleAndCenter( ssiv.scrollTo(when {
ssiv.minScale, scrollToRestore != 0 -> scrollToRestore
PointF( itemView.top < 0 -> ssiv.getScrollRange()
ssiv.sWidth / 2f, else -> 0
when { })
scrollToRestore != 0f -> scrollToRestore
itemView.top < 0 -> ssiv.sHeight.toFloat()
else -> 0f
}
)
)
} }
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageLoadError(e: Exception) = onError(e)

View File

@@ -2,42 +2,66 @@ package org.koitharu.kotatsu.ui.reader.wetoon
import android.content.Context import android.content.Context
import android.graphics.PointF import android.graphics.PointF
import android.graphics.RectF
import android.util.AttributeSet import android.util.AttributeSet
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import org.koitharu.kotatsu.utils.ext.toIntUp import org.koitharu.kotatsu.utils.ext.toIntUp
class WebtoonImageView : SubsamplingScaleImageView { class WebtoonImageView @JvmOverloads constructor(context: Context, attr: AttributeSet? = null) :
SubsamplingScaleImageView(context, attr) {
constructor(context: Context?) : super(context)
constructor(context: Context?, attr: AttributeSet?) : super(context, attr)
private val pan = RectF()
private val ct = PointF() private val ct = PointF()
fun dispatchVerticalScroll(dy: Int): Int { private var scrollPos = 0
private var scrollRange = SCROLL_UNKNOWN
fun scrollBy(delta: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
return
}
val newScroll = scrollPos + delta
scrollToInternal(newScroll.coerceIn(0, maxScroll))
}
fun scrollTo(y: Int) {
val maxScroll = getScrollRange()
if (maxScroll == 0) {
return
}
scrollToInternal(y.coerceIn(0, maxScroll))
}
fun getScroll() = scrollPos
fun getScrollRange(): Int {
if (scrollRange == SCROLL_UNKNOWN) {
computeScrollRange()
}
return scrollRange.coerceAtLeast(0)
}
override fun recycle() {
scrollRange = SCROLL_UNKNOWN
scrollPos = 0
super.recycle()
}
private fun scrollToInternal(pos: Int) {
scrollPos = pos
ct.set(sWidth / 2f, (height / 2f + pos.toFloat()) / minScale)
setScaleAndCenter(minScale, ct)
}
private fun computeScrollRange() {
if (!isReady) { if (!isReady) {
return 0 return
}
getPanRemaining(pan)
// pan.offset(0f, -nonConsumedScroll.toFloat())
ct.set(width / 2f, height / 2f)
viewToSourceCoord(ct.x, ct.y, ct) ?: return 0
val s = scale
return when {
dy > 0 -> {
val delta = minOf(pan.bottom.toIntUp(), dy)
ct.offset(0f, delta.toFloat() / s)
setScaleAndCenter(s, ct)
delta
}
dy < 0 -> {
val delta = minOf(pan.top.toInt(), -dy)
ct.offset(0f, -delta.toFloat() / s)
setScaleAndCenter(s, ct)
-delta
}
else -> 0
} }
val totalHeight = (sHeight * minScale).toIntUp()
scrollRange = (totalHeight - height).coerceAtLeast(0)
}
private companion object {
const val SCROLL_UNKNOWN = -1
} }
} }

View File

@@ -61,12 +61,12 @@ class WebtoonReaderFragment : AbstractReader(R.layout.fragment_reader_webtoon) {
) )
} }
override fun getCurrentPageScroll(): Float { override fun getCurrentPageScroll(): Int {
return (recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder) return (recyclerView.findViewHolderForAdapterPosition(getCurrentItem()) as? WebtoonHolder)
?.getScrollY() ?: 0f ?.getScrollY() ?: 0
} }
override fun restorePageScroll(position: Int, scroll: Float) { override fun restorePageScroll(position: Int, scroll: Int) {
recyclerView.post { recyclerView.post {
val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: return@post val holder = recyclerView.findViewHolderForAdapterPosition(position) ?: return@post
(holder as WebtoonHolder).restoreScroll(scroll) (holder as WebtoonHolder).restoreScroll(scroll)

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.ui.reader.wetoon
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import androidx.core.view.ViewCompat import androidx.core.view.ViewCompat
import androidx.core.view.children
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class WebtoonRecyclerView @JvmOverloads constructor( class WebtoonRecyclerView @JvmOverloads constructor(
@@ -39,13 +38,39 @@ class WebtoonRecyclerView @JvmOverloads constructor(
} }
private fun consumeVerticalScroll(dy: Int): Int { private fun consumeVerticalScroll(dy: Int): Int {
val child = when { if (childCount == 0) {
dy > 0 -> children.firstOrNull { it is WebtoonFrameLayout } return 0
dy < 0 -> children.lastOrNull { it is WebtoonFrameLayout } }
else -> null when {
} ?: return 0 dy > 0 -> {
var scrollY = dy val child = getChildAt(0) as WebtoonFrameLayout
scrollY -= (child as WebtoonFrameLayout).dispatchVerticalScroll(scrollY) var consumedByChild = child.dispatchVerticalScroll(dy)
return dy - scrollY if (consumedByChild < dy) {
if (childCount > 1) {
val nextChild = getChildAt(1) as WebtoonFrameLayout
val unconsumed = dy - consumedByChild - nextChild.top //will be consumed by scroll
if (unconsumed > 0) {
consumedByChild += nextChild.dispatchVerticalScroll(unconsumed)
}
}
}
return consumedByChild
}
dy < 0 -> {
val child = getChildAt(childCount - 1) as WebtoonFrameLayout
var consumedByChild = child.dispatchVerticalScroll(dy)
if (consumedByChild > dy) {
if (childCount > 1) {
val nextChild = getChildAt(childCount - 2) as WebtoonFrameLayout
val unconsumed = dy - consumedByChild + (height - nextChild.bottom) //will be consumed by scroll
if (unconsumed < 0) {
consumedByChild += nextChild.dispatchVerticalScroll(unconsumed)
}
}
}
return consumedByChild
}
}
return 0
} }
} }

View File

@@ -164,7 +164,7 @@ class AppUpdateService : BaseService() {
} }
return try { return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1") val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.getEncoded()) val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted() publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
e.printStackTrace() e.printStackTrace()

View File

@@ -2,7 +2,9 @@ package org.koitharu.kotatsu.ui.settings.sources
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.synthetic.main.fragment_settings_sources.* import kotlinx.android.synthetic.main.fragment_settings_sources.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaSource import org.koitharu.kotatsu.core.model.MangaSource
@@ -27,6 +29,7 @@ class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
recyclerView.addItemDecoration(DividerItemDecoration(view.context, RecyclerView.VERTICAL))
recyclerView.adapter = SourcesAdapter(this) recyclerView.adapter = SourcesAdapter(this)
reorderHelper.attachToRecyclerView(recyclerView) reorderHelper.attachToRecyclerView(recyclerView)
} }
@@ -42,6 +45,6 @@ class SourcesSettingsFragment : BaseFragment(R.layout.fragment_settings_sources)
override fun onItemLongClick(item: MangaSource, position: Int, view: View): Boolean { override fun onItemLongClick(item: MangaSource, position: Int, view: View): Boolean {
reorderHelper.startDrag(recyclerView.findViewHolderForAdapterPosition(position) ?: return false) reorderHelper.startDrag(recyclerView.findViewHolderForAdapterPosition(position) ?: return false)
return super.onItemLongClick(item, position, view) return true
} }
} }

View File

@@ -134,7 +134,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
newChapters.size, newChapters.size) newChapters.size, newChapters.size)
with(builder) { with(builder) {
setContentText(summary) setContentText(summary)
setContentText(manga.title) setContentTitle(manga.title)
setNumber(newChapters.size) setNumber(newChapters.size)
setLargeIcon(Coil.execute(GetRequestBuilder(applicationContext) setLargeIcon(Coil.execute(GetRequestBuilder(applicationContext)
.data(manga.coverUrl) .data(manga.coverUrl)
@@ -152,6 +152,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
intent, PendingIntent.FLAG_UPDATE_CURRENT)) intent, PendingIntent.FLAG_UPDATE_CURRENT))
setAutoCancel(true) setAutoCancel(true)
color = colorPrimary color = colorPrimary
setShortcutId(manga.id.toString())
priority = NotificationCompat.PRIORITY_DEFAULT priority = NotificationCompat.PRIORITY_DEFAULT
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
builder.setSound(settings.notificationSound.toUriOrNull()) builder.setSound(settings.notificationSound.toUriOrNull())

View File

@@ -20,6 +20,8 @@ class WidgetUpdater(private val context: Context) : OnFavouritesChangeListener,
updateWidget(RecentWidgetProvider::class.java) updateWidget(RecentWidgetProvider::class.java)
} }
override fun onCategoriesChanged() = Unit
private fun updateWidget(cls: Class<*>) { private fun updateWidget(cls: Class<*>) {
val intent = Intent(context, cls) val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE

View File

@@ -80,7 +80,7 @@ class ShelfConfigActivity : BaseActivity(), FavouriteCategoriesView,
override fun onCategoriesChanged(categories: List<FavouriteCategory>) { override fun onCategoriesChanged(categories: List<FavouriteCategory>) {
val data = ArrayList<FavouriteCategory>(categories.size + 1) val data = ArrayList<FavouriteCategory>(categories.size + 1)
data += FavouriteCategory(0L, getString(R.string.favourites), Date()) data += FavouriteCategory(0L, getString(R.string.all_favourites), -1, Date())
data += categories data += categories
adapter.replaceData(data) adapter.replaceData(data)
} }

View File

@@ -30,7 +30,13 @@ class ShelfListFactory(private val context: Context, widgetId: Int) : RemoteView
override fun onDataSetChanged() { override fun onDataSetChanged() {
dataSet.clear() dataSet.clear()
val data = runBlocking { val data = runBlocking {
FavouritesRepository().getManga(config.categoryId, 0) val category = config.categoryId
val repo = FavouritesRepository()
if (category == 0L) {
repo.getAllManga()
} else {
repo.getManga(category)
}
} }
dataSet.addAll(data) dataSet.addAll(data)
} }

View File

@@ -3,7 +3,6 @@ package org.koitharu.kotatsu.utils.ext
import android.annotation.SuppressLint import android.annotation.SuppressLint
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this) fun Date.format(pattern: String): String = SimpleDateFormat(pattern).format(this)

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.os.storage.StorageManager import android.os.storage.StorageManager
@@ -45,4 +46,6 @@ fun File.getStorageName(context: Context): String = safe {
Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage) Environment.isExternalStorageRemovable(this) -> context.getString(R.string.external_storage)
else -> null else -> null
} }
} ?: context.getString(R.string.other_storage) } ?: context.getString(R.string.other_storage)
fun Uri.toFileOrNull() = if (scheme == "file") path?.let(::File) else null

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="12dp"
android:height="12dp"
android:tint="?android:textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,4v16L6,20L6,8.83L10.83,4L18,4m0,-2h-8L4,8v12c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,4c0,-1.1 -0.9,-2 -2,-2zM9,7h2v4L9,11zM12,7h2v4h-2zM15,7h2v4h-2z" />
</vector>

View File

@@ -0,0 +1,11 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>

View File

@@ -23,6 +23,7 @@
android:id="@+id/tabs" android:id="@+id/tabs"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:tabGravity="center"
app:tabMode="scrollable" /> app:tabMode="scrollable" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>

View File

@@ -0,0 +1,22 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"
app:tabGravity="center"
app:tabMode="scrollable" />
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -1,14 +1,40 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<TextView <LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/textView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?android:listPreferredItemHeight" android:layout_height="?android:listPreferredItemHeightSmall"
android:background="?android:selectableItemBackground" android:background="?android:windowBackground"
android:gravity="start|center_vertical" android:gravity="center_vertical"
android:paddingStart="?android:listPreferredItemPaddingStart" android:orientation="horizontal">
android:paddingEnd="?android:listPreferredItemPaddingEnd"
android:textAppearance="@style/TextAppearance.AppCompat.Medium" <ImageView
android:textColor="?android:textColorPrimary" android:id="@+id/imageView_handle"
tools:text="@tools:sample/lorem[4]" /> android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="?listPreferredItemPaddingStart"
android:scaleType="center"
android:src="@drawable/ic_reorder_handle" />
<TextView
android:id="@+id/textView_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="marquee"
android:fadingEdge="horizontal"
android:singleLine="true"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary"
tools:text="@tools:sample/lorem[1]" />
<ImageView
android:id="@+id/imageView_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:padding="?listPreferredItemPaddingEnd"
android:scaleType="center"
android:src="@drawable/ic_more" />
</LinearLayout>

View File

@@ -10,6 +10,8 @@
android:id="@+id/ssiv" android:id="@+id/ssiv"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:zoomEnabled="false"
app:quickScaleEnabled="false"
app:panEnabled="false" /> app:panEnabled="false" />
<ProgressBar <ProgressBar

View File

@@ -1,8 +1,2 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu />
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
</menu>

View File

@@ -15,6 +15,11 @@
<item quantity="few">%1$d новых главы</item> <item quantity="few">%1$d новых главы</item>
<item quantity="many">%1$d новых глав</item> <item quantity="many">%1$d новых глав</item>
</plurals> </plurals>
<plurals name="chapters">
<item quantity="one">%1$d глава</item>
<item quantity="few">%1$d главы</item>
<item quantity="many">%1$d глав</item>
</plurals>
<plurals name="chapters_from_x"> <plurals name="chapters_from_x">
<item quantity="one">%1$d глава из %2$d</item> <item quantity="one">%1$d глава из %2$d</item>
<item quantity="few">%1$d главы из %2$d</item> <item quantity="few">%1$d главы из %2$d</item>

View File

@@ -102,7 +102,7 @@
<string name="app_update_available">Доступно обновление приложения</string> <string name="app_update_available">Доступно обновление приложения</string>
<string name="show_notification_app_update">Показывать уведомление при наличии новой версии</string> <string name="show_notification_app_update">Показывать уведомление при наличии новой версии</string>
<string name="open_in_browser">Открыть в браузере</string> <string name="open_in_browser">Открыть в браузере</string>
<string name="large_manga_save_confirm">В этой манге %d глав. Вы уверены, что хотите сохранить их все?</string> <string name="large_manga_save_confirm">В этой манге %s. Вы уверены, что хотите сохранить их все?</string>
<string name="save_manga">Сохранить мангу</string> <string name="save_manga">Сохранить мангу</string>
<string name="notifications">Уведомления</string> <string name="notifications">Уведомления</string>
<string name="enabled_d_from_d">Включено %1$d из %2$d</string> <string name="enabled_d_from_d">Включено %1$d из %2$d</string>
@@ -132,4 +132,7 @@
<string name="other_storage">Другое хранилище</string> <string name="other_storage">Другое хранилище</string>
<string name="use_ssl">Защищённое соединение (HTTPS)</string> <string name="use_ssl">Защищённое соединение (HTTPS)</string>
<string name="done">Готово</string> <string name="done">Готово</string>
<string name="all_favourites">Всё избранное</string>
<string name="favourites_category_empty">В этой категории ничего нет</string>
<string name="read_later">Прочитать позже</string>
</resources> </resources>

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="key_list_mode">list_mode</string> <string name="key_list_mode">list_mode</string>
<string name="key_app_section">app_section</string>
<string name="key_theme">theme</string> <string name="key_theme">theme</string>
<string name="key_sources_order">sources_order</string> <string name="key_sources_order">sources_order</string>
<string name="key_sources_hidden">sources_hidden</string> <string name="key_sources_hidden">sources_hidden</string>

View File

@@ -12,6 +12,10 @@
<item quantity="one">%1$d new chapter</item> <item quantity="one">%1$d new chapter</item>
<item quantity="other">%1$d new chapters</item> <item quantity="other">%1$d new chapters</item>
</plurals> </plurals>
<plurals name="chapters">
<item quantity="one">%1$d chapter</item>
<item quantity="other">%1$d chapters</item>
</plurals>
<plurals name="chapters_from_x"> <plurals name="chapters_from_x">
<item quantity="one">%1$d chapter from %2$d</item> <item quantity="one">%1$d chapter from %2$d</item>
<item quantity="other">%1$d chapters from %2$d</item> <item quantity="other">%1$d chapters from %2$d</item>

View File

@@ -1,4 +1,4 @@
<resources> <resources xmlns:tools="http://schemas.android.com/tools">
<string name="app_name" translatable="false">Kotatsu</string> <string name="app_name" translatable="false">Kotatsu</string>
<string name="close_menu">Close menu</string> <string name="close_menu">Close menu</string>
<string name="open_menu">Open menu</string> <string name="open_menu">Open menu</string>
@@ -103,10 +103,10 @@
<string name="app_update_available">Application update is available</string> <string name="app_update_available">Application update is available</string>
<string name="show_notification_app_update">Show notification if update is available</string> <string name="show_notification_app_update">Show notification if update is available</string>
<string name="open_in_browser">Open in browser</string> <string name="open_in_browser">Open in browser</string>
<string name="large_manga_save_confirm">This manga has %d chapters. Do you want to save all of it?</string> <string name="large_manga_save_confirm">This manga has %s. Do you want to save all of it?</string>
<string name="save_manga">Save manga</string> <string name="save_manga">Save manga</string>
<string name="notifications">Notifications</string> <string name="notifications">Notifications</string>
<string name="enabled_d_from_d">Enabled %1$d from %2$d</string> <string name="enabled_d_from_d" tools:ignore="PluralsCandidate">Enabled %1$d from %2$d</string>
<string name="new_chapters">New chapters</string> <string name="new_chapters">New chapters</string>
<string name="show_notification_new_chapters">Notify about updates of manga you are reading</string> <string name="show_notification_new_chapters">Notify about updates of manga you are reading</string>
<string name="download">Download</string> <string name="download">Download</string>
@@ -133,4 +133,7 @@
<string name="other_storage">Other storage</string> <string name="other_storage">Other storage</string>
<string name="use_ssl">Use secure connection (HTTPS)</string> <string name="use_ssl">Use secure connection (HTTPS)</string>
<string name="done">Done</string> <string name="done">Done</string>
<string name="all_favourites">All favourites</string>
<string name="favourites_category_empty">This category is empty</string>
<string name="read_later">Read later</string>
</resources> </resources>

View File

@@ -1,6 +1,6 @@
#Sun May 10 11:33:24 EEST 2020 #Sat May 16 07:20:00 EEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.4-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.4.1-all.zip