Merge branch 'devel'

This commit is contained in:
Koitharu
2020-07-05 17:14:37 +03:00
50 changed files with 458 additions and 329 deletions

View File

@@ -3,6 +3,7 @@
<words> <words>
<w>chucker</w> <w>chucker</w>
<w>desu</w> <w>desu</w>
<w>failsafe</w>
<w>koin</w> <w>koin</w>
<w>kotatsu</w> <w>kotatsu</w>
<w>manga</w> <w>manga</w>

View File

@@ -16,7 +16,7 @@ android {
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode gitCommits versionCode gitCommits
versionName '0.5-b1' versionName '0.5-rc1'
kapt { kapt {
arguments { arguments {
@@ -64,13 +64,13 @@ dependencies {
implementation 'androidx.core:core-ktx:1.5.0-alpha01' implementation 'androidx.core:core-ktx:1.5.0-alpha01'
implementation 'androidx.activity:activity-ktx:1.2.0-alpha06' implementation 'androidx.activity:activity-ktx:1.2.0-alpha06'
implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06' implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha04' implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta6' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta7'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha03' implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha04'
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-beta01' implementation 'androidx.work:work-runtime-ktx:2.4.0-rc01'
implementation 'com.google.android.material:material:1.3.0-alpha01' implementation 'com.google.android.material:material:1.3.0-alpha01'
implementation 'androidx.room:room-runtime:2.2.5' implementation 'androidx.room:room-runtime:2.2.5'

View File

@@ -1,10 +1,9 @@
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.ImageLoaderBuilder
@@ -24,6 +23,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 +46,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))
@@ -75,7 +88,7 @@ class KotatsuApp : Application() {
single { single {
MangaLoaderContext() MangaLoaderContext()
} }
factory { single {
AppSettings(applicationContext) AppSettings(applicationContext)
} }
single { single {

View File

@@ -19,4 +19,10 @@ interface TrackLogsDao {
@Query("DELETE FROM track_logs WHERE manga_id = :mangaId") @Query("DELETE FROM track_logs WHERE manga_id = :mangaId")
suspend fun removeAll(mangaId: Long) suspend fun removeAll(mangaId: Long)
@Query("DELETE FROM track_logs WHERE manga_id NOT IN (SELECT manga_id FROM tracks)")
suspend fun cleanup()
@Query("SELECT COUNT(*) FROM track_logs")
suspend fun count(): Int
} }

View File

@@ -25,11 +25,13 @@ abstract class TracksDao {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId") @Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long) abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites)")
abstract suspend fun cleanup()
@Transaction @Transaction
open suspend fun upsert(entity: TrackEntity) { open suspend fun upsert(entity: TrackEntity) {
if (update(entity) == 0) { if (update(entity) == 0) {
insert(entity) insert(entity)
} }
} }
} }

View File

@@ -9,5 +9,6 @@ data class AppVersion(
val name: String, val name: String,
val url: String, val url: String,
val apkSize: Long, val apkSize: Long,
val apkUrl: String val apkUrl: String,
val description: String
) : Parcelable ) : Parcelable

View File

@@ -22,7 +22,8 @@ class GithubRepository : KoinComponent {
url = json.getString("html_url"), url = json.getString("html_url"),
name = json.getString("name").removePrefix("v"), name = json.getString("name").removePrefix("v"),
apkSize = asset.getLong("size"), apkSize = asset.getLong("size"),
apkUrl = asset.getString("browser_download_url") apkUrl = asset.getString("browser_download_url"),
description = json.getString("body")
) )
} }
} }

View File

@@ -3,6 +3,7 @@ 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 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
@@ -72,15 +73,13 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
} }
} }
fun delete(manga: Manga): Boolean { fun delete(manga: Manga): Boolean {
val file = Uri.parse(manga.url).toFile() val file = Uri.parse(manga.url).toFile()
return file.delete() return file.delete()
} }
@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)
@@ -99,14 +98,14 @@ class LocalMangaRepository : MangaRepository, KoinComponent {
} }
// 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,

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

View File

