Compare commits

...

30 Commits

Author SHA1 Message Date
0a4f2f848e UI fixes 2020-09-20 17:31:33 +03:00
85fc3a024c Fix henchan 2020-09-20 17:08:42 +03:00
eeb536b1ac Fix crash on import 2020-09-19 17:32:22 +03:00
5b8e8d76c0 Update target sdk 2020-09-19 17:27:45 +03:00
73cf2964b2 Option to manually track manga updates 2020-09-19 15:22:18 +03:00
8372f9b5de Update dependencies 2020-09-19 14:21:32 +03:00
d7181e35e7 Update dependencies 2020-09-05 19:37:40 +03:00
55d824bb94 Remove header from drawer 2020-08-31 19:40:58 +03:00
229d9fa2ae Update launcher icon 2020-08-31 19:37:14 +03:00
3eb68e1ff9 Manual screen rotation in reader 2020-08-28 21:01:59 +03:00
b103589bba Fix unsupported image formats in reader 2020-08-27 19:54:53 +03:00
0726c037a4 Update dependencies 2020-08-26 16:14:32 +03:00
0ff64931e0 Close drawer on back pressed 2020-08-09 16:54:00 +03:00
2374c96009 Fix source preferences summaries 2020-07-19 11:44:12 +03:00
2dd51117e9 Remove unused resources 2020-07-16 19:27:59 +03:00
6c5f3c7d97 Fix readmanga search 2020-07-16 19:20:59 +03:00
626bb20edb Fix global search 2020-07-12 13:48:52 +03:00
d363869dab Fix cbz thumbnails 2020-07-10 07:22:16 +03:00
774f33c63d Get remote manga for local in tracker 2020-07-10 07:14:41 +03:00
079427346a Update dependencies 2020-07-10 07:09:41 +03:00
a1a3125834 Fix crash on manga downloading 2020-07-06 20:05:41 +03:00
fc9c8f8a79 Update readme 2020-07-05 17:30:44 +03:00
c06923dbdf Merge branch 'devel' 2020-07-05 17:14:37 +03:00
66ca51cc73 Fix MangaTown endless search 2020-07-05 17:09:38 +03:00
bf45480366 Update henchan default domain 2020-07-05 17:01:43 +03:00
28618e394e Fix *chan search 2020-07-05 16:59:20 +03:00
9762a466ce Fix Mangalib pages loading 2020-07-05 16:45:34 +03:00
367a97a95b Fix page error processing 2020-07-01 19:37:15 +03:00
c3ab197aa0 Fix some StrictMode warnings 2020-07-01 19:17:08 +03:00
0e08d75626 Update dependencies 2020-06-14 17:12:30 +03:00
110 changed files with 732 additions and 483 deletions

View File

@@ -5,9 +5,9 @@ jdk:
android:
components:
- tools
- platform-tools-29.0.6
- build-tools-29.0.3
- android-29
- platform-tools-30.0.3
- build-tools-30.0.2
- android-30
licenses:
- android-sdk-preview-license-.+
- android-sdk-license-.+

View File

