Compare commits

...

34 Commits

Author SHA1 Message Date
1c46fc7f23 Fix manga downloading 2020-09-30 20:36:53 +03:00
b7e4c6b8c0 New cbz write utility 2020-09-26 17:51:47 +03:00
df599e9d50 Fix MangaLib parser 2020-09-25 19:44:18 +03:00
6009f089e7 Update coil version 2020-09-25 19:23:57 +03:00
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
113 changed files with 865 additions and 531 deletions

View File

@@ -5,9 +5,9 @@ jdk:
android: android:
components: components:
- tools - tools
- platform-tools-29.0.6 - platform-tools-30.0.3
- build-tools-29.0.3 - build-tools-30.0.2
- android-29 - android-30
licenses: licenses:
- android-sdk-preview-license-.+ - android-sdk-preview-license-.+
- android-sdk-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 * Reading third-party comics from CBZ
* Standard and Webtoon-optimized reader * Standard and Webtoon-optimized reader
* Notifications about new chapters * Notifications about new chapters
### Coming Features
* Updates feed * Updates feed
* Global search * Global search
@@ -44,4 +41,4 @@ published by the Free Software Foundation, either version 3 of the License, or
### Disclaimer ### Disclaimer
The developers of this application does not have any affiliation with the content providers available. The developers of this application does not have any affiliation with the content providers available.

View File