@@ -12,7 +12,7 @@ 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 = "henchan.pro"
override val source = MangaSource.HENCHAN override val source = MangaSource.HENCHAN
override suspend fun getDetails(manga: Manga): Manga { override suspend fun getDetails(manga: Manga): Manga {

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

@@ -13,7 +13,8 @@ import java.util.*
object MangaProviderFactory : KoinComponent { object MangaProviderFactory : KoinComponent {
private val loaderContext by inject<MangaLoaderContext>() private val loaderContext by inject<MangaLoaderContext>()
private val cache = EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java) private val cache =
EnumMap<MangaSource, WeakReference<MangaRepository>>(MangaSource::class.java)
fun getSources(includeHidden: Boolean): List<MangaSource> { fun getSources(includeHidden: Boolean): List<MangaSource> {
val settings = get<AppSettings>() val settings = get<AppSettings>()
@@ -33,24 +34,37 @@ object MangaProviderFactory : KoinComponent {
} }
} }
fun createLocal(): LocalMangaRepository = fun createLocal(): LocalMangaRepository {
(cache[MangaSource.LOCAL]?.get() as? LocalMangaRepository) var instance = cache[MangaSource.LOCAL]?.get()
?: LocalMangaRepository().also { if (instance == null) {
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(it) synchronized(cache) {
instance = cache[MangaSource.LOCAL]?.get()
if (instance == null) {
instance = LocalMangaRepository()
cache[MangaSource.LOCAL] = WeakReference<MangaRepository>(instance)
}
} }
}
return instance as LocalMangaRepository
}
@Throws(Throwable::class) @Throws(Throwable::class)
fun create(source: MangaSource): MangaRepository { fun create(source: MangaSource): MangaRepository {
cache[source]?.get()?.let { var instance = cache[source]?.get()
return it if (instance == null) {
synchronized(cache) {
instance = cache[source]?.get()
if (instance == null) {
instance = try {
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference(instance!!)
}
}
} }
val instance = try { return instance!!
source.cls.getDeclaredConstructor(MangaLoaderContext::class.java)
.newInstance(loaderContext)
} catch (e: NoSuchMethodException) {
source.cls.newInstance()
}
cache[source] = WeakReference<MangaRepository>(instance)
return instance
} }
} }

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

@@ -44,6 +44,17 @@ class TrackingRepository : KoinComponent {
} }
} }
suspend fun count() = db.trackLogsDao.count()
suspend fun clearLogs() = db.trackLogsDao.clear()
suspend fun cleanup() {
db.withTransaction {
db.tracksDao.cleanup()
db.trackLogsDao.cleanup()
}
}
suspend fun storeTrackResult( suspend fun storeTrackResult(
mangaId: Long, mangaId: Long,
knownChaptersCount: Int, knownChaptersCount: Int,

View File

@@ -29,7 +29,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
webView.webViewClient = BrowserClient(this) webView.webViewClient = BrowserClient(this)
val url = intent?.dataString val url = intent?.dataString
if (url.isNullOrEmpty()) { if (url.isNullOrEmpty()) {
finish() finishAfterTransition()
} else { } else {
webView.loadUrl(url) webView.loadUrl(url)
} }
@@ -43,7 +43,7 @@ class BrowserActivity : BaseActivity(), BrowserCallback {
override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {
android.R.id.home -> { android.R.id.home -> {
webView.stopLoading() webView.stopLoading()
finish() finishAfterTransition()
true true
} }
R.id.action_browser -> { R.id.action_browser -> {

View File

@@ -1,12 +1,10 @@
package org.koitharu.kotatsu.ui.common package org.koitharu.kotatsu.ui.common
import android.view.KeyEvent
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import moxy.MvpAppCompatActivity import moxy.MvpAppCompatActivity
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent { abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
@@ -29,13 +27,4 @@ abstract class BaseActivity : MvpAppCompatActivity(), KoinComponent {
onBackPressed() onBackPressed()
true true
} else super.onOptionsItemSelected(item) } else super.onOptionsItemSelected(item)
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
//TODO remove. Just for testing
if (BuildConfig.DEBUG && keyCode == KeyEvent.KEYCODE_VOLUME_UP) {
recreate()
return true
}
return super.onKeyDown(keyCode, event)
}
} }

View File

@@ -0,0 +1,25 @@
package org.koitharu.kotatsu.ui.common
import android.util.ArrayMap
import moxy.MvpPresenter
import java.lang.ref.WeakReference
abstract class SharedPresenterHolder<T : MvpPresenter<*>> {
private val cache = ArrayMap<Int, WeakReference<T>>(3)
fun getInstance(key: Int): T {
var instance = cache[key]?.get()
if (instance == null) {
instance = onCreatePresenter(key)
cache[key] = WeakReference(instance)
}
return instance
}
fun clear(key: Int) {
cache.remove(key)
}
protected abstract fun onCreatePresenter(key: Int): T
}

View File

@@ -25,7 +25,9 @@ class ChaptersFragment : BaseFragment(R.layout.fragment_chapters), MangaDetailsV
OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback { OnRecyclerItemClickListener<MangaChapter>, ActionMode.Callback {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null

View File

@@ -37,7 +37,9 @@ import org.koitharu.kotatsu.utils.ext.getThemeColor
class MangaDetailsActivity : BaseActivity(), MangaDetailsView, class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
TabLayoutMediator.TabConfigurationStrategy { TabLayoutMediator.TabConfigurationStrategy {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null
@@ -52,7 +54,7 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
presenter.loadDetails(it, true) presenter.loadDetails(it, true)
} ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let { } ?: intent?.getLongExtra(EXTRA_MANGA_ID, 0)?.takeUnless { it == 0L }?.let {
presenter.findMangaById(it) presenter.findMangaById(it)
} ?: finish() } ?: finishAfterTransition()
} }
} }
@@ -73,13 +75,13 @@ class MangaDetailsActivity : BaseActivity(), MangaDetailsView,
this, getString(R.string._s_deleted_from_local_storage, manga.title), this, getString(R.string._s_deleted_from_local_storage, manga.title),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
finish() finishAfterTransition()
} }
override fun onError(e: Throwable) { override fun onError(e: Throwable) {
if (manga == null) { if (manga == null) {
Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show() Toast.makeText(this, e.getDisplayMessage(resources), Toast.LENGTH_LONG).show()
finish() finishAfterTransition()
} else { } else {
Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show() Snackbar.make(pager, e.getDisplayMessage(resources), Snackbar.LENGTH_LONG).show()
} }

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 androidx.lifecycle.lifecycleScope
import coil.api.load import coil.api.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
@@ -29,7 +33,9 @@ class MangaDetailsFragment : BaseFragment(R.layout.fragment_details), MangaDetai
View.OnLongClickListener { View.OnLongClickListener {
@Suppress("unused") @Suppress("unused")
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
private var manga: Manga? = null private var manga: Manga? = null
private var history: MangaHistory? = null private var history: MangaHistory? = null
@@ -71,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

@@ -21,13 +21,13 @@ import org.koitharu.kotatsu.domain.history.HistoryRepository
import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener import org.koitharu.kotatsu.domain.history.OnHistoryChangeListener
import org.koitharu.kotatsu.domain.tracking.TrackingRepository import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePresenter import org.koitharu.kotatsu.ui.common.BasePresenter
import org.koitharu.kotatsu.ui.common.SharedPresenterHolder
import org.koitharu.kotatsu.utils.ext.safe import org.koitharu.kotatsu.utils.ext.safe
import java.io.IOException import java.io.IOException
@InjectViewState @InjectViewState
class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsView>(), class MangaDetailsPresenter private constructor(private val key: Int) :
OnHistoryChangeListener, BasePresenter<MangaDetailsView>(), OnHistoryChangeListener, OnFavouritesChangeListener {
OnFavouritesChangeListener {
private lateinit var historyRepository: HistoryRepository private lateinit var historyRepository: HistoryRepository
private lateinit var favouritesRepository: FavouritesRepository private lateinit var favouritesRepository: FavouritesRepository
@@ -55,7 +55,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
} ?: throw MangaNotFoundException("Cannot find manga by id") } ?: throw MangaNotFoundException("Cannot find manga by id")
viewState.onMangaUpdated(manga) viewState.onMangaUpdated(manga)
loadDetails(manga, true) loadDetails(manga, true)
} catch (_: CancellationException){ } catch (_: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
@@ -83,7 +83,7 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
viewState.onMangaUpdated(data) viewState.onMangaUpdated(data)
this@MangaDetailsPresenter.manga = data this@MangaDetailsPresenter.manga = data
viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id)) viewState.onNewChaptersChanged(trackingRepository.getNewChaptersCount(manga.id))
} catch (_: CancellationException){ } catch (_: CancellationException) {
} catch (e: Throwable) { } catch (e: Throwable) {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
e.printStackTrace() e.printStackTrace()
@@ -198,18 +198,12 @@ class MangaDetailsPresenter private constructor() : BasePresenter<MangaDetailsVi
override fun onDestroy() { override fun onDestroy() {
HistoryRepository.unsubscribe(this) HistoryRepository.unsubscribe(this)
FavouritesRepository.unsubscribe(this) FavouritesRepository.unsubscribe(this)
instance = null clear(key)
super.onDestroy() super.onDestroy()
} }
companion object { companion object Holder : SharedPresenterHolder<MangaDetailsPresenter>() {
private var instance: MangaDetailsPresenter? = null override fun onCreatePresenter(key: Int) = MangaDetailsPresenter(key)
fun getInstance(): MangaDetailsPresenter = instance ?: synchronized(this) {
MangaDetailsPresenter().also {
instance = it
}
}
} }
} }