@@ -21,9 +21,6 @@ Legacy build (Android 4.1+): [available here](https://github.com/nv95/Kotatsu/re
* Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader
* Notifications about new chapters
### Coming Features
* Updates feed
* Global search

View File

@@ -8,15 +8,15 @@ plugins {
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
android {
compileSdkVersion 29
buildToolsVersion '29.0.3'
compileSdkVersion 30
buildToolsVersion '30.0.2'
defaultConfig {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 29
targetSdkVersion 30
versionCode gitCommits
versionName '0.5-b2'
versionName '0.5.2'
kapt {
arguments {
@@ -28,10 +28,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
@@ -55,23 +51,31 @@ android {
androidExtensions {
experimental = true
}
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += ['-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi', '-Xjvm-default=all']
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-alpha01'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.core:core-ktx:1.5.0-alpha03'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07'
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01'
implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'com.google.android.material:material:1.3.0-alpha02'
//noinspection LifecycleAnnotationProcessorWithJava8
kapt 'androidx.lifecycle:lifecycle-compiler:2.3.0-alpha07'
implementation 'androidx.room:room-runtime:2.2.5'
implementation 'androidx.room:room-ktx:2.2.5'
@@ -83,12 +87,12 @@ dependencies {
implementation 'com.github.moxy-community:moxy-ktx:2.1.2'
kapt 'com.github.moxy-community:moxy-compiler:2.1.2'
implementation 'com.squareup.okhttp3:okhttp:4.7.2'
implementation 'com.squareup.okio:okio:2.6.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.8.0'
implementation 'org.jsoup:jsoup:1.13.1'
implementation 'org.koin:koin-android:2.1.5'
implementation 'io.coil-kt:coil:0.11.0'
implementation 'org.koin:koin-android:2.2.0-beta-1'
implementation 'io.coil-kt:coil:1.0.0-rc2'
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
implementation 'com.tomclaw.cache:cache:1.0'

View File

@@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#E6000A">
<group android:scaleX="0.40188664"
android:scaleY="0.40188664"
android:translateX="32.90095"
android:translateY="18.7272">
<group android:translateY="139.39206">
<path android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z"
android:fillColor="#E6000A"/>
</group>
</group>
</vector>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>

View File

@@ -75,7 +75,6 @@
<service
android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service
android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />

View File

@@ -1,13 +1,12 @@
package org.koitharu.kotatsu
import android.app.Application
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import coil.Coil
import coil.ComponentRegistry
import coil.ImageLoaderBuilder
import coil.ImageLoader
import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor
@@ -15,6 +14,7 @@ import okhttp3.OkHttpClient
import org.koin.android.ext.koin.androidContext
import org.koin.android.ext.koin.androidLogger
import org.koin.core.context.startKoin
import org.koin.core.logger.Level
import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback
import org.koitharu.kotatsu.core.db.MangaDatabase
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.core.local.PagesCache
import org.koitharu.kotatsu.core.local.cookies.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor
import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext
@@ -46,6 +47,19 @@ class KotatsuApp : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.build())
StrictMode.setVmPolicy(StrictMode.VmPolicy.Builder()
.detectAll()
.setClassInstanceLimit(LocalMangaRepository::class.java, 1)
.setClassInstanceLimit(PagesCache::class.java, 1)
.setClassInstanceLimit(MangaLoaderContext::class.java, 1)
.penaltyLog()
.build())
}
initKoin()
initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
@@ -60,7 +74,7 @@ class KotatsuApp : Application() {
private fun initKoin() {
startKoin {
androidLogger()
androidLogger(Level.ERROR)
androidContext(applicationContext)
modules(
module {
@@ -75,7 +89,7 @@ class KotatsuApp : Application() {
single {
MangaLoaderContext()
}
factory {
single {
AppSettings(applicationContext)
}
single {
@@ -88,7 +102,7 @@ class KotatsuApp : Application() {
private fun initCoil() {
Coil.setImageLoader(
ImageLoaderBuilder(applicationContext)
ImageLoader.Builder(applicationContext)
.okHttpClient(
okHttp()
.cache(CoilUtils.createDefaultCache(applicationContext))

View File

@@ -4,19 +4,19 @@ import androidx.room.*
import org.koitharu.kotatsu.core.db.entity.TagEntity
@Dao
interface TagsDao {
abstract class TagsDao {
@Query("SELECT * FROM tags")
suspend fun getAllTags(): List<TagEntity>
abstract suspend fun getAllTags(): List<TagEntity>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(tag: TagEntity): Long
abstract suspend fun insert(tag: TagEntity): Long
@Update(onConflict = OnConflictStrategy.IGNORE)
suspend fun update(tag: TagEntity): Int
abstract suspend fun update(tag: TagEntity): Int
@Transaction
suspend fun upsert(tags: Iterable<TagEntity>) {
open suspend fun upsert(tags: Iterable<TagEntity>) {
tags.forEach { tag ->
if (update(tag) <= 0) {
insert(tag)

View File

@@ -2,7 +2,7 @@ package org.koitharu.kotatsu.core.local
import android.net.Uri
import android.webkit.MimeTypeMap
import coil.bitmappool.BitmapPool
import coil.bitmap.BitmapPool
import coil.decode.DataSource
import coil.decode.Options
import coil.fetch.FetchResult

View File

@@ -3,6 +3,8 @@ package org.koitharu.kotatsu.core.parser
import android.annotation.SuppressLint
import android.content.Context
import android.net.Uri
import android.webkit.MimeTypeMap
import androidx.collection.ArraySet
import androidx.core.net.toFile
import androidx.core.net.toUri
import org.koin.core.KoinComponent
@@ -78,8 +80,7 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
}
@SuppressLint("DefaultLocale")
fun getFromFile(file: File): Manga {
val zip = ZipFile(file)
fun getFromFile(file: File): Manga = ZipFile(file).use { zip ->
val fileUri = file.toUri().toString()
val entry = zip.getEntry(MangaZip.INDEX_ENTRY)
val index = entry?.let(zip::readText)?.let(::MangaIndex)
@@ -91,26 +92,26 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
coverUrl = zipUri(
file,
entryName = index.getCoverEntry()
?: findFirstEntry(zip.entries())?.name.orEmpty()
?: findFirstEntry(zip.entries(), isImage = true)?.name.orEmpty()
),
chapters = info.chapters?.map { c -> c.copy(url = fileUri) }
)
}
// fallback
val title = file.nameWithoutExtension.replace("_", " ").capitalize()
val chapters = HashSet<String>()
val chapters = ArraySet<String>()
for (x in zip.entries()) {
if (!x.isDirectory) {
chapters += x.name.substringBeforeLast(File.separatorChar, "")
}
}
val uriBuilder = file.toUri().buildUpon()
return Manga(
Manga(
id = file.absolutePath.longHashCode(),
title = title,
url = fileUri,
source = MangaSource.LOCAL,
coverUrl = zipUri(file, findFirstEntry(zip.entries())?.name.orEmpty()),
coverUrl = zipUri(file, findFirstEntry(zip.entries(), isImage = true)?.name.orEmpty()),
chapters = chapters.sortedWith(AlphanumComparator()).mapIndexed { i, s ->
MangaChapter(
id = "$i$s".longHashCode(),
@@ -136,11 +137,19 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
private fun zipUri(file: File, entryName: String) =
Uri.fromParts("cbz", file.path, entryName).toString()
private fun findFirstEntry(entries: Enumeration<out ZipEntry>): ZipEntry? {
private fun findFirstEntry(entries: Enumeration<out ZipEntry>, isImage: Boolean): ZipEntry? {
val list = entries.toList()
.filterNot { it.isDirectory }
.sortedWith(compareBy(AlphanumComparator()) { x -> x.name })
return list.firstOrNull()
return if (isImage) {
val map = MimeTypeMap.getSingleton()
list.firstOrNull {
map.getMimeTypeFromExtension(it.name.substringAfterLast('.'))
?.startsWith("image/") == true
}
} else {
list.firstOrNull()
}
}
override val sortOrders = emptySet<SortOrder>()

View File

@@ -23,7 +23,12 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
): List<Manga> {
val domain = conf.getDomain(defaultDomain)
val url = when {
query != null -> "https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}"
!query.isNullOrEmpty() -> {
if (offset != 0) {
return emptyList()
}
"https://$domain/?do=search&subaction=search&story=${query.urlEncoded()}"
}
tag != null -> "https://$domain/tags/${tag.key}&n=${getSortKey2(sortOrder)}?offset=$offset"
else -> "https://$domain/${getSortKey(sortOrder)}?offset=$offset"
}
@@ -126,7 +131,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
override fun onCreatePreferences() = setOf(R.string.key_parser_domain)
private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minBy { it.ordinal }) {
when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
SortOrder.ALPHABETICAL -> "catalog"
SortOrder.POPULARITY -> "mostfavorites"
SortOrder.NEWEST -> "manga/new"
@@ -134,7 +139,7 @@ abstract class ChanRepository(loaderContext: MangaLoaderContext) : RemoteMangaRe
}
private fun getSortKey2(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minBy { it.ordinal }) {
when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
SortOrder.ALPHABETICAL -> "abcasc"
SortOrder.POPULARITY -> "favdesc"
SortOrder.NEWEST -> "datedesc"

View File

@@ -28,7 +28,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
val doc = when {
!query.isNullOrEmpty() -> loaderContext.httpPost(
"https://$domain/search",
mapOf("q" to query, "offset" to offset.toString())
mapOf("q" to query.urlEncoded(), "offset" to offset.toString())
)
tag == null -> loaderContext.httpGet(
"https://$domain/list?sortType=${getSortKey(
@@ -165,7 +165,7 @@ abstract class GroupleRepository(loaderContext: MangaLoaderContext) :
override fun onCreatePreferences() = setOf(R.string.key_parser_domain)
private fun getSortKey(sortOrder: SortOrder?) =
when (sortOrder ?: sortOrders.minBy { it.ordinal }) {
when (sortOrder ?: sortOrders.minByOrNull { it.ordinal }) {
SortOrder.ALPHABETICAL -> "name"
SortOrder.POPULARITY -> "rate"
SortOrder.UPDATED -> "updated"

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.model.MangaTag
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml
@@ -12,9 +9,25 @@ import org.koitharu.kotatsu.utils.ext.withDomain
class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {
override val defaultDomain = "h-chan.me"
override val defaultDomain = "hentaichan.pro"
override val source = MangaSource.HENCHAN
override suspend fun getList(
offset: Int,
query: String?,
sortOrder: SortOrder?,
tag: MangaTag?
): List<Manga> {
return super.getList(offset, query, sortOrder, tag).map {
val cover = it.coverUrl
if (cover.contains("_blur")) {
it.copy(coverUrl = cover.replace("_blur", ""))
} else {
it
}
}
}
override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml()

View File

@@ -37,7 +37,12 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposi
}
val page = (offset / 30) + 1
val url = when {
!query.isNullOrEmpty() -> "$scheme://$domain/search?name=${query.urlEncoded()}"
!query.isNullOrEmpty() -> {
if (offset != 0) {
return emptyList()
}
"$scheme://$domain/search?name=${query.urlEncoded()}"
}
tag != null -> "$scheme://$domain/directory/${tag.key}/$page.htm$sortKey"
else -> "$scheme://$domain/directory/$page.htm$sortKey"
}

View File

@@ -5,6 +5,6 @@ import org.koitharu.kotatsu.domain.MangaLoaderContext
class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
override val defaultDomain = "readmanga.me"
override val defaultDomain = "readmanga.live"
override val source = MangaSource.READMANGA_RU
}

View File

@@ -18,7 +18,12 @@ class MangaSearchRepository : KoinComponent {
var isEmitted = false
for (source in sources) {
val list = lists.getOrPut(source) {
try {
MangaProviderFactory.create(source).getList(0, query, SortOrder.POPULARITY)
} catch (e: Throwable) {
e.printStackTrace()
emptyList<Manga>()
}
}
if (i < list.size) {
emit(list.subList(i, (i + batchSize).coerceAtMost(list.lastIndex)))

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain.favourites
import androidx.collection.ArraySet
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -12,7 +13,6 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
import org.koitharu.kotatsu.core.db.entity.TagEntity
import org.koitharu.kotatsu.core.model.FavouriteCategory
import org.koitharu.kotatsu.core.model.Manga
import java.util.*
class FavouritesRepository : KoinComponent {
@@ -98,7 +98,7 @@ class FavouritesRepository : KoinComponent {
companion object {
private val listeners = HashSet<OnFavouritesChangeListener>()
private val listeners = ArraySet<OnFavouritesChangeListener>()
fun subscribe(listener: OnFavouritesChangeListener) {
listeners += listener

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.domain.history
import androidx.collection.ArraySet
import androidx.room.withTransaction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -71,7 +72,7 @@ class HistoryRepository : KoinComponent {
companion object {
private val listeners = HashSet<OnHistoryChangeListener>()
private val listeners = ArraySet<OnHistoryChangeListener>()
fun subscribe(listener: OnHistoryChangeListener) {
listeners += listener

View File

@@ -6,10 +6,8 @@ import org.koin.core.inject
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.entity.TrackEntity
import org.koitharu.kotatsu.core.db.entity.TrackLogEntity
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.core.model.MangaTracking
import org.koitharu.kotatsu.core.model.TrackingLogItem
import org.koitharu.kotatsu.core.model.*
import org.koitharu.kotatsu.domain.MangaProviderFactory
import java.util.*
class TrackingRepository : KoinComponent {
@@ -24,12 +22,16 @@ class TrackingRepository : KoinComponent {
suspend fun getAllTracks(): List<MangaTracking> {
val favourites = db.favouritesDao.findAllManga()
val history = db.historyDao.findAllManga()
val manga = (favourites + history).distinctBy { it.id }
val mangas = (favourites + history).distinctBy { it.id }
val tracks = db.tracksDao.findAll().groupBy { it.mangaId }
return manga.map { m ->
val track = tracks[m.id]?.singleOrNull()
return mangas.mapNotNull { me ->
var manga = me.toManga()
if (manga.source == MangaSource.LOCAL) {
manga = MangaProviderFactory.createLocal().getRemoteManga(manga) ?: return@mapNotNull null
}
val track = tracks[manga.id]?.singleOrNull()
MangaTracking(
manga = m.toManga(),
manga = manga,
knownChaptersCount = track?.totalChapters ?: -1,
lastChapterId = track?.lastChapterId ?: 0L,
lastNotifiedChapterId = track?.lastNotifiedChapterId ?: 0L,

View File

@@ -27,7 +27,7 @@ class BrowserClient(private val callback: BrowserCallback) : WebViewClient(), Ko
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
callback.onTitleChanged(view.title, url)
callback.onTitleChanged(view.title.orEmpty(), url)
}
override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?) = false

View File

@@ -28,7 +28,7 @@ class ChapterHolder(parent: ViewGroup) :
}
ChapterExtra.CURRENT -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_outline_accent)
textView_number.setTextColor(context.getThemeColor(R.attr.colorAccent))
textView_number.setTextColor(context.getThemeColor(androidx.appcompat.R.attr.colorAccent))
}
ChapterExtra.NEW -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_accent)

View File

@@ -206,7 +206,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
override fun onSupportActionModeFinished(mode: ActionMode) {
super.onSupportActionModeFinished(mode)
pager.isUserInputEnabled = true
window?.statusBarColor = getThemeColor(R.attr.colorPrimaryDark)
window?.statusBarColor = getThemeColor(androidx.appcompat.R.attr.colorPrimaryDark)
}
companion object {

View File

@@ -5,9 +5,13 @@ import android.view.View
import androidx.core.net.toUri
import androidx.core.text.parseAsHtml
import androidx.core.view.isVisible
import coil.api.load
import androidx.lifecycle.lifecycleScope
import coil.load
import com.google.android.material.chip.Chip
import kotlinx.android.synthetic.main.fragment_details.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import moxy.ktx.moxyPresenter
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.FavouriteCategory
@@ -73,15 +77,20 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
)
}
manga.url.toUri().toFileOrNull()?.let { f ->
lifecycleScope.launch {
val size = withContext(Dispatchers.IO) {
f.length()
}
chips_tags.addChips(listOf(f)) {
create(
text = FileSizeUtils.formatBytes(context, it.length()),
text = FileSizeUtils.formatBytes(context, size),
iconRes = R.drawable.ic_chip_storage,
tag = it,
onClickListener = this@MangaDetailsFragment
)
}
}
}
imageView_favourite.setOnClickListener(this)
button_read.setOnClickListener(this)
button_read.setOnLongClickListener(this)

View File

@@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.ext.clearActions
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import kotlin.math.roundToInt

View File

@@ -8,7 +8,7 @@ import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.core.content.ContextCompat
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient
@@ -77,9 +77,9 @@ class DownloadService : BaseService() {
return launch(Dispatchers.IO) {
mutex.lock()
wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
withContext(Dispatchers.Main) {
notification.fillFrom(manga)
notification.setCancelId(startId)
withContext(Dispatchers.Main) {
startForeground(DownloadNotification.NOTIFICATION_ID, notification())
}
val destination = settings.getStorageDir(this@DownloadService)
@@ -89,15 +89,13 @@ class DownloadService : BaseService() {
val repo = MangaProviderFactory.create(manga.source)
val cover = safe {
Coil.execute(
GetRequestBuilder(this@DownloadService)
ImageRequest.Builder(this@DownloadService)
.data(manga.coverUrl)
.build()
).drawable
}
withContext(Dispatchers.Main) {
notification.setLargeIcon(cover)
notification.update()
}
val data = if (manga.chapters == null) repo.getDetails(manga) else manga
output = MangaZip.findInDir(destination, data)
output.prepare(data)
@@ -141,31 +139,25 @@ class DownloadService : BaseService() {
}
}
}
withContext(Dispatchers.Main) {
notification.setCancelId(0)
notification.setPostProcessing()
notification.update()
}
output.compress()
val result = MangaProviderFactory.createLocal().getFromFile(output.file)
withContext(Dispatchers.Main) {
notification.setDone(result)
notification.dismiss()
notification.update(manga.id.toInt().absoluteValue)
}
} catch (_: CancellationException) {
withContext(Dispatchers.Main + NonCancellable) {
withContext(NonCancellable) {
notification.setCancelling()
notification.setCancelId(0)
notification.update()
}
} catch (e: Throwable) {
withContext(Dispatchers.Main) {
notification.setError(e)
notification.setCancelId(0)
notification.dismiss()
notification.update(manga.id.toInt().absoluteValue)
}
} finally {
withContext(NonCancellable) {
jobs.remove(startId)

View File

@@ -37,6 +37,7 @@ import org.koitharu.kotatsu.ui.settings.SettingsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp
import java.io.Closeable
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener,
SharedPreferences.OnSharedPreferenceChangeListener, MainView {
@@ -45,6 +46,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private val settings by inject<AppSettings>()
private lateinit var drawerToggle: ActionBarDrawerToggle
private var closeable: Closeable? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -74,6 +76,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
}
override fun onDestroy() {
closeable?.close()
settings.unsubscribe(this)
super.onDestroy()
}
@@ -89,10 +92,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
drawerToggle.onConfigurationChanged(newConfig)
}
override fun onBackPressed() {
if (drawer.isDrawerOpen(navigationView)) {
drawer.closeDrawer(navigationView)
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_main, menu)
menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem)
closeable = SearchHelper.setupSearchView(menuItem)
}
return super.onCreateOptionsMenu(menu)
}

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup
import coil.api.clear
import coil.api.load
import coil.clear
import coil.load
import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga

View File

@@ -3,8 +3,8 @@ package org.koitharu.kotatsu.ui.list
import android.annotation.SuppressLint
import android.view.ViewGroup
import androidx.core.view.isVisible
import coil.api.clear
import coil.api.load
import coil.clear
import coil.load
import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga

View File

@@ -126,6 +126,7 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
}
final override fun onRefresh() {
swipeRefreshLayout.isRefreshing = true
onRequestMoreItems(0)
}
@@ -188,10 +189,11 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
override fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems
swipeRefreshLayout.isRefreshing = isLoading && hasItems
swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible
if (isLoading) {
layout_holder.isVisible = false
} else {
swipeRefreshLayout.isRefreshing = false
}
}

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup
import coil.api.clear
import coil.api.load
import coil.clear
import coil.load
import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga

View File

@@ -3,6 +3,7 @@ package org.koitharu.kotatsu.ui.list.feed
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar
@@ -16,6 +17,7 @@ import org.koitharu.kotatsu.ui.common.list.OnRecyclerItemClickListener
import org.koitharu.kotatsu.ui.common.list.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.callOnScrollListeners
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems
@@ -53,6 +55,15 @@ class FeedFragment : BaseFragment(R.layout.fragment_tracklogs), FeedView,
inflater.inflate(R.menu.opt_feed, menu)
}
override fun onOptionsItemSelected(item: MenuItem) = when(item.itemId) {
R.id.action_update -> {
TrackWorker.startNow(requireContext())
Snackbar.make(recyclerView, R.string.feed_will_update_soon, Snackbar.LENGTH_LONG).show()
true
}
else -> super.onOptionsItemSelected(item)
}
override fun onDestroyView() {
adapter = null
super.onDestroyView()

View File

@@ -2,8 +2,8 @@ package org.koitharu.kotatsu.ui.list.feed
import android.text.format.DateUtils
import android.view.ViewGroup
import coil.api.clear
import coil.api.load
import coil.clear
import coil.load
import kotlinx.android.synthetic.main.item_tracklog.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.TrackingLogItem

View File

@@ -21,6 +21,10 @@ import java.io.File
class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri> {
private val presenter by moxyPresenter(factory = ::LocalListPresenter)
private val importCall = registerForActivityResult(
ActivityResultContracts.OpenDocument(),
this
)
override fun onRequestMoreItems(offset: Int) {
presenter.loadList(offset)
@@ -35,8 +39,7 @@ class LocalListFragment : MangaListFragment<File>(), ActivityResultCallback<Uri>
return when (item.itemId) {
R.id.action_import -> {
try {
registerForActivityResult(ActivityResultContracts.OpenDocument(), this)
.launch(arrayOf("*/*"))
importCall.launch(arrayOf("*/*"))
} catch (e: ActivityNotFoundException) {
if (BuildConfig.DEBUG) {
e.printStackTrace()

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.ui.reader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.util.ArrayMap
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient
import okhttp3.Request
import org.koin.core.KoinComponent
@@ -16,9 +21,10 @@ import kotlin.coroutines.CoroutineContext
class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val job = SupervisorJob()
private val tasks = HashMap<String, Deferred<File>>()
private val tasks = ArrayMap<String, Deferred<File>>()
private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>()
private val convertLock = Mutex()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
@@ -30,7 +36,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
return it
}
}
val task = tasks[url]?.takeUnless { it.isCancelled }
val task = tasks[url]?.takeUnless { it.isCancelled || (force && it.isCompleted) }
return (task ?: loadAsync(url).also { tasks[url] = it }).await()
}
@@ -48,10 +54,14 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
val request = Request.Builder()
.url(url)
.get()
.header("Accept", "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CacheUtils.CONTROL_DISABLED)
.build()
okHttp.newCall(request).await().use { response ->
val body = response.body
check(response.isSuccessful) {
"Invalid response: ${response.code} ${response.message}"
}
checkNotNull(body) {
"Null response"
}
@@ -62,6 +72,21 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
}
}
suspend fun convertInPlace(file: File) {
convertLock.withLock(file) {
withContext(Dispatchers.IO) {
val image = BitmapFactory.decodeFile(file.absolutePath)
try {
file.outputStream().use { out ->
image.compress(Bitmap.CompressFormat.WEBP, 100, out)
}
} finally {
image.recycle()
}
}
}
}
override fun dispose() {
coroutineContext.cancel()
tasks.clear()

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.reader
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
@@ -17,10 +18,13 @@ import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.core.view.updatePadding
import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import moxy.MvpDelegate
import moxy.ktx.moxyPresenter
@@ -40,6 +44,7 @@ import org.koitharu.kotatsu.ui.reader.thumbnails.PagesThumbnailsSheet
import org.koitharu.kotatsu.ui.reader.wetoon.WebtoonReaderFragment
import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.*
@@ -56,6 +61,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
private set
private lateinit var touchHelper: GridTouchHelper
private lateinit var orientationHelper: ScreenOrientationHelper
private var isTapSwitchEnabled = true
private var isVolumeKeysSwitchEnabled = false
@@ -67,6 +73,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
setContentView(R.layout.activity_reader)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this)
toolbar_bottom.inflateMenu(R.menu.opt_reader_bottom)
toolbar_bottom.setOnMenuItemClickListener(::onOptionsItemSelected)
@@ -89,6 +96,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
settings.subscribe(this)
loadSettings()
orientationHelper.observeAutoOrientation()
.onEach {
toolbar_bottom.menu.findItem(R.id.action_screen_rotate).isVisible = !it
}.launchIn(lifecycleScope)
if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
presenter.init(state.manga)
@@ -165,6 +176,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
)
true
}
R.id.action_screen_rotate -> {
orientationHelper.toggleOrientation()
true
}
R.id.action_pages_thumbs -> {
if (reader?.hasItems == true) {
val pages = reader?.getPages()
@@ -325,14 +340,18 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
presenter.setMode(state.manga, mode)
}
@SuppressLint("ShowToast")
override fun onPageSaved(uri: Uri?) {
if (uri != null) {
Snackbar.make(container, R.string.page_saved, Snackbar.LENGTH_LONG)
.setAnchorView(appbar_bottom)
.setAction(R.string.share) {
ShareHelper.shareImage(this, uri)
}.show()
} else {
Snackbar.make(container, R.string.error_occurred, Snackbar.LENGTH_SHORT).show()
Snackbar.make(container, R.string.error_occurred, Snackbar.LENGTH_SHORT)
.setAnchorView(appbar_bottom)
.show()
}
}
@@ -369,8 +388,16 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
}
override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
appbar_top.updatePadding(top = insets.systemWindowInsetTop)
appbar_bottom.updatePadding(bottom = insets.systemWindowInsetBottom)
appbar_top.updatePadding(
top = insets.systemWindowInsetTop,
right = insets.systemWindowInsetRight,
left = insets.systemWindowInsetLeft
)
appbar_bottom.updatePadding(
bottom = insets.systemWindowInsetBottom,
right = insets.systemWindowInsetRight,
left = insets.systemWindowInsetLeft
)
return insets.consumeSystemWindowInsets()
}

View File

@@ -0,0 +1,106 @@
package org.koitharu.kotatsu.ui.reader.base
import android.net.Uri
import androidx.core.net.toUri
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.coroutines.*
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.utils.ext.launchAfter
import org.koitharu.kotatsu.utils.ext.launchInstead
import java.io.File
import java.io.IOException
class PageHolderDelegate(
private val loader: PageLoader,
private val callback: Callback
) : SubsamplingScaleImageView.DefaultOnImageEventListener(), CoroutineScope by loader {
private var state = State.EMPTY
private var job: Job? = null
private var file: File? = null
fun onBind(page: MangaPage) {
doLoad(page, force = false)
}
fun retry(page: MangaPage) {
doLoad(page, force = true)
}
fun onRecycle() {
state = State.EMPTY
file = null
job?.cancel()
}
override fun onReady() {
state = State.SHOWING
callback.onImageShowing()
}
override fun onImageLoaded() {
state = State.SHOWN
callback.onImageShown()
}
override fun onImageLoadError(e: Exception) {
val file = this.file
if (state == State.LOADED && e is IOException && file != null && file.exists()) {
job = launchAfter(job) {
state = State.CONVERTING
try {
loader.convertInPlace(file)
state = State.CONVERTED
callback.onImageReady(file.toUri())
} catch (e2: Throwable) {
e2.addSuppressed(e)
state = State.ERROR
callback.onError(e2)
}
}
} else {
state = State.ERROR
callback.onError(e)
}
}
private fun doLoad(data: MangaPage, force: Boolean) {
job = launchInstead(job) {
state = State.LOADING
callback.onLoadingStarted()
try {
val file = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}
this@PageHolderDelegate.file = file
state = State.LOADED
callback.onImageReady(file.toUri())
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
state = State.ERROR
callback.onError(e)
}
}
}
private enum class State {
EMPTY, LOADING, LOADED, CONVERTING, CONVERTED, SHOWING, SHOWN, ERROR
}
interface Callback {
fun onLoadingStarted()
fun onError(e: Throwable)
fun onImageReady(uri: Uri)
fun onImageShowing()
fun onImageShown()
}
}

View File

@@ -1,79 +1,67 @@
package org.koitharu.kotatsu.ui.reader.standard
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class PageHolder(parent: ViewGroup, private val loader: PageLoader) :
class PageHolder(parent: ViewGroup, loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page),
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
PageHolderDelegate.Callback, View.OnClickListener {
private var job: Job? = null
private val delegate = PageHolderDelegate(loader, this)
init {
ssiv.setOnImageEventListener(this)
button_retry.setOnClickListener {
doLoad(boundData ?: return@setOnClickListener, force = true)
}
ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener(this)
}
override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false)
delegate.onBind(data)
}
override fun onRecycled() {
job?.cancel()
delegate.onRecycle()
ssiv.recycle()
}
private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel()
job = launch {
override fun onLoadingStarted() {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
onError(e)
}
}
}
override fun onReady() {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat())
override fun onImageReady(uri: Uri) {
ssiv.setImage(ImageSource.uri(uri))
}
override fun onImageShowing() {
ssiv.maxScale = 2f * maxOf(
ssiv.width / ssiv.sWidth.toFloat(),
ssiv.height / ssiv.sHeight.toFloat()
)
ssiv.resetScaleAndCenter()
}
override fun onImageLoadError(e: Exception) = onError(e)
override fun onImageLoaded() {
override fun onImageShown() {
progressBar.isVisible = false
}
override fun onTileLoadError(e: Exception?) = Unit
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return)
}
}
override fun onPreviewReleased() = Unit
override fun onPreviewLoadError(e: Exception?) = Unit
private fun onError(e: Throwable) {
override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.ui.reader.thumbnails
import android.view.ViewGroup
import androidx.core.net.toUri
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import coil.size.PixelSize
import coil.size.Size
import kotlinx.android.synthetic.main.item_page_thumb.*
@@ -38,10 +38,12 @@ class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope)
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
extra[pageUrl]?.toUri()?.toString() ?: pageUrl
}
val drawable = Coil.execute(GetRequestBuilder(context)
val drawable = Coil.execute(
ImageRequest.Builder(context)
.data(url)
.size(thumbSize)
.build()).drawable
.build()
).drawable
withContext(Dispatchers.Main) {
imageView_thumb.setImageDrawable(drawable)
}

View File

@@ -1,64 +1,80 @@
package org.koitharu.kotatsu.ui.reader.wetoon
import android.net.Uri
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage
import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon),
SubsamplingScaleImageView.OnImageEventListener, CoroutineScope by loader {
PageHolderDelegate.Callback, View.OnClickListener {
private var job: Job? = null
private val delegate = PageHolderDelegate(loader, this)
private var scrollToRestore = 0
init {
ssiv.setOnImageEventListener(this)
button_retry.setOnClickListener {
doLoad(boundData ?: return@setOnClickListener, force = true)
}
ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener(this)
}
override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false)
}
private fun doLoad(data: MangaPage, force: Boolean) {
job?.cancel()
scrollToRestore = 0
job = launch {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
try {
val uri = withContext(Dispatchers.IO) {
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
loader.loadFile(pageUrl, force)
}.toUri()
ssiv.setImage(ImageSource.uri(uri))
} catch (e: CancellationException) {
//do nothing
} catch (e: Exception) {
onError(e)
}
}
delegate.onBind(data)
}
override fun onRecycled() {
job?.cancel()
delegate.onRecycle()
ssiv.recycle()
}
override fun onLoadingStarted() {
layout_error.isVisible = false
progressBar.isVisible = true
ssiv.recycle()
}
override fun onImageReady(uri: Uri) {
ssiv.setImage(ImageSource.uri(uri))
}
override fun onImageShowing() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.scrollTo(
when {
scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> ssiv.getScrollRange()
else -> 0
}
)
}
override fun onImageShown() {
progressBar.isVisible = false
}
override fun onClick(v: View) {
when (v.id) {
R.id.button_retry -> delegate.retry(boundData ?: return)
}
}
override fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false
}
fun getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Int) {
@@ -68,33 +84,4 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
scrollToRestore = scroll
}
}
override fun onReady() {
ssiv.maxScale = 2f * ssiv.width / ssiv.sWidth.toFloat()
ssiv.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CUSTOM)
ssiv.minScale = ssiv.width / ssiv.sWidth.toFloat()
ssiv.scrollTo(when {
scrollToRestore != 0 -> scrollToRestore
itemView.top < 0 -> ssiv.getScrollRange()
else -> 0
})
}
override fun onImageLoadError(e: Exception) = onError(e)
override fun onImageLoaded() {
progressBar.isVisible = false
}
override fun onTileLoadError(e: Exception?) = Unit
override fun onPreviewReleased() = Unit
override fun onPreviewLoadError(e: Exception?) = Unit
private fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true
progressBar.isVisible = false
}
}

View File

@@ -79,7 +79,7 @@ class MangaSuggestionsProvider : SearchRecentSuggestionsProvider() {
}
@JvmStatic
fun getItemsCount(context: Context) = getCursor(context)?.count ?: 0
fun getItemsCount(context: Context) = getCursor(context)?.use { it.count } ?: 0
@JvmStatic
private fun getCursor(context: Context): Cursor? {

View File

@@ -37,6 +37,11 @@ class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
}
}
override fun onDestroy() {
searchView.suggestionsAdapter?.changeCursor(null) //close cursor
super.onDestroy()
}
override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) {
title = query

View File

@@ -8,17 +8,20 @@ import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ext.safe
import java.io.Closeable
object SearchHelper {
@JvmStatic
fun setupSearchView(menuItem: MenuItem) {
val view = menuItem.actionView as? SearchView ?: return
fun setupSearchView(menuItem: MenuItem): Closeable? {
val view = menuItem.actionView as? SearchView ?: return null
val context = view.context
val adapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
view.queryHint = context.getString(R.string.search_manga)
view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
view.suggestionsAdapter = adapter
view.setOnQueryTextListener(QueryListener(context))
view.setOnSuggestionListener(SuggestionListener(view))
return adapter?.cursor
}
private class QueryListener(private val context: Context) :

View File

@@ -67,10 +67,15 @@ class AppUpdateChecker(private val activity: ComponentActivity) : KoinComponent
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))
appendln()
append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize)))
appendln()
appendln()
appendLine()
append(
activity.getString(
R.string.size_s,
FileSizeUtils.formatBytes(activity, version.apkSize)
)
)
appendLine()
appendLine()
append(version.description)
})
.setPositiveButton(R.string.download) { _, _ ->

View File

@@ -3,8 +3,6 @@ package org.koitharu.kotatsu.ui.settings
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.transition.Slide
import android.view.Gravity
import androidx.fragment.app.Fragment
import androidx.fragment.app.commit
import androidx.preference.Preference
@@ -23,9 +21,7 @@ class SettingsActivity : BaseActivity(),
if (supportFragmentManager.findFragmentById(R.id.container) == null) {
supportFragmentManager.commit {
replace(R.id.container, MainSettingsFragment().also {
it.exitTransition = Slide(Gravity.START)
})
replace(R.id.container, MainSettingsFragment())
}
}
}
@@ -49,10 +45,9 @@ class SettingsActivity : BaseActivity(),
}
private fun openFragment(fragment: Fragment) {
fragment.enterTransition = Slide(Gravity.END)
fragment.exitTransition = Slide(Gravity.START)
supportFragmentManager.commit {
replace(R.id.container, fragment)
setReorderingAllowed(true)
addToBackStack(null)
}
}

View File

@@ -17,17 +17,13 @@ class SourceSettingsFragment : PreferenceFragmentCompat(), KoinComponent {
requireArguments().getParcelable<MangaSource>(EXTRA_SOURCE)!!
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
preferenceManager.sharedPreferencesName = source.name
}
override fun onResume() {
super.onResume()
activity?.title = source.title
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceManager.sharedPreferencesName = source.name
val repo = MangaProviderFactory.create(source) as? RemoteMangaRepository ?: return
val keys = repo.onCreatePreferences().map(::getString)
addPreferencesFromResource(R.xml.pref_source)

View File

@@ -10,7 +10,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.work.*
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
@@ -143,7 +143,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
setNumber(newChapters.size)
setLargeIcon(
Coil.execute(
GetRequestBuilder(applicationContext)
ImageRequest.Builder(applicationContext)
.data(manga.coverUrl)
.build()
).toBitmapOrNull()
@@ -222,5 +222,17 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request)
}
fun startNow(context: Context) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<TrackWorker>()
.setConstraints(constraints)
.addTag(TAG)
.build()
WorkManager.getInstance(context)
.enqueue(request)
}
}
}

View File

@@ -5,7 +5,7 @@ import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
@@ -38,9 +38,11 @@ class RecentListFactory(private val context: Context) : RemoteViewsService.Remot
val item = dataSet[position]
try {
val cover = runBlocking {
Coil.execute(GetRequestBuilder(context)
Coil.execute(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()).requireBitmap()
.build()
).requireBitmap()
}
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {

View File

@@ -5,7 +5,7 @@ import android.content.Intent
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga
@@ -49,9 +49,11 @@ class ShelfListFactory(private val context: Context, widgetId: Int) : RemoteView
views.setTextViewText(R.id.textView_title, item.title)
try {
val cover = runBlocking {
Coil.execute(GetRequestBuilder(context)
Coil.execute(
ImageRequest.Builder(context)
.data(item.coverUrl)
.build()).requireBitmap()
.build()
).requireBitmap()
}
views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) {

View File

@@ -10,7 +10,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import coil.Coil
import coil.request.GetRequestBuilder
import coil.request.ImageRequest
import coil.size.PixelSize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@@ -40,7 +40,7 @@ class MangaShortcut(private val manga: Manga) {
}
builder.setRank(1)
if (shortcuts.isNotEmpty() && shortcuts.size >= limit) {
manager.removeDynamicShortcuts(listOf(shortcuts.minBy { it.rank }!!.id))
manager.removeDynamicShortcuts(listOf(shortcuts.minByOrNull { it.rank }!!.id))
}
manager.addDynamicShortcuts(listOf(builder.build().toShortcutInfo()))
}
@@ -66,9 +66,11 @@ class MangaShortcut(private val manga: Manga) {
val icon = safe {
val size = getIconSize(context)
withContext(Dispatchers.IO) {
val bmp = Coil.execute(GetRequestBuilder(context)
val bmp = Coil.execute(
ImageRequest.Builder(context)
.data(manga.coverUrl)
.build()).requireBitmap()
.build()
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, size.width, size.height, 0)
}
}
@@ -78,7 +80,7 @@ class MangaShortcut(private val manga: Manga) {
.setLongLabel(manga.title)
.setIcon(icon?.let {
IconCompat.createWithAdaptiveBitmap(it)
} ?: IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground))
} ?: IconCompat.createWithResource(context, R.drawable.ic_shortcut_default))
.setIntent(
MangaDetailsActivity.newIntent(context, manga.id)
.setAction(MangaDetailsActivity.ACTION_MANGA_VIEW)

View File

@@ -0,0 +1,53 @@
package org.koitharu.kotatsu.utils
import android.app.Activity
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.database.ContentObserver
import android.os.Handler
import android.provider.Settings
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.sendBlocking
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onStart
class ScreenOrientationHelper(private val activity: Activity) {
val isAutoRotationEnabled: Boolean
get() = Settings.System.getInt(
activity.contentResolver,
Settings.System.ACCELEROMETER_ROTATION,
0
) == 1
var isLandscape: Boolean
get() = activity.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
set(value) {
activity.requestedOrientation = if (value) {
ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
}
}
fun toggleOrientation() {
isLandscape = !isLandscape
}
fun observeAutoOrientation() = callbackFlow<Boolean> {
val observer = object : ContentObserver(Handler(activity.mainLooper)) {
override fun onChange(selfChange: Boolean) {
sendBlocking(isAutoRotationEnabled)
}
}
activity.contentResolver.registerContentObserver(
Settings.System.CONTENT_URI, true, observer
)
awaitClose {
activity.contentResolver.unregisterContentObserver(observer)
}
}.onStart {
emit(isAutoRotationEnabled)
}.distinctUntilChanged()
}

View File

@@ -2,15 +2,15 @@ package org.koitharu.kotatsu.utils.ext
import androidx.core.graphics.drawable.toBitmap
import coil.request.ErrorResult
import coil.request.RequestResult
import coil.request.ImageResult
import coil.request.SuccessResult
fun RequestResult.requireBitmap() = when(this) {
fun ImageResult.requireBitmap() = when (this) {
is SuccessResult -> drawable.toBitmap()
is ErrorResult -> throw throwable
}
fun RequestResult.toBitmapOrNull() = when(this) {
fun ImageResult.toBitmapOrNull() = when (this) {
is SuccessResult -> try {
drawable.toBitmap()
} catch (_: Throwable) {

View File

@@ -7,7 +7,7 @@ import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import java.io.IOException
import java.net.SocketTimeoutException
inline fun <T, R> T.safe(action: T.() -> R?) = try {
this.action()
@@ -38,12 +38,8 @@ fun Throwable.getDisplayMessage(resources: Resources) = when (this) {
is UnsupportedOperationException -> resources.getString(R.string.operation_not_supported)
is UnsupportedFileException -> resources.getString(R.string.text_file_not_supported)
is EmptyHistoryException -> resources.getString(R.string.history_is_empty)
is IOException -> resources.getString(R.string.network_error)
else -> if (BuildConfig.DEBUG) {
message ?: resources.getString(R.string.error_occurred)
} else {
resources.getString(R.string.error_occurred)
}
is SocketTimeoutException -> resources.getString(R.string.network_error)
else -> message ?: resources.getString(R.string.error_occurred)
}
inline fun <T> measured(tag: String, block: () -> T): T {

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.io.IOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -38,3 +41,35 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
}
}
}
fun CoroutineScope.launchAfter(
job: Job?,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) {
try {
job?.join()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
block()
}
fun CoroutineScope.launchInstead(
job: Job?,
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = launch(context, start) {
try {
job?.cancelAndJoin()
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
block()
}

View File

@@ -1,12 +0,0 @@
package org.koitharu.kotatsu.utils.ext
import android.annotation.SuppressLint
import androidx.core.app.NotificationCompat
@SuppressLint("RestrictedApi")
fun NotificationCompat.Builder.clearActions(): NotificationCompat.Builder {
safe {
mActions.clear()
}
return this
}

View File

@@ -10,13 +10,14 @@ import org.jsoup.select.Elements
fun Response.parseHtml(): Document {
try {
val stream = body?.byteStream() ?: throw NullPointerException("Response body is null")
(body?.byteStream() ?: throw NullPointerException("Response body is null")).use { stream ->
val charset = body!!.contentType()?.charset()?.name()
return Jsoup.parse(
stream,
charset,
request.url.toString()
)
}
} finally {
closeQuietly()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 403 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<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="#000"
android:pathData="M13,5V11H14.17L12,13.17L9.83,11H11V5H13M15,3H9V9H5L12,16L19,9H15V3M19,18H5V20H19V18Z" />
</vector>

View File

@@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:tint="#0D47A1"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="0.40188664"
android:scaleY="0.40188664"
android:translateX="32.90095"
android:translateY="18.7272">
<group android:translateY="139.39206">
<path
android:fillColor="#0D47A1"
android:pathData="M83.796875,-0L105.6875,-0L60.765625,-55.828125L103.09375,-101L82.078125,-101L32.25,-49.1875L32.25,-101L13.53125,-101L13.53125,-0L32.25,-0L32.25,-25.8125L48.234375,-42.265625L83.796875,-0Z" />
</group>
</group>
</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="?android:textColorPrimary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.48,2.52c3.27,1.55 5.61,4.72 5.97,8.48h1.5C23.44,4.84 18.29,0 12,0l-0.66,0.03 3.81,3.81 1.33,-1.32zM10.23,1.75c-0.59,-0.59 -1.54,-0.59 -2.12,0L1.75,8.11c-0.59,0.59 -0.59,1.54 0,2.12l12.02,12.02c0.59,0.59 1.54,0.59 2.12,0l6.36,-6.36c0.59,-0.59 0.59,-1.54 0,-2.12L10.23,1.75zM14.83,21.19L2.81,9.17l6.36,-6.36 12.02,12.02 -6.36,6.36zM7.52,21.48C4.25,19.94 1.91,16.76 1.55,13L0.05,13C0.56,19.16 5.71,24 12,24l0.66,-0.03 -3.81,-3.81 -1.33,1.32z" />
</vector>

View File

@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer"
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:id="@+id/drawer"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.list.MainActivity"
@@ -41,12 +41,12 @@
android:layout_margin="16dp"
android:src="@drawable/ic_read_fill"
android:visibility="gone"
app:fabSize="normal"
app:backgroundTint="?colorAccent"
app:fabSize="normal"
app:layout_anchor="@id/container"
app:layout_anchorGravity="bottom|end"
app:layout_dodgeInsetEdges="bottom"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_dodgeInsetEdges="bottom"
tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
@@ -56,7 +56,6 @@
android:layout_width="260dp"
android:layout_height="match_parent"
android:layout_gravity="start"
app:headerLayout="@layout/view_nav_header"
app:menu="@menu/nav_drawer" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@@ -5,10 +5,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:colorControlNormal"
app:strokeColor="?colorOnSurface"
app:strokeWidth="1px">
<LinearLayout
@@ -30,7 +29,7 @@
android:gravity="center_vertical|start"
android:lines="2"
android:padding="6dp"
android:text="?android:textColorPrimary"
android:textColor="?android:textColorPrimary"
tools:text="@tools:sample/lorem" />
</LinearLayout>

View File

@@ -5,10 +5,9 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="@dimen/manga_list_details_item_height"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp"
app:cardMaxElevation="0dp"
app:strokeColor="?android:colorControlNormal"
app:strokeColor="?colorOnSurface"
app:strokeWidth="1px">
<LinearLayout

View File

@@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="92dp">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_launcher_foreground"
android:layout_alignParentStart="true"
android:layout_centerVertical="true" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="?android:listDivider"
android:layout_alignParentStart="true"
android:layout_alignParentEnd="true"
android:layout_alignParentBottom="true" />
</RelativeLayout>

View File

@@ -1,2 +1,12 @@
<?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">
<item
android:id="@+id/action_update"
android:orderInCategory="50"
android:title="@string/update"
app:showAsAction="never" />
</menu>

View File

@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<menu
xmlns:tools="http://schemas.android.com/tools"
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"
tools:ignore="AlwaysShowAction">
<item
android:id="@+id/action_screen_rotate"
android:icon="@drawable/ic_screen_rotation"
android:title="@string/rotate_screen"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/action_bookmark_add"
android:icon="@drawable/ic_bookmark_add"
@@ -12,12 +19,6 @@
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/action_save_page"
android:icon="@drawable/ic_page_image"
android:title="@string/save_page"
app:showAsAction="always" />
<item
android:id="@+id/action_pages_thumbs"
android:icon="@drawable/ic_grid"
@@ -30,10 +31,14 @@
android:title="@string/read_mode"
app:showAsAction="always" />
<item
android:id="@+id/action_save_page"
android:title="@string/save_page"
app:showAsAction="never" />
<item
android:id="@+id/action_settings"
android:icon="@drawable/ic_tune"
android:title="@string/settings"
app:showAsAction="always" />
app:showAsAction="never" />
</menu>

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<adaptive-icon
xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/blue_primary" />
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<background android:drawable="@color/blue_primary"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Some files were not shown because too many files have changed in this diff Show More