@@ -8,15 +8,15 @@ plugins {
def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger() def gitCommits = 'git rev-list --count HEAD'.execute([], rootDir).text.trim().toInteger()
android { android {
compileSdkVersion 29 compileSdkVersion 30
buildToolsVersion '29.0.3' buildToolsVersion '30.0.2'
defaultConfig { defaultConfig {
applicationId 'org.koitharu.kotatsu' applicationId 'org.koitharu.kotatsu'
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 30
versionCode gitCommits versionCode gitCommits
versionName '0.5-b2' versionName '0.5.4'
kapt { kapt {
arguments { arguments {
@@ -28,10 +28,6 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8
} }
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
freeCompilerArgs += "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi"
}
buildTypes { buildTypes {
debug { debug {
applicationIdSuffix = '.debug' applicationIdSuffix = '.debug'
@@ -55,23 +51,31 @@ android {
androidExtensions { androidExtensions {
experimental = true 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 { 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.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.9'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
implementation 'androidx.core:core-ktx:1.5.0-alpha01' implementation 'androidx.core:core-ktx:1.5.0-alpha03'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06' implementation 'androidx.activity:activity-ktx:1.2.0-alpha08'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06' implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7' implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' 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.viewpager2:viewpager2:1.1.0-alpha01'
implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'androidx.preference:preference-ktx:1.1.1'
implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01' implementation 'androidx.work:work-runtime-ktx:2.4.0'
implementation 'com.google.android.material:material:1.3.0-alpha01' 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-runtime:2.2.5'
implementation 'androidx.room:room-ktx: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' 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.7.2' implementation 'com.squareup.okhttp3:okhttp:4.9.0'
implementation 'com.squareup.okio:okio:2.6.0' implementation 'com.squareup.okio:okio:2.8.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.2.0-beta-1'
implementation 'io.coil-kt:coil:0.11.0' implementation 'io.coil-kt:coil:1.0.0-rc3'
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

@@ -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 <service
android:name=".ui.download.DownloadService" android:name=".ui.download.DownloadService"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name=".ui.settings.AppUpdateService" />
<service <service
android:name=".ui.widget.shelf.ShelfWidgetService" android:name=".ui.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" /> android:permission="android.permission.BIND_REMOTEVIEWS" />

View File

@@ -1,13 +1,12 @@
package org.koitharu.kotatsu package org.koitharu.kotatsu
import android.app.Application import android.app.Application
import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.room.Room import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import coil.Coil import coil.Coil
import coil.ComponentRegistry import coil.ComponentRegistry
import coil.ImageLoaderBuilder import coil.ImageLoader
import coil.util.CoilUtils import coil.util.CoilUtils
import com.chuckerteam.chucker.api.ChuckerCollector import com.chuckerteam.chucker.api.ChuckerCollector
import com.chuckerteam.chucker.api.ChuckerInterceptor 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.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.core.logger.Level
import org.koin.dsl.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback import org.koitharu.kotatsu.core.db.DatabasePrePopulateCallback
import org.koitharu.kotatsu.core.db.MangaDatabase 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.PersistentCookieJar
import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache import org.koitharu.kotatsu.core.local.cookies.cache.SetCookieCache
import org.koitharu.kotatsu.core.local.cookies.persistence.SharedPrefsCookiePersistor 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.parser.UserAgentInterceptor
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaLoaderContext import org.koitharu.kotatsu.domain.MangaLoaderContext
@@ -46,6 +47,19 @@ class KotatsuApp : Application() {
override fun onCreate() { override fun onCreate() {
super.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() initKoin()
initCoil() initCoil()
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext)) Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
@@ -60,7 +74,7 @@ class KotatsuApp : Application() {
private fun initKoin() { private fun initKoin() {
startKoin { startKoin {
androidLogger() androidLogger(Level.ERROR)
androidContext(applicationContext) androidContext(applicationContext)
modules( modules(
module { module {
@@ -75,7 +89,7 @@ class KotatsuApp : Application() {
single { single {
MangaLoaderContext() MangaLoaderContext()
} }
factory { single {
AppSettings(applicationContext) AppSettings(applicationContext)
} }
single { single {
@@ -88,7 +102,7 @@ class KotatsuApp : Application() {
private fun initCoil() { private fun initCoil() {
Coil.setImageLoader( Coil.setImageLoader(
ImageLoaderBuilder(applicationContext) ImageLoader.Builder(applicationContext)
.okHttpClient( .okHttpClient(
okHttp() okHttp()
.cache(CoilUtils.createDefaultCache(applicationContext)) .cache(CoilUtils.createDefaultCache(applicationContext))

View File

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

View File

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

View File

@@ -0,0 +1,96 @@
package org.koitharu.kotatsu.core.local
import androidx.annotation.CheckResult
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
class WritableCbzFile(private val file: File) {
private val dir = File(file.parentFile, file.nameWithoutExtension)
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun prepare() = withContext(Dispatchers.IO) {
check(dir.list().isNullOrEmpty()) {
"Dir ${dir.name} is not empty"
}
if (!dir.exists()) {
dir.mkdir()
}
if (!file.exists()) {
return@withContext
}
ZipInputStream(FileInputStream(file)).use { zip ->
var entry = zip.nextEntry
while (entry != null) {
val target = File(dir.path + File.separator + entry.name)
target.parentFile?.mkdirs()
target.outputStream().use { out ->
zip.copyTo(out)
}
zip.closeEntry()
entry = zip.nextEntry
}
}
}
suspend fun cleanup() = withContext(Dispatchers.IO) {
dir.deleteRecursively()
}
@CheckResult
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun flush() = withContext(Dispatchers.IO) {
val tempFile = File(file.path + ".tmp")
if (tempFile.exists()) {
tempFile.delete()
}
try {
ZipOutputStream(FileOutputStream(tempFile)).use { zip ->
dir.listFiles()?.forEach {
zipFile(it, it.name, zip)
}
zip.flush()
}
tempFile.renameTo(file)
} finally {
if (tempFile.exists()) {
tempFile.delete()
}
}
}
operator fun get(name: String) = File(dir, name)
operator fun set(name: String, file: File) {
file.copyTo(this[name], overwrite = true)
}
companion object {
private fun zipFile(fileToZip: File, fileName: String, zipOut: ZipOutputStream) {
if (fileToZip.isDirectory) {
if (fileName.endsWith("/")) {
zipOut.putNextEntry(ZipEntry(fileName))
} else {
zipOut.putNextEntry(ZipEntry("$fileName/"))
}
zipOut.closeEntry()
fileToZip.listFiles()?.forEach { childFile ->
zipFile(childFile, "$fileName/${childFile.name}", zipOut)
}
} else {
FileInputStream(fileToZip).use { fis ->
val zipEntry = ZipEntry(fileName)
zipOut.putNextEntry(zipEntry)
fis.copyTo(zipOut)
}
}
}
}
}

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,7 @@
package org.koitharu.kotatsu.core.parser.site package org.koitharu.kotatsu.core.parser.site
import org.koitharu.kotatsu.core.exceptions.ParseException import org.koitharu.kotatsu.core.exceptions.ParseException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.*
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.domain.MangaLoaderContext import org.koitharu.kotatsu.domain.MangaLoaderContext
import org.koitharu.kotatsu.utils.ext.longHashCode import org.koitharu.kotatsu.utils.ext.longHashCode
import org.koitharu.kotatsu.utils.ext.parseHtml import org.koitharu.kotatsu.utils.ext.parseHtml
@@ -12,9 +9,25 @@ import org.koitharu.kotatsu.utils.ext.withDomain
class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) { class HenChanRepository(loaderContext: MangaLoaderContext) : ChanRepository(loaderContext) {
override val defaultDomain = "h-chan.me" override val defaultDomain = "hentaichan.pro"
override val source = MangaSource.HENCHAN 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 { override suspend fun getDetails(manga: Manga): Manga {
val domain = conf.getDomain(defaultDomain) val domain = conf.getDomain(defaultDomain)
val doc = loaderContext.httpGet(manga.url).parseHtml() val doc = loaderContext.httpGet(manga.url).parseHtml()

View File

@@ -95,7 +95,7 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
append("/c") append("/c")
append(item.getString("chapter_number")) append(item.getString("chapter_number"))
append('/') append('/')
append(item.getJSONArray("teams").getJSONObject(0).getString("slug")) append(item.getJSONArray("teams").getJSONObject(0).optString("slug"))
} }
var name = item.getString("chapter_name") var name = item.getString("chapter_name")
if (name.isNullOrBlank() || name == "null") { if (name.isNullOrBlank() || name == "null") {
@@ -142,12 +142,18 @@ open class MangaLibRepository(loaderContext: MangaLoaderContext) :
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> { override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
val doc = loaderContext.httpGet(chapter.url).parseHtml() val doc = loaderContext.httpGet(chapter.url).parseHtml()
val scripts = doc.head().select("script") val scripts = doc.head().select("script")
val pg = doc.body().getElementById("pg").html().substringAfter('=').substringBeforeLast(';') val pg = doc.body().getElementById("pg").html()
.substringAfter('=')
.substringBeforeLast(';')
val pages = JSONArray(pg) val pages = JSONArray(pg)
for (script in scripts) { for (script in scripts) {
val raw = script.html().trim() val raw = script.html().trim()
if (raw.startsWith("window.__info")) { if (raw.contains("window.__info")) {
val json = JSONObject(raw.substringAfter('=').substringBeforeLast(';')) val json = JSONObject(
raw.substringAfter("window.__info")
.substringAfter('=')
.substringBeforeLast(';')
)
val domain = json.getJSONObject("servers").run { val domain = json.getJSONObject("servers").run {
getStringOrNull("main") ?: getString( getStringOrNull("main") ?: getString(
json.getJSONObject("img").getString("server") json.getJSONObject("img").getString("server")

View File

@@ -37,7 +37,12 @@ class MangaTownRepository(loaderContext: MangaLoaderContext) : RemoteMangaReposi
} }
val page = (offset / 30) + 1 val page = (offset / 30) + 1
val url = when { 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" tag != null -> "$scheme://$domain/directory/${tag.key}/$page.htm$sortKey"
else -> "$scheme://$domain/directory/$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) { class ReadmangaRepository(loaderContext: MangaLoaderContext) : GroupleRepository(loaderContext) {
override val defaultDomain = "readmanga.me" override val defaultDomain = "readmanga.live"
override val source = MangaSource.READMANGA_RU override val source = MangaSource.READMANGA_RU
} }

View File

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

View File

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

View File

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

View File

@@ -1,63 +1,35 @@
package org.koitharu.kotatsu.domain.local package org.koitharu.kotatsu.domain.local
import androidx.annotation.CheckResult
import androidx.annotation.WorkerThread import androidx.annotation.WorkerThread
import org.koitharu.kotatsu.core.local.WritableCbzFile
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaChapter import org.koitharu.kotatsu.core.model.MangaChapter
import org.koitharu.kotatsu.utils.ext.sub
import org.koitharu.kotatsu.utils.ext.takeIfReadable import org.koitharu.kotatsu.utils.ext.takeIfReadable
import org.koitharu.kotatsu.utils.ext.toFileNameSafe import org.koitharu.kotatsu.utils.ext.toFileNameSafe
import java.io.File import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
@WorkerThread @WorkerThread
class MangaZip(val file: File) { class MangaZip(val file: File) {
private val dir = file.parentFile?.sub(file.name + ".tmp")?.takeIf { it.mkdir() } private val writableCbz = WritableCbzFile(file)
?: throw RuntimeException("Cannot create temporary directory")
private var index = MangaIndex(null) private var index = MangaIndex(null)
fun prepare(manga: Manga) { suspend fun prepare(manga: Manga) {
extract() writableCbz.prepare()
index = MangaIndex(dir.sub(INDEX_ENTRY).takeIfReadable()?.readText()) index = MangaIndex(writableCbz[INDEX_ENTRY].takeIfReadable()?.readText())
index.setMangaInfo(manga, append = true) index.setMangaInfo(manga, append = true)
} }
fun cleanup() { suspend fun cleanup() {
dir.deleteRecursively() writableCbz.cleanup()
} }
fun compress() { @CheckResult
dir.sub(INDEX_ENTRY).writeText(index.toString()) suspend fun compress(): Boolean {
ZipOutputStream(file.outputStream()).use { out -> writableCbz[INDEX_ENTRY].writeText(index.toString())
for (file in dir.listFiles().orEmpty()) { return writableCbz.flush()
val entry = ZipEntry(file.name)
out.putNextEntry(entry)
file.inputStream().use { stream ->
stream.copyTo(out)
}
out.closeEntry()
}
}
}
private fun extract() {
if (!file.exists()) {
return
}
ZipInputStream(file.inputStream()).use { input ->
while (true) {
val entry = input.nextEntry ?: return
if (!entry.isDirectory) {
dir.sub(entry.name).outputStream().use { out ->
input.copyTo(out)
}
}
input.closeEntry()
}
}
} }
fun addCover(file: File, ext: String) { fun addCover(file: File, ext: String) {
@@ -68,7 +40,7 @@ class MangaZip(val file: File) {
append(ext) append(ext)
} }
} }
file.copyTo(dir.sub(name), overwrite = true) writableCbz[name] = file
index.setCoverEntry(name) index.setCoverEntry(name)
} }
@@ -80,7 +52,7 @@ class MangaZip(val file: File) {
append(ext) append(ext)
} }
} }
file.copyTo(dir.sub(name), overwrite = true) writableCbz[name] = file
index.addChapter(chapter) index.addChapter(chapter)
} }

View File

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

View File

@@ -28,7 +28,7 @@ class ChapterHolder(parent: ViewGroup) :
} }
ChapterExtra.CURRENT -> { ChapterExtra.CURRENT -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_outline_accent) 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 -> { ChapterExtra.NEW -> {
textView_number.setBackgroundResource(R.drawable.bg_badge_accent) textView_number.setBackgroundResource(R.drawable.bg_badge_accent)

View File

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

View File

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

View File

@@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity import org.koitharu.kotatsu.ui.details.MangaDetailsActivity
import org.koitharu.kotatsu.utils.ext.clearActions
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import kotlin.math.roundToInt import kotlin.math.roundToInt
@@ -48,6 +47,7 @@ class DownloadNotification(private val context: Context) {
builder.setSmallIcon(android.R.drawable.stat_sys_download) builder.setSmallIcon(android.R.drawable.stat_sys_download)
builder.setLargeIcon(null) builder.setLargeIcon(null)
builder.setContentIntent(null) builder.setContentIntent(null)
builder.setStyle(null)
} }
fun setCancelId(startId: Int) { fun setCancelId(startId: Int) {
@@ -69,13 +69,15 @@ class DownloadNotification(private val context: Context) {
} }
fun setError(e: Throwable) { fun setError(e: Throwable) {
val message = e.getDisplayMessage(context.resources)
builder.setProgress(0, 0, false) builder.setProgress(0, 0, false)
builder.setSmallIcon(android.R.drawable.stat_notify_error) builder.setSmallIcon(android.R.drawable.stat_notify_error)
builder.setSubText(context.getString(R.string.error)) builder.setSubText(context.getString(R.string.error))
builder.setContentText(e.getDisplayMessage(context.resources)) builder.setContentText(message)
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setContentIntent(null) builder.setContentIntent(null)
builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setCategory(NotificationCompat.CATEGORY_ERROR)
builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
} }
fun setLargeIcon(icon: Drawable?) { fun setLargeIcon(icon: Drawable?) {
@@ -90,16 +92,19 @@ class DownloadNotification(private val context: Context) {
builder.setProgress(max, progress, false) builder.setProgress(max, progress, false)
builder.setContentText("%d%%".format(percent)) builder.setContentText("%d%%".format(percent))
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null)
} }
fun setWaitingForNetwork() { fun setWaitingForNetwork() {
builder.setProgress(0, 0, false) builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.waiting_for_network)) builder.setContentText(context.getString(R.string.waiting_for_network))
builder.setStyle(null)
} }
fun setPostProcessing() { fun setPostProcessing() {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.processing_)) builder.setContentText(context.getString(R.string.processing_))
builder.setStyle(null)
} }
fun setDone(manga: Manga) { fun setDone(manga: Manga) {
@@ -109,12 +114,14 @@ class DownloadNotification(private val context: Context) {
builder.setAutoCancel(true) builder.setAutoCancel(true)
builder.setSmallIcon(android.R.drawable.stat_sys_download_done) builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
builder.setCategory(null) builder.setCategory(null)
builder.setStyle(null)
} }
fun setCancelling() { fun setCancelling() {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.cancelling_)) builder.setContentText(context.getString(R.string.cancelling_))
builder.setContentIntent(null) builder.setContentIntent(null)
builder.setStyle(null)
} }
fun update(id: Int = NOTIFICATION_ID) { fun update(id: Int = NOTIFICATION_ID) {

View File

@@ -8,7 +8,7 @@ import android.webkit.MimeTypeMap
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import coil.Coil import coil.Coil
import coil.request.GetRequestBuilder import coil.request.ImageRequest
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
@@ -77,9 +77,9 @@ class DownloadService : BaseService() {
return launch(Dispatchers.IO) { return launch(Dispatchers.IO) {
mutex.lock() mutex.lock()
wakeLock.acquire(TimeUnit.HOURS.toMillis(1)) wakeLock.acquire(TimeUnit.HOURS.toMillis(1))
notification.fillFrom(manga)
notification.setCancelId(startId)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.fillFrom(manga)
notification.setCancelId(startId)
startForeground(DownloadNotification.NOTIFICATION_ID, notification()) startForeground(DownloadNotification.NOTIFICATION_ID, notification())
} }
val destination = settings.getStorageDir(this@DownloadService) val destination = settings.getStorageDir(this@DownloadService)
@@ -89,15 +89,13 @@ class DownloadService : BaseService() {
val repo = MangaProviderFactory.create(manga.source) val repo = MangaProviderFactory.create(manga.source)
val cover = safe { val cover = safe {
Coil.execute( Coil.execute(
GetRequestBuilder(this@DownloadService) ImageRequest.Builder(this@DownloadService)
.data(manga.coverUrl) .data(manga.coverUrl)
.build() .build()
).drawable ).drawable
} }
withContext(Dispatchers.Main) { notification.setLargeIcon(cover)
notification.setLargeIcon(cover) notification.update()
notification.update()
}
val data = if (manga.chapters == null) repo.getDetails(manga) else manga val data = if (manga.chapters == null) repo.getDetails(manga) else manga
output = MangaZip.findInDir(destination, data) output = MangaZip.findInDir(destination, data)
output.prepare(data) output.prepare(data)
@@ -141,31 +139,30 @@ class DownloadService : BaseService() {
} }
} }
} }
withContext(Dispatchers.Main) { notification.setCancelId(0)
notification.setCancelId(0) notification.setPostProcessing()
notification.setPostProcessing() notification.update()
notification.update() if (!output.compress()) {
throw RuntimeException("Cannot create target file")
} }
output.compress()
val result = MangaProviderFactory.createLocal().getFromFile(output.file) val result = MangaProviderFactory.createLocal().getFromFile(output.file)
withContext(Dispatchers.Main) { notification.setDone(result)
notification.setDone(result) notification.dismiss()
notification.dismiss() notification.update(manga.id.toInt().absoluteValue)
notification.update(manga.id.toInt().absoluteValue)
}
} catch (_: CancellationException) { } catch (_: CancellationException) {
withContext(Dispatchers.Main + NonCancellable) { withContext(NonCancellable) {
notification.setCancelling() notification.setCancelling()
notification.setCancelId(0) notification.setCancelId(0)
notification.update() notification.update()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
withContext(Dispatchers.Main) { if (BuildConfig.DEBUG) {
notification.setError(e) e.printStackTrace()
notification.setCancelId(0)
notification.dismiss()
notification.update(manga.id.toInt().absoluteValue)
} }
notification.setError(e)
notification.setCancelId(0)
notification.dismiss()
notification.update(manga.id.toInt().absoluteValue)
} finally { } finally {
withContext(NonCancellable) { withContext(NonCancellable) {
jobs.remove(startId) 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.ui.tracker.TrackWorker
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.resolveDp import org.koitharu.kotatsu.utils.ext.resolveDp
import java.io.Closeable
class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener, class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener,
SharedPreferences.OnSharedPreferenceChangeListener, MainView { SharedPreferences.OnSharedPreferenceChangeListener, MainView {
@@ -45,6 +46,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private lateinit var drawerToggle: ActionBarDrawerToggle private lateinit var drawerToggle: ActionBarDrawerToggle
private var closeable: Closeable? = null
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -74,6 +76,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} }
override fun onDestroy() { override fun onDestroy() {
closeable?.close()
settings.unsubscribe(this) settings.unsubscribe(this)
super.onDestroy() super.onDestroy()
} }
@@ -89,10 +92,18 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
drawerToggle.onConfigurationChanged(newConfig) drawerToggle.onConfigurationChanged(newConfig)
} }
override fun onBackPressed() {
if (drawer.isDrawerOpen(navigationView)) {
drawer.closeDrawer(navigationView)
} else {
super.onBackPressed()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean { override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.opt_main, menu) menuInflater.inflate(R.menu.opt_main, menu)
menu.findItem(R.id.action_search)?.let { menuItem -> menu.findItem(R.id.action_search)?.let { menuItem ->
SearchHelper.setupSearchView(menuItem) closeable = SearchHelper.setupSearchView(menuItem)
} }
return super.onCreateOptionsMenu(menu) return super.onCreateOptionsMenu(menu)
} }

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.list package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear import coil.clear
import coil.api.load import coil.load
import kotlinx.android.synthetic.main.item_manga_grid.* import kotlinx.android.synthetic.main.item_manga_grid.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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.annotation.SuppressLint
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.api.clear import coil.clear
import coil.api.load import coil.load
import kotlinx.android.synthetic.main.item_manga_list_details.* import kotlinx.android.synthetic.main.item_manga_list_details.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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() { final override fun onRefresh() {
swipeRefreshLayout.isRefreshing = true
onRequestMoreItems(0) onRequestMoreItems(0)
} }
@@ -188,10 +189,11 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
override fun onLoadingStateChanged(isLoading: Boolean) { override fun onLoadingStateChanged(isLoading: Boolean) {
val hasItems = recyclerView.hasItems val hasItems = recyclerView.hasItems
progressBar.isVisible = isLoading && !hasItems progressBar.isVisible = isLoading && !hasItems
swipeRefreshLayout.isRefreshing = isLoading && hasItems
swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible swipeRefreshLayout.isEnabled = isSwipeRefreshEnabled && !progressBar.isVisible
if (isLoading) { if (isLoading) {
layout_holder.isVisible = false layout_holder.isVisible = false
} else {
swipeRefreshLayout.isRefreshing = false
} }
} }

View File

@@ -1,8 +1,8 @@
package org.koitharu.kotatsu.ui.list package org.koitharu.kotatsu.ui.list
import android.view.ViewGroup import android.view.ViewGroup
import coil.api.clear import coil.clear
import coil.api.load import coil.load
import kotlinx.android.synthetic.main.item_manga_list.* import kotlinx.android.synthetic.main.item_manga_list.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga 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.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem
import android.view.View import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.google.android.material.snackbar.Snackbar 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.PaginationScrollListener
import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration import org.koitharu.kotatsu.ui.common.list.decor.SpacingItemDecoration
import org.koitharu.kotatsu.ui.details.MangaDetailsActivity 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.callOnScrollListeners
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import org.koitharu.kotatsu.utils.ext.hasItems 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) 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() { override fun onDestroyView() {
adapter = null adapter = null
super.onDestroyView() super.onDestroyView()

View File

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

View File

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

View File

@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.ui.reader package org.koitharu.kotatsu.ui.reader
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.util.ArrayMap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@@ -16,9 +21,10 @@ import kotlin.coroutines.CoroutineContext
class PageLoader : KoinComponent, CoroutineScope, DisposableHandle { class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
private val job = SupervisorJob() 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 okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
private val convertLock = Mutex()
override val coroutineContext: CoroutineContext override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job get() = Dispatchers.Main + job
@@ -30,7 +36,7 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
return it 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() return (task ?: loadAsync(url).also { tasks[url] = it }).await()
} }
@@ -48,10 +54,14 @@ class PageLoader : KoinComponent, CoroutineScope, DisposableHandle {
val request = Request.Builder() val request = Request.Builder()
.url(url) .url(url)
.get() .get()
.header("Accept", "image/webp,image/png;q=0.9,image/jpeg,*/*;q=0.8")
.cacheControl(CacheUtils.CONTROL_DISABLED) .cacheControl(CacheUtils.CONTROL_DISABLED)
.build() .build()
okHttp.newCall(request).await().use { response -> okHttp.newCall(request).await().use { response ->
val body = response.body val body = response.body
check(response.isSuccessful) {
"Invalid response: ${response.code} ${response.message}"
}
checkNotNull(body) { checkNotNull(body) {
"Null response" "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() { override fun dispose() {
coroutineContext.cancel() coroutineContext.cancel()
tasks.clear() tasks.clear()

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.reader package org.koitharu.kotatsu.ui.reader
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
@@ -17,10 +18,13 @@ import androidx.core.view.isVisible
import androidx.core.view.postDelayed import androidx.core.view.postDelayed
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import androidx.fragment.app.commit import androidx.fragment.app.commit
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.*
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moxy.MvpDelegate import moxy.MvpDelegate
import moxy.ktx.moxyPresenter 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.ui.reader.wetoon.WebtoonReaderFragment
import org.koitharu.kotatsu.utils.GridTouchHelper import org.koitharu.kotatsu.utils.GridTouchHelper
import org.koitharu.kotatsu.utils.MangaShortcut import org.koitharu.kotatsu.utils.MangaShortcut
import org.koitharu.kotatsu.utils.ScreenOrientationHelper
import org.koitharu.kotatsu.utils.ShareHelper import org.koitharu.kotatsu.utils.ShareHelper
import org.koitharu.kotatsu.utils.anim.Motion import org.koitharu.kotatsu.utils.anim.Motion
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
@@ -56,6 +61,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
private set private set
private lateinit var touchHelper: GridTouchHelper private lateinit var touchHelper: GridTouchHelper
private lateinit var orientationHelper: ScreenOrientationHelper
private var isTapSwitchEnabled = true private var isTapSwitchEnabled = true
private var isVolumeKeysSwitchEnabled = false private var isVolumeKeysSwitchEnabled = false
@@ -67,6 +73,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
setContentView(R.layout.activity_reader) setContentView(R.layout.activity_reader)
supportActionBar?.setDisplayHomeAsUpEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true)
touchHelper = GridTouchHelper(this, this) touchHelper = GridTouchHelper(this, this)
orientationHelper = ScreenOrientationHelper(this)
toolbar_bottom.inflateMenu(R.menu.opt_reader_bottom) toolbar_bottom.inflateMenu(R.menu.opt_reader_bottom)
toolbar_bottom.setOnMenuItemClickListener(::onOptionsItemSelected) toolbar_bottom.setOnMenuItemClickListener(::onOptionsItemSelected)
@@ -89,6 +96,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
settings.subscribe(this) settings.subscribe(this)
loadSettings() 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) { if (savedInstanceState?.containsKey(MvpDelegate.MOXY_DELEGATE_TAGS_KEY) != true) {
presenter.init(state.manga) presenter.init(state.manga)
@@ -165,6 +176,10 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
) )
true true
} }
R.id.action_screen_rotate -> {
orientationHelper.toggleOrientation()
true
}
R.id.action_pages_thumbs -> { R.id.action_pages_thumbs -> {
if (reader?.hasItems == true) { if (reader?.hasItems == true) {
val pages = reader?.getPages() val pages = reader?.getPages()
@@ -325,14 +340,18 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
presenter.setMode(state.manga, mode) presenter.setMode(state.manga, mode)
} }
@SuppressLint("ShowToast")
override fun onPageSaved(uri: Uri?) { override fun onPageSaved(uri: Uri?) {
if (uri != null) { if (uri != null) {
Snackbar.make(container, R.string.page_saved, Snackbar.LENGTH_LONG) Snackbar.make(container, R.string.page_saved, Snackbar.LENGTH_LONG)
.setAnchorView(appbar_bottom)
.setAction(R.string.share) { .setAction(R.string.share) {
ShareHelper.shareImage(this, uri) ShareHelper.shareImage(this, uri)
}.show() }.show()
} else { } 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 { override fun onApplyWindowInsets(v: View, insets: WindowInsets): WindowInsets {
appbar_top.updatePadding(top = insets.systemWindowInsetTop) appbar_top.updatePadding(
appbar_bottom.updatePadding(bottom = insets.systemWindowInsetBottom) top = insets.systemWindowInsetTop,
right = insets.systemWindowInsetRight,
left = insets.systemWindowInsetLeft
)
appbar_bottom.updatePadding(
bottom = insets.systemWindowInsetBottom,
right = insets.systemWindowInsetRight,
left = insets.systemWindowInsetLeft
)
return insets.consumeSystemWindowInsets() 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 package org.koitharu.kotatsu.ui.reader.standard
import android.net.Uri
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page.* import kotlinx.android.synthetic.main.item_page.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage 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.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage 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), 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 { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener { button_retry.setOnClickListener(this)
doLoad(boundData ?: return@setOnClickListener, force = true)
}
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false) delegate.onBind(data)
} }
override fun onRecycled() { override fun onRecycled() {
job?.cancel() delegate.onRecycle()
ssiv.recycle() ssiv.recycle()
} }
private fun doLoad(data: MangaPage, force: Boolean) { override fun onLoadingStarted() {
job?.cancel() layout_error.isVisible = false
job = launch { progressBar.isVisible = true
layout_error.isVisible = false ssiv.recycle()
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() { override fun onImageReady(uri: Uri) {
ssiv.maxScale = 2f * maxOf(ssiv.width / ssiv.sWidth.toFloat(), ssiv.height / ssiv.sHeight.toFloat()) ssiv.setImage(ImageSource.uri(uri))
}
override fun onImageShowing() {
ssiv.maxScale = 2f * maxOf(
ssiv.width / ssiv.sWidth.toFloat(),
ssiv.height / ssiv.sHeight.toFloat()
)
ssiv.resetScaleAndCenter() ssiv.resetScaleAndCenter()
} }
override fun onImageLoadError(e: Exception) = onError(e) override fun onImageShown() {
override fun onImageLoaded() {
progressBar.isVisible = false 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 onError(e: Throwable) {
override fun onPreviewLoadError(e: Exception?) = Unit
private fun onError(e: Throwable) {
textView_error.text = e.getDisplayMessage(context.resources) textView_error.text = e.getDisplayMessage(context.resources)
layout_error.isVisible = true layout_error.isVisible = true
progressBar.isVisible = false progressBar.isVisible = false

View File

@@ -3,7 +3,7 @@ package org.koitharu.kotatsu.ui.reader.thumbnails
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri import androidx.core.net.toUri
import coil.Coil import coil.Coil
import coil.request.GetRequestBuilder import coil.request.ImageRequest
import coil.size.PixelSize import coil.size.PixelSize
import coil.size.Size import coil.size.Size
import kotlinx.android.synthetic.main.item_page_thumb.* import kotlinx.android.synthetic.main.item_page_thumb.*
@@ -18,7 +18,7 @@ class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope)
BaseViewHolder<MangaPage, PagesCache>(parent, R.layout.item_page_thumb) { BaseViewHolder<MangaPage, PagesCache>(parent, R.layout.item_page_thumb) {
private var job: Job? = null private var job: Job? = null
private val thumbSize: Size private val thumbSize: Size
init { init {
val width = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width) val width = itemView.context.resources.getDimensionPixelSize(R.dimen.preferred_grid_width)
@@ -38,10 +38,12 @@ class PageThumbnailHolder(parent: ViewGroup, private val scope: CoroutineScope)
val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data) val pageUrl = MangaProviderFactory.create(data.source).getPageFullUrl(data)
extra[pageUrl]?.toUri()?.toString() ?: pageUrl extra[pageUrl]?.toUri()?.toString() ?: pageUrl
} }
val drawable = Coil.execute(GetRequestBuilder(context) val drawable = Coil.execute(
.data(url) ImageRequest.Builder(context)
.size(thumbSize) .data(url)
.build()).drawable .size(thumbSize)
.build()
).drawable
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
imageView_thumb.setImageDrawable(drawable) imageView_thumb.setImageDrawable(drawable)
} }

View File

@@ -1,64 +1,80 @@
package org.koitharu.kotatsu.ui.reader.wetoon package org.koitharu.kotatsu.ui.reader.wetoon
import android.net.Uri
import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import kotlinx.android.synthetic.main.item_page_webtoon.* import kotlinx.android.synthetic.main.item_page_webtoon.*
import kotlinx.coroutines.*
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.MangaPage 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.common.list.BaseViewHolder
import org.koitharu.kotatsu.ui.reader.PageLoader import org.koitharu.kotatsu.ui.reader.PageLoader
import org.koitharu.kotatsu.ui.reader.base.PageHolderDelegate
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) : class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
BaseViewHolder<MangaPage, Unit>(parent, R.layout.item_page_webtoon), 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 private var scrollToRestore = 0
init { init {
ssiv.setOnImageEventListener(this) ssiv.setOnImageEventListener(delegate)
button_retry.setOnClickListener { button_retry.setOnClickListener(this)
doLoad(boundData ?: return@setOnClickListener, force = true)
}
} }
override fun onBind(data: MangaPage, extra: Unit) { override fun onBind(data: MangaPage, extra: Unit) {
doLoad(data, force = false) delegate.onBind(data)
}
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)
}
}
} }
override fun onRecycled() { override fun onRecycled() {
job?.cancel() delegate.onRecycle()
ssiv.recycle() 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 getScrollY() = ssiv.getScroll()
fun restoreScroll(scroll: Int) { fun restoreScroll(scroll: Int) {
@@ -68,33 +84,4 @@ class WebtoonHolder(parent: ViewGroup, private val loader: PageLoader) :
scrollToRestore = scroll 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 @JvmStatic
fun getItemsCount(context: Context) = getCursor(context)?.count ?: 0 fun getItemsCount(context: Context) = getCursor(context)?.use { it.count } ?: 0
@JvmStatic @JvmStatic
private fun getCursor(context: Context): Cursor? { 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 { override fun onQueryTextSubmit(query: String?): Boolean {
return if (!query.isNullOrBlank()) { return if (!query.isNullOrBlank()) {
title = query title = query

View File

@@ -8,17 +8,20 @@ import androidx.appcompat.widget.SearchView
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity import org.koitharu.kotatsu.ui.search.global.GlobalSearchActivity
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import java.io.Closeable
object SearchHelper { object SearchHelper {
@JvmStatic @JvmStatic
fun setupSearchView(menuItem: MenuItem) { fun setupSearchView(menuItem: MenuItem): Closeable? {
val view = menuItem.actionView as? SearchView ?: return val view = menuItem.actionView as? SearchView ?: return null
val context = view.context val context = view.context
val adapter = MangaSuggestionsProvider.getSuggestionAdapter(context)
view.queryHint = context.getString(R.string.search_manga) view.queryHint = context.getString(R.string.search_manga)
view.suggestionsAdapter = MangaSuggestionsProvider.getSuggestionAdapter(context) view.suggestionsAdapter = adapter
view.setOnQueryTextListener(QueryListener(context)) view.setOnQueryTextListener(QueryListener(context))
view.setOnSuggestionListener(SuggestionListener(view)) view.setOnSuggestionListener(SuggestionListener(view))
return adapter?.cursor
} }
private class QueryListener(private val context: Context) : 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) .setTitle(R.string.app_update_available)
.setMessage(buildString { .setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name)) append(activity.getString(R.string.new_version_s, version.name))
appendln() appendLine()
append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize))) append(
appendln() activity.getString(
appendln() R.string.size_s,
FileSizeUtils.formatBytes(activity, version.apkSize)
)
)
appendLine()
appendLine()
append(version.description) append(version.description)
}) })
.setPositiveButton(R.string.download) { _, _ -> .setPositiveButton(R.string.download) { _, _ ->

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.work.* import androidx.work.*
import coil.Coil import coil.Coil
import coil.request.GetRequestBuilder import coil.request.ImageRequest
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
@@ -143,7 +143,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
setNumber(newChapters.size) setNumber(newChapters.size)
setLargeIcon( setLargeIcon(
Coil.execute( Coil.execute(
GetRequestBuilder(applicationContext) ImageRequest.Builder(applicationContext)
.data(manga.coverUrl) .data(manga.coverUrl)
.build() .build()
).toBitmapOrNull() ).toBitmapOrNull()
@@ -222,5 +222,17 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
WorkManager.getInstance(context) WorkManager.getInstance(context)
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.KEEP, request) .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,8 @@ import android.content.Intent
import android.widget.RemoteViews import android.widget.RemoteViews
import android.widget.RemoteViewsService import android.widget.RemoteViewsService
import coil.Coil import coil.Coil
import coil.request.GetRequestBuilder import coil.executeBlocking
import coil.request.ImageRequest
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
@@ -37,11 +38,11 @@ class RecentListFactory(private val context: Context) : RemoteViewsService.Remot
val views = RemoteViews(context.packageName, R.layout.item_recent) val views = RemoteViews(context.packageName, R.layout.item_recent)
val item = dataSet[position] val item = dataSet[position]
try { try {
val cover = runBlocking { val cover = Coil.imageLoader(context).executeBlocking(
Coil.execute(GetRequestBuilder(context) ImageRequest.Builder(context)
.data(item.coverUrl) .data(item.coverUrl)
.build()).requireBitmap() .build()
} ).requireBitmap()
views.setImageViewBitmap(R.id.imageView_cover, cover) views.setImageViewBitmap(R.id.imageView_cover, cover)
} catch (e: IOException) { } catch (e: IOException) {
views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder) views.setImageViewResource(R.id.imageView_cover, R.drawable.ic_placeholder)

View File

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

View File

@@ -10,7 +10,7 @@ import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.drawable.IconCompat
import coil.Coil import coil.Coil
import coil.request.GetRequestBuilder import coil.request.ImageRequest
import coil.size.PixelSize import coil.size.PixelSize
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -40,7 +40,7 @@ class MangaShortcut(private val manga: Manga) {
} }
builder.setRank(1) builder.setRank(1)
if (shortcuts.isNotEmpty() && shortcuts.size >= limit) { 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())) manager.addDynamicShortcuts(listOf(builder.build().toShortcutInfo()))
} }
@@ -66,9 +66,11 @@ class MangaShortcut(private val manga: Manga) {
val icon = safe { val icon = safe {
val size = getIconSize(context) val size = getIconSize(context)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val bmp = Coil.execute(GetRequestBuilder(context) val bmp = Coil.execute(
.data(manga.coverUrl) ImageRequest.Builder(context)
.build()).requireBitmap() .data(manga.coverUrl)
.build()
).requireBitmap()
ThumbnailUtils.extractThumbnail(bmp, size.width, size.height, 0) ThumbnailUtils.extractThumbnail(bmp, size.width, size.height, 0)
} }
} }
@@ -78,7 +80,7 @@ class MangaShortcut(private val manga: Manga) {
.setLongLabel(manga.title) .setLongLabel(manga.title)
.setIcon(icon?.let { .setIcon(icon?.let {
IconCompat.createWithAdaptiveBitmap(it) IconCompat.createWithAdaptiveBitmap(it)
} ?: IconCompat.createWithResource(context, R.drawable.ic_launcher_foreground)) } ?: IconCompat.createWithResource(context, R.drawable.ic_shortcut_default))
.setIntent( .setIntent(
MangaDetailsActivity.newIntent(context, manga.id) MangaDetailsActivity.newIntent(context, manga.id)
.setAction(MangaDetailsActivity.ACTION_MANGA_VIEW) .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 androidx.core.graphics.drawable.toBitmap
import coil.request.ErrorResult import coil.request.ErrorResult
import coil.request.RequestResult import coil.request.ImageResult
import coil.request.SuccessResult import coil.request.SuccessResult
fun RequestResult.requireBitmap() = when(this) { fun ImageResult.requireBitmap() = when (this) {
is SuccessResult -> drawable.toBitmap() is SuccessResult -> drawable.toBitmap()
is ErrorResult -> throw throwable is ErrorResult -> throw throwable
} }
fun RequestResult.toBitmapOrNull() = when(this) { fun ImageResult.toBitmapOrNull() = when (this) {
is SuccessResult -> try { is SuccessResult -> try {
drawable.toBitmap() drawable.toBitmap()
} catch (_: Throwable) { } catch (_: Throwable) {

View File

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

View File

@@ -1,12 +1,15 @@
package org.koitharu.kotatsu.utils.ext package org.koitharu.kotatsu.utils.ext
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call import okhttp3.Call
import okhttp3.Callback import okhttp3.Callback
import okhttp3.Response import okhttp3.Response
import org.koitharu.kotatsu.BuildConfig
import java.io.IOException import java.io.IOException
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@@ -37,4 +40,36 @@ fun <T> Flow<T>.onFirst(action: suspend (T) -> Unit): Flow<T> {
isFirstCall = false isFirstCall = false
} }
} }
}
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 { fun Response.parseHtml(): Document {
try { 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() val charset = body!!.contentType()?.charset()?.name()
return Jsoup.parse( return Jsoup.parse(
stream, stream,
charset, charset,
request.url.toString() request.url.toString()
) )
}
} finally { } finally {
closeQuietly() 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,62 +1,61 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout <androidx.drawerlayout.widget.DrawerLayout
android:id="@+id/drawer" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
xmlns:tools="http://schemas.android.com/tools" android:id="@+id/drawer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context=".ui.list.MainActivity" tools:context=".ui.list.MainActivity"
tools:openDrawer="start"> tools:openDrawer="start">
<androidx.coordinatorlayout.widget.CoordinatorLayout <androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout <com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar" android:id="@+id/appbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?colorPrimary" android:background="?colorPrimary"
android:theme="@style/AppToolbarTheme"> android:theme="@style/AppToolbarTheme">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar" android:id="@id/toolbar"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways" /> app:layout_scrollFlags="scroll|enterAlways" />
</com.google.android.material.appbar.AppBarLayout> </com.google.android.material.appbar.AppBarLayout>
<androidx.fragment.app.FragmentContainerView <androidx.fragment.app.FragmentContainerView
android:id="@id/container" android:id="@id/container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" /> app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton <com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab" android:id="@+id/fab"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_margin="16dp" android:layout_margin="16dp"
android:src="@drawable/ic_read_fill" android:src="@drawable/ic_read_fill"
android:visibility="gone" android:visibility="gone"
app:fabSize="normal" app:backgroundTint="?colorAccent"
app:backgroundTint="?colorAccent" app:fabSize="normal"
app:layout_anchor="@id/container" app:layout_anchor="@id/container"
app:layout_anchorGravity="bottom|end" app:layout_anchorGravity="bottom|end"
app:layout_dodgeInsetEdges="bottom" app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior"
app:layout_behavior="com.google.android.material.behavior.HideBottomViewOnScrollBehavior" app:layout_dodgeInsetEdges="bottom"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView <com.google.android.material.navigation.NavigationView
android:id="@+id/navigationView" android:id="@+id/navigationView"
android:layout_width="260dp" android:layout_width="260dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="start" android:layout_gravity="start"
app:headerLayout="@layout/view_nav_header" app:menu="@menu/nav_drawer" />
app:menu="@menu/nav_drawer" />
</androidx.drawerlayout.widget.DrawerLayout> </androidx.drawerlayout.widget.DrawerLayout>

View File

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

View File

@@ -5,10 +5,9 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/manga_list_details_item_height" android:layout_height="@dimen/manga_list_details_item_height"
app:cardBackgroundColor="?android:windowBackground"
app:cardElevation="0dp" app:cardElevation="0dp"
app:cardMaxElevation="0dp" app:cardMaxElevation="0dp"
app:strokeColor="?android:colorControlNormal" app:strokeColor="?colorOnSurface"
app:strokeWidth="1px"> app:strokeWidth="1px">
<LinearLayout <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"?> <?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"?> <?xml version="1.0" encoding="utf-8"?>
<menu <menu
xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AlwaysShowAction"> 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 <item
android:id="@+id/action_bookmark_add" android:id="@+id/action_bookmark_add"
android:icon="@drawable/ic_bookmark_add" android:icon="@drawable/ic_bookmark_add"
@@ -12,12 +19,6 @@
android:visible="false" android:visible="false"
app:showAsAction="always" /> 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 <item
android:id="@+id/action_pages_thumbs" android:id="@+id/action_pages_thumbs"
android:icon="@drawable/ic_grid" android:icon="@drawable/ic_grid"
@@ -30,10 +31,14 @@
android:title="@string/read_mode" android:title="@string/read_mode"
app:showAsAction="always" /> app:showAsAction="always" />
<item
android:id="@+id/action_save_page"
android:title="@string/save_page"
app:showAsAction="never" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:icon="@drawable/ic_tune"
android:title="@string/settings" android:title="@string/settings"
app:showAsAction="always" /> app:showAsAction="never" />
</menu> </menu>

View File

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

View File

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

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