View File

@@ -10,7 +10,9 @@ import org.koitharu.kotatsu.ui.list.MangaListFragment
class RelatedMangaFragment : MangaListFragment<Unit>(), MangaDetailsView { class RelatedMangaFragment : MangaListFragment<Unit>(), MangaDetailsView {
private val presenter by moxyPresenter(factory = MangaDetailsPresenter.Companion::getInstance) private val presenter by moxyPresenter {
MangaDetailsPresenter.getInstance(activity.hashCode())
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)

View File

@@ -92,6 +92,11 @@ class DownloadNotification(private val context: Context) {
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
} }
fun setWaitingForNetwork() {
builder.setProgress(0, 0, false)
builder.setContentText(context.getString(R.string.waiting_for_network))
}
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_))

View File

@@ -13,6 +13,7 @@ import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okio.IOException
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -24,10 +25,7 @@ import org.koitharu.kotatsu.domain.local.MangaZip
import org.koitharu.kotatsu.ui.common.BaseService import org.koitharu.kotatsu.ui.common.BaseService
import org.koitharu.kotatsu.ui.common.dialog.CheckBoxAlertDialog import org.koitharu.kotatsu.ui.common.dialog.CheckBoxAlertDialog
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
import org.koitharu.kotatsu.utils.ext.await import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.ext.retryUntilSuccess
import org.koitharu.kotatsu.utils.ext.safe
import org.koitharu.kotatsu.utils.ext.sub
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.collections.set import kotlin.collections.set
@@ -37,6 +35,7 @@ class DownloadService : BaseService() {
private lateinit var notification: DownloadNotification private lateinit var notification: DownloadNotification
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var connectivityManager: ConnectivityManager
private val okHttp by inject<OkHttpClient>() private val okHttp by inject<OkHttpClient>()
private val cache by inject<PagesCache>() private val cache by inject<PagesCache>()
@@ -47,6 +46,7 @@ class DownloadService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notification = DownloadNotification(this) notification = DownloadNotification(this)
connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
} }
@@ -88,9 +88,11 @@ class DownloadService : BaseService() {
try { try {
val repo = MangaProviderFactory.create(manga.source) val repo = MangaProviderFactory.create(manga.source)
val cover = safe { val cover = safe {
Coil.execute(GetRequestBuilder(this@DownloadService) Coil.execute(
.data(manga.coverUrl) GetRequestBuilder(this@DownloadService)
.build()).drawable .data(manga.coverUrl)
.build()
).drawable
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
notification.setLargeIcon(cover) notification.setLargeIcon(cover)
@@ -112,14 +114,23 @@ class DownloadService : BaseService() {
if (chaptersIds == null || chapter.id in chaptersIds) { if (chaptersIds == null || chapter.id in chaptersIds) {
val pages = repo.getPages(chapter) val pages = repo.getPages(chapter)
for ((pageIndex, page) in pages.withIndex()) { for ((pageIndex, page) in pages.withIndex()) {
val url = repo.getPageFullUrl(page) failsafe@ do {
val file = cache[url] ?: downloadPage(url, destination) try {
output.addPage( val url = repo.getPageFullUrl(page)
chapter, val file = cache[url] ?: downloadPage(url, destination)
file, output.addPage(
pageIndex, chapter,
MimeTypeMap.getFileExtensionFromUrl(url) file,
) pageIndex,
MimeTypeMap.getFileExtensionFromUrl(url)
)
} catch (e: IOException) {
notification.setWaitingForNetwork()
notification.update()
connectivityManager.waitForNetwork()
continue@failsafe
}
} while (false)
notification.setProgress( notification.setProgress(
chapters.size, chapters.size,
pages.size, pages.size,

View File

@@ -11,7 +11,6 @@ import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.postDelayed
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.swiperefreshlayout.widget.CircularProgressDrawable import androidx.swiperefreshlayout.widget.CircularProgressDrawable
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
@@ -26,18 +25,19 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
import org.koitharu.kotatsu.ui.common.BaseActivity import org.koitharu.kotatsu.ui.common.BaseActivity
import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment import org.koitharu.kotatsu.ui.list.favourites.FavouritesContainerFragment
import org.koitharu.kotatsu.ui.list.feed.FeedFragment
import org.koitharu.kotatsu.ui.list.history.HistoryListFragment import org.koitharu.kotatsu.ui.list.history.HistoryListFragment
import org.koitharu.kotatsu.ui.list.local.LocalListFragment import org.koitharu.kotatsu.ui.list.local.LocalListFragment
import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment import org.koitharu.kotatsu.ui.list.remote.RemoteListFragment
import org.koitharu.kotatsu.ui.list.feed.FeedFragment
import org.koitharu.kotatsu.ui.reader.ReaderActivity import org.koitharu.kotatsu.ui.reader.ReaderActivity
import org.koitharu.kotatsu.ui.reader.ReaderState import org.koitharu.kotatsu.ui.reader.ReaderState
import org.koitharu.kotatsu.ui.search.SearchHelper import org.koitharu.kotatsu.ui.search.SearchHelper
import org.koitharu.kotatsu.ui.settings.AppUpdateService import org.koitharu.kotatsu.ui.settings.AppUpdateChecker
import org.koitharu.kotatsu.ui.settings.SettingsActivity 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 {
@@ -46,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)
@@ -70,13 +71,12 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
} ?: run { } ?: run {
openDefaultSection() openDefaultSection()
} }
drawer.postDelayed(2000) {
AppUpdateService.startIfRequired(applicationContext)
}
TrackWorker.setup(applicationContext) TrackWorker.setup(applicationContext)
AppUpdateChecker(this).invoke()
} }
override fun onDestroy() { override fun onDestroy() {
closeable?.close()
settings.unsubscribe(this) settings.unsubscribe(this)
super.onDestroy() super.onDestroy()
} }
@@ -95,7 +95,7 @@ class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedList
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

@@ -41,9 +41,9 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener { SectionItemDecoration.Callback, SwipeRefreshLayout.OnRefreshListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val adapterConfig = MergeAdapter.Config.Builder() private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true) .setIsolateViewTypes(true)
.setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS) .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build() .build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
@@ -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
} }
} }
@@ -245,18 +247,17 @@ abstract class MangaListFragment<E> : BaseFragment(R.layout.fragment_list),
adapter?.listMode = mode adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> { ListMode.GRID -> {
val spanCount = UiUtils.resolveGridSpanCount(ctx) GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
GridLayoutManager(ctx, spanCount).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount()) override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else spanCount 1 else this@apply.spanCount
} }
} }
} }
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.recycledViewPool.clear() recyclerView.recycledViewPool.clear()
recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter) recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -36,9 +36,9 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener { SharedPreferences.OnSharedPreferenceChangeListener, Toolbar.OnMenuItemClickListener {
private val settings by inject<AppSettings>() private val settings by inject<AppSettings>()
private val adapterConfig = MergeAdapter.Config.Builder() private val adapterConfig = ConcatAdapter.Config.Builder()
.setIsolateViewTypes(true) .setIsolateViewTypes(true)
.setStableIdMode(MergeAdapter.Config.StableIdMode.SHARED_STABLE_IDS) .setStableIdMode(ConcatAdapter.Config.StableIdMode.SHARED_STABLE_IDS)
.build() .build()
private var adapter: MangaListAdapter? = null private var adapter: MangaListAdapter? = null
@@ -181,17 +181,16 @@ abstract class MangaListSheet<E> : BaseBottomSheet(R.layout.sheet_list),
adapter?.listMode = mode adapter?.listMode = mode
recyclerView.layoutManager = when (mode) { recyclerView.layoutManager = when (mode) {
ListMode.GRID -> { ListMode.GRID -> {
val spanCount = UiUtils.resolveGridSpanCount(ctx) GridLayoutManager(ctx, UiUtils.resolveGridSpanCount(ctx)).apply {
GridLayoutManager(ctx, spanCount).apply {
spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
override fun getSpanSize(position: Int) = if (position < getItemsCount()) override fun getSpanSize(position: Int) = if (position < getItemsCount())
1 else spanCount 1 else this@apply.spanCount
} }
} }
} }
else -> LinearLayoutManager(ctx) else -> LinearLayoutManager(ctx)
} }
recyclerView.adapter = MergeAdapter(adapterConfig, adapter, progressAdapter) recyclerView.adapter = ConcatAdapter(adapterConfig, adapter, progressAdapter)
recyclerView.addItemDecoration( recyclerView.addItemDecoration(
when (mode) { when (mode) {
ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL) ListMode.LIST -> DividerItemDecoration(ctx, RecyclerView.VERTICAL)

View File

@@ -13,7 +13,6 @@ import org.koin.core.get
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException import org.koitharu.kotatsu.core.exceptions.UnsupportedFileException
import org.koitharu.kotatsu.core.model.Manga import org.koitharu.kotatsu.core.model.Manga
import org.koitharu.kotatsu.core.model.MangaSource
import org.koitharu.kotatsu.core.parser.LocalMangaRepository import org.koitharu.kotatsu.core.parser.LocalMangaRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.domain.MangaProviderFactory import org.koitharu.kotatsu.domain.MangaProviderFactory
@@ -33,16 +32,17 @@ class LocalListPresenter : BasePresenter<MangaListView<File>>() {
private lateinit var repository: LocalMangaRepository private lateinit var repository: LocalMangaRepository
override fun onFirstViewAttach() { override fun onFirstViewAttach() {
repository = MangaProviderFactory.create(MangaSource.LOCAL) as LocalMangaRepository repository = MangaProviderFactory.createLocal()
super.onFirstViewAttach() super.onFirstViewAttach()
} }
fun loadList(offset: Int) { fun loadList(offset: Int) {
if (offset != 0) {
viewState.onListAppended(emptyList())
return
}
presenterScope.launch { presenterScope.launch {
if (offset != 0) {
viewState.onListAppended(emptyList())
return@launch
}
viewState.onLoadingStateChanged(true) viewState.onLoadingStateChanged(true)
try { try {
val list = withContext(Dispatchers.IO) { val list = withContext(Dispatchers.IO) {

View File

@@ -1,6 +1,7 @@
package org.koitharu.kotatsu.ui.reader package org.koitharu.kotatsu.ui.reader
import android.net.Uri import android.net.Uri
import android.util.ArrayMap
import kotlinx.coroutines.* import kotlinx.coroutines.*
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@@ -16,7 +17,7 @@ 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>()
@@ -30,7 +31,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 +49,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"
} }

View File

@@ -75,7 +75,7 @@ class ReaderActivity : BaseFullscreenActivity(), ReaderView, ChaptersDialog.OnCh
?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE) ?: intent.getParcelableExtra<ReaderState>(EXTRA_STATE)
?: let { ?: let {
Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show() Toast.makeText(this, R.string.error_occurred, Toast.LENGTH_SHORT).show()
finish() finishAfterTransition()
return return
} }

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

@@ -19,7 +19,7 @@ class SearchActivity : BaseActivity(), SearchView.OnQueryTextListener {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search) setContentView(R.layout.activity_search)
source = intent.getParcelableExtra(EXTRA_SOURCE) ?: run { source = intent.getParcelableExtra(EXTRA_SOURCE) ?: run {
finish() finishAfterTransition()
return return
} }
val query = intent.getStringExtra(EXTRA_QUERY) val query = intent.getStringExtra(EXTRA_QUERY)
@@ -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

@@ -14,7 +14,7 @@ class GlobalSearchActivity : BaseActivity() {
val query = intent.getStringExtra(EXTRA_QUERY) val query = intent.getStringExtra(EXTRA_QUERY)
if (query == null) { if (query == null) {
finish() finishAfterTransition()
return return
} }

View File

@@ -0,0 +1,128 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.KoinComponent
import org.koin.core.inject
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateChecker(private val activity: ComponentActivity) : KoinComponent {
private val settings by inject<AppSettings>()
operator fun invoke() {
if (isUpdateSupported(activity) && settings.appUpdateAuto && settings.appUpdate + PERIOD < System.currentTimeMillis()) {
launch()
}
}
private fun launch() = activity.lifecycleScope.launch {
try {
val repo = GithubRepository()
val version = withContext(Dispatchers.IO) {
repo.getLatestVersion()
}
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
showUpdateDialog(version)
}
settings.appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
}
}
private fun showUpdateDialog(version: AppVersion) {
MaterialAlertDialogBuilder(activity)
.setTitle(R.string.app_update_available)
.setMessage(buildString {
append(activity.getString(R.string.new_version_s, version.name))
appendln()
append(activity.getString(R.string.size_s, FileSizeUtils.formatBytes(activity, version.apkSize)))
appendln()
appendln()
append(version.description)
})
.setPositiveButton(R.string.download) { _, _ ->
activity.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(version.apkUrl)))
}
.setNegativeButton(R.string.close, null)
.create()
.show()
}
companion object {
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? {
val packageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert)
val c = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
e.printStackTrace()
return null
}
return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -1,178 +0,0 @@
package org.koitharu.kotatsu.ui.settings
import android.annotation.SuppressLint
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.BitmapFactory
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.github.AppVersion
import org.koitharu.kotatsu.core.github.GithubRepository
import org.koitharu.kotatsu.core.github.VersionId
import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.ui.common.BaseService
import org.koitharu.kotatsu.utils.FileSizeUtils
import org.koitharu.kotatsu.utils.ext.byte2HexFormatted
import java.io.ByteArrayInputStream
import java.io.InputStream
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import java.security.cert.CertificateException
import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
class AppUpdateService : BaseService() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
launch(Dispatchers.IO) {
try {
val repo = GithubRepository()
val version = repo.getLatestVersion()
val newVersionId = VersionId.parse(version.name)
val currentVersionId = VersionId.parse(BuildConfig.VERSION_NAME)
if (newVersionId > currentVersionId) {
withContext(Dispatchers.Main) {
showUpdateNotification(version)
}
}
AppSettings(this@AppUpdateService).appUpdate = System.currentTimeMillis()
} catch (_: CancellationException) {
} catch (e: Throwable) {
if (BuildConfig.DEBUG) {
e.printStackTrace()
}
} finally {
withContext(Dispatchers.Main) {
stopSelf(startId)
}
}
}
return START_NOT_STICKY
}
private fun showUpdateNotification(newVersion: AppVersion) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
&& manager.getNotificationChannel(CHANNEL_ID) == null
) {
val channel = NotificationChannel(
CHANNEL_ID,
getString(R.string.application_update),
NotificationManager.IMPORTANCE_DEFAULT
)
manager.createNotificationChannel(channel)
}
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
builder.setContentTitle(getString(R.string.app_update_available))
builder.setContentText(buildString {
append(newVersion.name)
append(" (")
append(FileSizeUtils.formatBytes(this@AppUpdateService, newVersion.apkSize))
append(')')
})
builder.setContentIntent(
PendingIntent.getActivity(
this,
NOTIFICATION_ID,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.url)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.addAction(
R.drawable.ic_download, getString(R.string.download),
PendingIntent.getActivity(
this,
NOTIFICATION_ID + 1,
Intent(Intent.ACTION_VIEW, Uri.parse(newVersion.apkUrl)),
PendingIntent.FLAG_CANCEL_CURRENT
)
)
builder.setSmallIcon(R.drawable.ic_stat_update)
builder.setAutoCancel(true)
builder.color = ContextCompat.getColor(this, R.color.blue_primary_dark)
builder.setLargeIcon(BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher))
manager.notify(NOTIFICATION_ID, builder.build())
}
companion object {
private const val CERT_SHA1 = "2C:19:C7:E8:07:61:2B:8E:94:51:1B:FD:72:67:07:64:5D:C2:58:AE"
private const val NOTIFICATION_ID = 202
private const val CHANNEL_ID = "update"
private val PERIOD = TimeUnit.HOURS.toMillis(6)
fun isUpdateSupported(context: Context): Boolean {
return getCertificateSHA1Fingerprint(context) == CERT_SHA1
}
fun startIfRequired(context: Context) {
if (!isUpdateSupported(context)) {
return
}
val settings = AppSettings(context)
if (settings.appUpdateAuto) {
val lastUpdate = settings.appUpdate
if (lastUpdate + PERIOD < System.currentTimeMillis()) {
start(context)
}
}
}
private fun start(context: Context) {
try {
context.startService(Intent(context, AppUpdateService::class.java))
} catch (_: IllegalStateException) {
}
}
@Suppress("DEPRECATION")
@SuppressLint("PackageManagerGetSignatures")
private fun getCertificateSHA1Fingerprint(context: Context): String? {
val packageInfo = try {
context.packageManager.getPackageInfo(
context.packageName,
PackageManager.GET_SIGNATURES
)
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
return null
}
val signatures = packageInfo?.signatures
val cert: ByteArray = signatures?.firstOrNull()?.toByteArray() ?: return null
val input: InputStream = ByteArrayInputStream(cert)
val c = try {
val cf = CertificateFactory.getInstance("X509")
cf.generateCertificate(input) as X509Certificate
} catch (e: CertificateException) {
e.printStackTrace()
return null
}
return try {
val md: MessageDigest = MessageDigest.getInstance("SHA1")
val publicKey: ByteArray = md.digest(c.encoded)
publicKey.byte2HexFormatted()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
null
} catch (e: CertificateEncodingException) {
e.printStackTrace()
null
}
}
}
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.local.Cache import org.koitharu.kotatsu.core.local.Cache
import org.koitharu.kotatsu.domain.tracking.TrackingRepository
import org.koitharu.kotatsu.ui.common.BasePreferenceFragment import org.koitharu.kotatsu.ui.common.BasePreferenceFragment
import org.koitharu.kotatsu.ui.search.MangaSuggestionsProvider import org.koitharu.kotatsu.ui.search.MangaSuggestionsProvider
import org.koitharu.kotatsu.utils.CacheUtils import org.koitharu.kotatsu.utils.CacheUtils
@@ -17,6 +18,10 @@ import org.koitharu.kotatsu.utils.ext.getDisplayMessage
class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) { class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cache) {
private val trackerRepo by lazy {
TrackingRepository()
}
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
addPreferencesFromResource(R.xml.pref_history) addPreferencesFromResource(R.xml.pref_history)
findPreference<Preference>(R.string.key_pages_cache_clear)?.let { pref -> findPreference<Preference>(R.string.key_pages_cache_clear)?.let { pref ->
@@ -39,10 +44,16 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
val items = MangaSuggestionsProvider.getItemsCount(p.context) val items = MangaSuggestionsProvider.getItemsCount(p.context)
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items) p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
} }
findPreference<Preference>(R.string.key_updates_feed_clear)?.let { p ->
lifecycleScope.launchWhenResumed {
val items = trackerRepo.count()
p.summary = p.context.resources.getQuantityString(R.plurals.items, items, items)
}
}
} }
override fun onPreferenceTreeClick(preference: Preference): Boolean { override fun onPreferenceTreeClick(preference: Preference): Boolean {
return when(preference.key) { return when (preference.key) {
getString(R.string.key_pages_cache_clear) -> { getString(R.string.key_pages_cache_clear) -> {
clearCache(preference, Cache.PAGES) clearCache(preference, Cache.PAGES)
true true
@@ -53,8 +64,26 @@ class HistorySettingsFragment : BasePreferenceFragment(R.string.history_and_cach
} }
getString(R.string.key_search_history_clear) -> { getString(R.string.key_search_history_clear) -> {
MangaSuggestionsProvider.clearHistory(preference.context) MangaSuggestionsProvider.clearHistory(preference.context)
preference.context.resources.getQuantityString(R.plurals.items, 0, 0) preference.summary = preference.context.resources
Snackbar.make(view ?: return true, R.string.search_history_cleared, Snackbar.LENGTH_SHORT).show() .getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return true,
R.string.search_history_cleared,
Snackbar.LENGTH_SHORT
).show()
true
}
getString(R.string.key_updates_feed_clear) -> {
lifecycleScope.launch {
trackerRepo.clearLogs()
preference.summary = preference.context.resources
.getQuantityString(R.plurals.items, 0, 0)
Snackbar.make(
view ?: return@launch,
R.string.updates_feed_cleared,
Snackbar.LENGTH_SHORT
).show()
}
true true
} }
else -> super.onPreferenceTreeClick(preference) else -> super.onPreferenceTreeClick(preference)

View File

@@ -42,7 +42,7 @@ class MainSettingsFragment : BasePreferenceFragment(R.string.settings),
findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider = findPreference<MultiSelectListPreference>(R.string.key_reader_switchers)?.summaryProvider =
MultiSummaryProvider(R.string.gestures_only) MultiSummaryProvider(R.string.gestures_only)
findPreference<Preference>(R.string.key_app_update_auto)?.run { findPreference<Preference>(R.string.key_app_update_auto)?.run {
isVisible = AppUpdateService.isUpdateSupported(context) isVisible = AppUpdateChecker.isUpdateSupported(context)
} }
findPreference<Preference>(R.string.key_local_storage)?.run { findPreference<Preference>(R.string.key_local_storage)?.run {
summary = settings.getStorageDir(context)?.getStorageName(context) summary = settings.getStorageDir(context)?.getStorageName(context)

View File

@@ -118,6 +118,7 @@ class TrackWorker(context: Context, workerParams: WorkerParameters) :
} }
success++ success++
} }
repo.cleanup()
if (success == 0) { if (success == 0) {
Result.retry() Result.retry()
} else { } else {

View File

@@ -48,7 +48,7 @@ class ShelfConfigActivity : BaseActivity(), FavouriteCategoriesView,
AppWidgetManager.INVALID_APPWIDGET_ID AppWidgetManager.INVALID_APPWIDGET_ID
) ?: AppWidgetManager.INVALID_APPWIDGET_ID ) ?: AppWidgetManager.INVALID_APPWIDGET_ID
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) { if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
finish() finishAfterTransition()
return return
} }
config = AppWidgetConfig.getInstance(this, appWidgetId) config = AppWidgetConfig.getInstance(this, appWidgetId)

View File

@@ -0,0 +1,22 @@
package org.koitharu.kotatsu.utils.ext
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
suspend fun ConnectivityManager.waitForNetwork(): Network {
val request = NetworkRequest.Builder().build()
return suspendCancellableCoroutine<Network> { cont ->
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
cont.resume(network)
}
}
registerNetworkCallback(request, callback)
cont.invokeOnCancellation {
unregisterNetworkCallback(callback)
}
}
}

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

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

View File

@@ -137,4 +137,9 @@
<string name="text_feed_holder">Здесь будут отображаться обновления манги, которую Вы читаете</string> <string name="text_feed_holder">Здесь будут отображаться обновления манги, которую Вы читаете</string>
<string name="search_results">Результаты поиска</string> <string name="search_results">Результаты поиска</string>
<string name="related">Похожие</string> <string name="related">Похожие</string>
<string name="new_version_s">Новая версия: %s</string>
<string name="size_s">Размер: %s</string>
<string name="waiting_for_network">Ожидание подключения…</string>
<string name="clear_updates_feed">Очистить ленту обновлений</string>
<string name="updates_feed_cleared">Лента обновлений очищена</string>
</resources> </resources>

View File

@@ -9,6 +9,7 @@
<string name="key_pages_cache_clear">pages_cache_clear</string> <string name="key_pages_cache_clear">pages_cache_clear</string>
<string name="key_thumbs_cache_clear">thumbs_cache_clear</string> <string name="key_thumbs_cache_clear">thumbs_cache_clear</string>
<string name="key_search_history_clear">search_history_clear</string> <string name="key_search_history_clear">search_history_clear</string>
<string name="key_updates_feed_clear">updates_feed_clear</string>
<string name="key_grid_size">grid_size</string> <string name="key_grid_size">grid_size</string>
<string name="key_remote_sources">remote_sources</string> <string name="key_remote_sources">remote_sources</string>
<string name="key_local_storage">local_storage</string> <string name="key_local_storage">local_storage</string>

View File

@@ -138,4 +138,9 @@
<string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string> <string name="text_feed_holder">Here you will see the new chapters of the manga you are reading</string>
<string name="search_results">Search results</string> <string name="search_results">Search results</string>
<string name="related">Related</string> <string name="related">Related</string>
<string name="new_version_s">New version: %s</string>
<string name="size_s">Size: %s</string>
<string name="waiting_for_network">Waiting for network…</string>
<string name="clear_updates_feed">Clear updates feed</string>
<string name="updates_feed_cleared">Updates feed cleared</string>
</resources> </resources>

View File

@@ -9,6 +9,12 @@
android:title="@string/clear_search_history" android:title="@string/clear_search_history"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<Preference
android:key="@string/key_updates_feed_clear"
android:persistent="false"
android:title="@string/clear_updates_feed"
app:iconSpaceReserved="false" />
<PreferenceCategory <PreferenceCategory
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
android:title="@string/cache"> android:title="@string/cache">

View File

@@ -6,7 +6,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:4.2.0-alpha01' classpath 'com.android.tools.build:gradle:4.2.0-alpha03'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong

View File

@@ -1,6 +1,6 @@
#Sat Jun 13 15:51:48 EEST 2020 #Wed Jul 01 18:26:34 EEST 2020
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip